Default implementation of session-based authentication

Published at: 2020-09-10 16:00:00

Updated at: 2023-10-01 13:59:37

session-auth

So today I'm gonna show you how to setup a default implementation of session-based auth. When I say default, meaning that we are not going to use any other kind of session store other than MemoryStore.

When using session-based auth, we basically able to choose whether to use among the three types of session storage, and they are:-

  1. Memory (eg: file system)
  2. Cache (eg: Redis or MemoryCache)
  3. DB (eg: MongoDB or Postgres)

Depends on what session storage you ended up using, you should know that it will pretty much affect your query time. Knowing which one is the best choice could take a lot of reading, comparing and even testing. Personally as of right now, I only know the default one, MemoryStore and Redis. But for now, I'll share the default one and please do remember to read through the comments between the line of codes too. It will help you understand the code much better.

In this example, we'll be using NodeJS to illustrate the use of session-based auth. So the main package for session handling is express-session.

Below are the list of steps involve in this implementation:-

  1. setup db and model (data schema)
  2. setup main index.js (or server.js)
  3. setup auth.js and home.js routes
  4. setup auth.js session handler
  5. setup verification for user's auth

1. Setup db and model

/* Dependencies */
// Mongoose
const mongoose = require('mongoose')
 
// Data Scheme
const allSchema = new mongoose.Schema({
    // User's Credential
    credentials: {
        // User's Email
        email: { type: String, required: true, min: 6 },
        // User's password
        password: { type: String, required: true, min: 6 }
    },
    // User's Information
    profiles: {
        // User's Name
        name: {
            // User's Firstname
            firstName: { type: String, required: true, min: 4 },
            // User's Lastname
            lastName: { type: String, required: true, min: 4 }
        }
    }
})
 
// Data Scheme Export
module.exports = mongoose.model('All', allSchema)

2. Setup main index.js

/** Dependencies */
// Express
const express = require('express')
const app = express()
// Authentication purposes (express-session)
const session = require('express-session')
// Dotenv
const dotenv = require('dotenv')
dotenv.config()
// Mongoose
const mongoose = require('mongoose')
// Default Env
const { isActive, SESSION_OPTIONS } = require('../final/config/index')
 
/** Global Middlewares */
// EJS
app.set('view engine', 'ejs')
// JSON Body Parser
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
// Session create
app.use(session({
    ...SESSION_OPTIONS
}))
// track user ABSOLUTE TIMEOUT
app.use(isActive)
 
/** Routes Middleware */
// AUTH Routes
app.use('/auth', require('../final/routes/auth'))
// HOME Routes
app.use('/', require('../final/routes/home'))
 
