In this post, we will learn how to authenticate users using session cookies in a Node.js server application.

banner

When a user logs into our application, we need to know who they are across all our HTTP methods and routes.

One way to do this is to store the users “session”. A session is started once a user logs in, and expires some time after that.

Each logged in user has some reference to the session (called a cookie), which they send with their requests. We then use this reference to look up the user that it belongs to and return information specific to them.

If you just want to see the source code for this tutorial, you can find it here

Overview

In this post, we will look at how to create and store the session of a logged in user as a cookie on the browser.

We will build an application with a /signin and a /welcome route.

  • The /signin route will accept a users username and password, and set a session cookie if successful.
  • The /welcome route will be a simple HTTP GET route which will show a personalized message to the currently logged in user.

The session information of the user will be stored in our applications local memory.

We will also assume that the users that are signing in have already created their username-password credentials on our application.

Creating the HTTP Server

Before we begin, we need to install the libraries that we will use for this example:

npm install express body-parser cookie-parser uuid

Let’s create an index.js file and initialize the HTTP server with the required routes:

const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const { signinHandler, welcomeHandler, refreshHandler } = require('./handlers')

const app = express()
app.use(bodyParser.json())
app.use(cookieParser())

app.post('/signin', signinHandler)
app.get('/welcome', welcomeHandler)

app.listen(8080)

We can now define the /signin and /welcome routes.

Creating Session Tokens

We will be creating a new session token every time a user signs in.

The /signin route will take the users credentials and log them in.

In order to make this simple, we’re storing the users information as an in-memory object in our code:

const users = {
    "user1": "password1",
    "user2": "password2"
}

For now, there are only two valid users in our application: user1, and user2.

We also need to define an object to store information about each users session:

// each session contains the username of the user and the time at which it expires
class Session {
    constructor(username, expiresAt) {
        this.username = username
        this.expiresAt = expiresAt
    }

		// we'll use this method later to determine if the session has expired
    isExpired() {
        this.expiresAt < (new Date())
    }
}

// this object stores the users sessions. For larger scale applications, you can use a database or cache for this purpose
const sessions = {}

Next, we can implement the Signin HTTP handler:

const signinHandler = (req, res) => {
    // get users credentials from the JSON body
    const { username, password } = req.body
    if (!username) {
        // If the username isn't present, return an HTTP unauthorized code
        res.status(401).end()
        return
    }

    // validate the password against our data
    // if invalid, send an unauthorized code
    const expectedPassword = users[username]
    if (!expectedPassword || expectedPassword !== password) {
        res.status(401).end()
        return
    }

    // generate a random UUID as the session token
    const sessionToken = uuid.v4()

    // set the expiry time as 120s after the current time
    const now = new Date()
    const expiresAt = new Date(+now + 120 * 1000)

    // create a session containing information about the user and expiry time
    const session = new Session(username, expiresAt)
    // add the session information to the sessions map
    sessions[sessionToken] = session

    // In the response, set a cookie on the client with the name "session_cookie"
    // and the value as the UUID we generated. We also set the expiry time
    res.cookie("session_token", sessionToken, { expires: expiresAt })
    res.end()
}

sign in diagram

If a user logs in successfully, this handler will set a cookie on the client side, and inside its own local memory. Once a cookie is set on a client, it is sent along with every subsequent request.

We can use UUIDs to represent session tokens, since they are uniform in structure, and difficult to guess.

Authenticating Users Through Session Cookies

Now that we have persisted the users session information on their client (in the form of the session_token cookie) and the server, we can write our /welcome handler to handle user specific information.

Since logged in clients have session information stored on their end as cookies, we can use it to:

  • Authenticate subsequent user requests
  • Get information about the user making the request

Let’s write our /welcome handler to do that:

