Authenticating a Session Cookie in Express Middleware with JsonWebToken

Web Development TypeScript

Logging in a user when you have the username and password is pretty straightforward. But you don't have the username and password on subsequent requests. How do you know when an incoming request is from an authenticated user? And how can you be confident that the user is who he says he is?

I use a two-part solution involving JsonWebToken and Express middleware. First, I use JsonWebToken to create a signed session cookie. Then, on future requests, the incoming session cookie is validated via JsonWebToken in Express middleware.

Before coding, let's install some dependencies

npm install --save express jsonwebtoken cookie-parsernpm install --save-dev typescript typingstsd install express jsonwebtoken

You will need some sort of login page or API call that authenticates a user based on credentials. Once you have authenticated the user and created a session object, you will use JsonWebToken to create and sign a session token and then store it in a cookie. It might look something like this:

loginApi.ts

import { Request, Response } from 'express';import * as jwt from 'jsonwebtoken';import loginCommand from '../model/loginCommand';const tokenSecret = "In a real app, this would come from a config file";function loginApi(req: Request, res: Response) {    loginCommand(req.body, (err: Error, session: Session) => {        if (err) {            res.status(403).send('Forbidden');            return;        }        const token = {            sid: session.id,        };        const signedToken = jwt.sign(token, tokenSecret, { expiresIn: 86400 });        res.status(200)            .cookie('token', signedToken, { maxAge: 86400 })            .send(/* ... */);    });});

Authenticated users will now have a cookie named 'token'. Reading this token is easy. But don't let that trick you into sprinkling cookie-reading logic throughout your code. We want to have the logic in exactly one place and, ideally, prevent other code from even knowing this logic exists because less-coupled code is more testable. Writing the logic as Express middleware achieves both goals.

Here is what the authentication middleware looks like (logging and a few other things have been removed for brevity):

authenticate.ts

import { Request, Response } from 'express';import * as jwt from 'jsonwebtoken';import getSession from '../../model/getSession';const tokenSecret = "In a real app, this would come from a config file";//// authenticate examines the request cookies for a cookie named// 'token'. If the token cookie exists, is correctly decoded, // and hasn't expired then this method will attempt to retrieve// the session and attach it and the session user to the request// object for use by downstream filters.//export function authenticate(req: Request, res: Response, next: Function) {    // Default to no user logged in    (<any>req).session = null;    req.user = null;    // Helper method to clear a token and invoke the next middleware    function clearTokenAndNext() {        res.clearCookie("token");        next();    }    // Read the cookie named 'token' and bail out if it doesn't exist    const { token } = req.cookies;    if (!token) {        return clearTokenAndNext();    }    // Test the validity of the token    jwt.verify(token, tokenSecret, (err: Error, decodedToken: any) => {        if (err) {            return clearTokenAndNext();        }        // Compare the token expiry (in seconds) to the current time (in milliseconds)        // Bail out if the token has expired        if (decodedToken.exp <= Date.now() / 1000) {            return clearTokenAndNext();        }        // Read the session ID from the decoded token        // and attempt to fetch the session by ID        // Note: getSession retrieves the session (e.g. from Redis, Database, etc).        const { sid: sessionId } = decodedToken;        getSession(sessionId, (err: Error, session: Session) => {            if (err) {                return clearTokenAndNext();            }            // Attach the session and user objects to the request            // (the following steps will access them)            (<any>req).session = session;            req.user = session.user;            next();        });    });};

You can store any arbitrary data in the session cookie. You can even optimize this to avoid a cache/database lookup. But beware that you must absolutely avoid storing sensitive information such as password, credit card details, and other personal information because the token itself is easily decrypted. JsonWebToken doesn't securely hide the information -- rather, it only lets you validate that the cookie hasn't been tampered with.

Wiring It Up

Finally, the middleware needs to be added to Express:

webServer.ts

import * as express from 'express';import * as cookieParser from 'cookie-parser';import { authenticate } from './middleware/authenticate';const app = express();// Add cookieParser first because our authenticate// middleware relies on cookies being availableapp.use(cookieParser());app.use(authenticate);// Add routes here...// Pretend this came from a config fileconst port = 8080;app.set('port', port);app.listen(port, () => {    console.log(`Running in ${process.env.NODE_ENV} mode`);    console.log(`Listening on port ${port}`);});

Level up Your React + Redux + TypeScript

for free with articles, tutorials, sample code, and Q&A.