/** Database Connection & Server Startup */
mongoose.connect(
    process.env.DB_CONNECT,
    { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
)
// if connection SUCCESS, then START the Server
.then(() => {
    console.log('Successfully connected to database!')
    app.listen(process.env.PORT || 3000, console.log(`Server is up and running at PORT ${process.env.PORT}!`))
})
// if connection UNSUCCESSFUL, then DON'T START the Server
.catch(err => {
    console.log(err)
})

3. Setup auth.js and home.js routes

  • auth.js:-
/* Dependencies */
// Express & Routes
const router = require('express').Router()
// Model
const All = require('../models/All')
// Hashing Password
const bcrypt = require('bcryptjs')
// Validation
const { registerValidation, loginValidation } = require('../controllers/validations')
// Verification
const { redirectLogin, redirectHome} = require('../controllers/verifications')
// Default Env
const { logIn, logOut } = require('../config/index')
 
/* Routes */
// REGISTER Post Router
router.post('/register', redirectHome, async(req, res) => {
    let {
        // user's email
        email,
        // user's password
        password,
        // user's firstName
        firstName,
        // user's lastName
        lastName
    } = req.body
 
    // 1. check email existance
    const emailExist = await All.findOne({ 'credentials.email': email })
    // - if exist, then throw error
    if(emailExist) return res.json({ message: 'Email already exist!' })
 
    // 2. hash password
    const salt = await bcrypt.genSalt(10)
    const hashedPassword = await bcrypt.hash(password, salt)
 
    // 3. create new user
    const user = new All({
        credentials: {
            email: email,
            password: hashedPassword
        },
        profiles: {
            name: {
                firstName: firstName,
                lastName: lastName
            }
        }
    })
 
    // 4. save new user
    try {
        const saveNewUser = await user.save()
        res.json({ message: "User successfully saved!" })
    } catch(err) {
        res.json({ message: "Unsuccessfully save new user!" })
    }
})
 
// LOGIN Get Router (Login page)
router.get('/', redirectHome, async(req, res) => {
    // render Login page
    res.send(`
        <h2>Login Page</h2>
        <p>Insert your credential below:-</p>
        <form action='/auth' method='POST'>
            <input type="email" name="email" placeholder="Username" required>
            <input type="password" name="password" placeholder="Password" required>
            <button type="submit">Login</button>
        </form>
    `)
})
 
// LOGIN Post Router
router.post('/', redirectHome, async(req, res) => {
    let {
        // user's email
        email,
        // user's password
        password
    } = req.body
 
    // 1. check email existance
    const user = await All.findOne({ 'credentials.email': email })
    // - if don't exist, then throw error
    if(!user) return res.render('auth/login', { message: `Email doesn't exist!` })
 
    // 2. check password matching
    // - try matching compare & password with DB
    const validPassword = await bcrypt.compare(password, user.credentials.password)
    // - if not match, then throw error
    if(!validPassword) return res.render('auth/login', { message: `Invalid password! Make sure you insert the correct password.` })
 
    // 3. assign new session info to user
    logIn(req, user._id)
 
    // 4. redirect user to HOME page
    return res.redirect('/')
})
 
// LOGOUT Post Router
router.post('/logout', redirectLogin, async (req, res) => {
    // handle user session destroy
    await logOut(req, res)
})
 
/* Auth Routes Export */
module.exports = router
  • home.js:-
/* Dependencies */
// Express & Routes
const router = require('express').Router()
// Model
const All = require('../models/All')
// Verification
const { redirectLogin } = require('../controllers/verifications')
 
/* Routes */
// HOME Get Router
router.get('/', redirectLogin, async(req, res) => {
    // get user from DB (using req.session.userId)
    All.findById(
        { _id: req.session.userId }
    ).then(user => {
        // data to send
        let data = {
            email: user.credentials.email,
            name: `${user.profiles.name.firstName} ${user.profiles.name.lastName}`
        }
 
        // render Home page
        return res.send(`
            <h1>Welcome to homepage!</h1>
            <div>
                <p>Email: ${data.email}</p>
                <p>Email: ${data.name}</p>
            </div>
            <form action='/auth/logout' method='POST'>
                <button type="submit">Logout</button>
            </form>
        `)
    }).catch(err => res.json({ message: err }))    
})
 
/* Auth Routes Export */
module.exports = router

4. Setup auth.js session handler

/** Dependencies */
// Express
// const { request, response } = require('express')
// Default Env
const { SESS_NAME, SESS_ABSOLUTE_TIMEOUT } = require('../config/session')
 
// check if user logged in
function isLoggedIn(req) {
    if(req.session.userId) return true
    else return false
}
 
// assign new session info to login user
function logIn(req, userId) {
    // set session userId to user._id
    req.session.userId = userId
 
    // set session createdAt (to track ABSOLUTE TIMEOUT)
    req.session.createdAt = Date.now()
}
 
// handle session destroy on logout
async function logOut(req, res) {
    // destroy user session
    req.session.destroy(err => {
        // if err, then return to Home page
        if(err) return res.redirect('/')
 
        // clear out cookies
        res.clearCookie(SESS_NAME)
 
        // return to Login page
        return res.redirect('/auth')
    })
}
 
// check if user is active (forced logout)
async function isActive (req, res, next) {
    // check if user isloggedIn
    if(isLoggedIn(req) === true){
        // get time stamp NOW
        const now = Date.now()
        // get time stamp BEFORE (the one created once the user loggedIn)
        const { createdAt } = req.session
 
        // check if user already exceed (active/non-active) time in the system
        // - eg: 10AM > 8AM + 4Hours (then user should be forced to logout)
        if(now > createdAt + SESS_ABSOLUTE_TIMEOUT) {
            // logout user
            return await logOut(req, res)
        }
    }
 
    next()
}
 
/** Module Exports */
module.exports = {
    logIn,
    logOut,
    isActive
}

5. Setup verification for user's auth

/** Dependencies */
// Express
// const { request, response, next } = require('express')
 
// redirect NOT log in user
function redirectLogin(req, res, next) {
    if(!req.session.userId) {
        // return res.json({ message: 'Access denied!' })
        // redirect to Login page
        res.redirect('/auth')
    } else {
        next()
    }
}
 
// redirect ALREADY logged in user
function redirectHome(req, res, next) {
    if(req.session.userId) {
        // return res.json({ message: 'Access denied!' })
        // redirect to Home page
        res.redirect('/')
    } else {
        next()
    }
}
 
/** Module Exports */
module.exports = {
    redirectLogin,
    redirectHome
}

That's how you implement a simple session-based authentication. Of course you could add more security with adding a proper validation layer. But that, I will show them in the next article.

Anyway, I hope now you able to implement a proper session-based authentication. If you want to follow along me coding this from the scratch, then you can go to my Youtube video link here, Implementation of Session-based Authentication - Default Version. And this is the link to the Github repo, Simple Session Authentication.

Like always, thanks for being here. Until next time, Chao! Salam.