const welcomeHandler = (req, res) => {
    // if this request doesn't have any cookies, that means it isn't
    // authenticated. Return an error code.
    if (!req.cookies) {
        res.status(401).end()
        return
    }

    // We can obtain the session token from the requests cookies, which come with every request
    const sessionToken = req.cookies['session_token']
    if (!sessionToken) {
        // If the cookie is not set, return an unauthorized status
        res.status(401).end()
        return
    }

    // We then get the session of the user from our session map
    // that we set in the signinHandler
    userSession = sessions[sessionToken]
    if (!userSession) {
        // If the session token is not present in session map, return an unauthorized error
        res.status(401).end()
        return
    }
    // if the session has expired, return an unauthorized error, and delete the 
    // session from our map
    if (userSession.isExpired()) {
        delete sessions[sessionToken]
        res.status(401).end()
        return
    }

    // If all checks have passed, we can consider the user authenticated and
    // send a welcome message
    res.send(`Welcome  ${userSession.username}!`).end()
}

From the code, we can see that our welcome handler gives us an “unauthorized” (or 401) status under certain circumstances:

  1. If there is no session_token cookie along with the request (which means that the requestor hasn’t logged in)
  2. If the session token is not present in memory (which means that the requestor is sending us an invalid session token)
  3. If the session has expired

welcome diagram

Session based authentication keeps your users sessions secure in a couple of ways:

  1. Since the session tokens are randomly generated, its near-impossible for a malicious user to brute-force their way into a users session.
  2. If a users session token is compromised somehow, it cannot be used after its expiry. This is why the expiry time is restricted to small intervals (a few seconds to a couple of minutes)

Refreshing Session Tokens

Since the expiry time of a session token is kept small, we need to issue a new token often to keep the user logged in.

Of course, we cannot expect the user to login every time their token expires. To solve this, we can create another route that accepts the users current session token, and issues a new session token with a renewed expiry time.

Lets define a handler to renew the users session token every time they hit the /refresh route in our application

const refreshHandler = (req, res) => {
    // (BEGIN) The code from this point is the same as the first part of the welcomeHandler
    if (!req.cookies) {
        res.status(401).end()
        return
    }

    const sessionToken = req.cookies['session_token']
    if (!sessionToken) {
        res.status(401).end()
        return
    }

    userSession = sessions[sessionToken]
    if (!userSession) {
        res.status(401).end()
        return
    }
    if (userSession.isExpired()) {
        delete sessions[sessionToken]
        res.status(401).end()
        return
    }
    // (END) The code until this point is the same as the first part of the welcomeHandler

    // create a new session token
    const newSessionToken = uuid.v4()

    // renew the expiry time
    const now = new Date()
    const expiresAt = new Date(+now + 120 * 1000)
    const session = new Session(userSession.username, expiresAt)

    // add the new session to our map, and delete the old session
    sessions[newSessionToken] = session
    delete sessions[sessionToken]

    // set the session token to the new value we generated, with a
    // renewed expiration time
    res.cookie("session_token", newSessionToken, { expires: expiresAt })
    res.end()
}

refresh token diagram

We can now add this to the rest of our routes:

app.post('/refresh', refreshHandler)

Logging Out Our Users

If the user decides to logout of our application, we need to remove their session token from our storage as well as the users client.

logging out a user - get the users session token and delete it from our storage, sending an empty cookie back

Let’s create a handler to implement this:

const logoutHandler = (req, res) => {
    if (!req.cookies) {
        res.status(401).end()
        return
    }

    const sessionToken = req.cookies['session_token']
    if (!sessionToken) {
        res.status(401).end()
        return
    }

    delete sessions[sessionToken]

    res.cookie("session_token", "", { expires: new Date() })
    res.end()
}

We can now add this to the rest of our routes:

app.get('/logout', logoutHandler)

Running Our Application

To start the application, we can run:

node index.js

Now, using any HTTP client with support for cookies (like Postman, or your web browser) make a sign-in request with the appropriate credentials:

POST http://localhost:8080/signin

{"username":"user2","password":"password2"}

You can now try hitting the welcome route from the same client to get the welcome message:

GET http://localhost:8080/welcome

Hit the refresh route, and then inspect the clients cookies to see the new value of the session_token:

POST http://localhost:8080/refresh

Finally, call the logout route to clear session data:

GET http://localhost:8080/logout

Calling the welcome and refresh routes after this will result in a 401 error.

You can find the working source code for this example on Github.