Node, Express, and Passport

Introduction

This tutorial will show you how to use Passport to add authentication and session functionality to a Node Express application. We’ll start with setting up the Passport middleware, then we’ll see how it all works with Express, and lastly how to test the Express application with Passport installed.

I’ve found the Passport documentation to be incomplete, so I’ll also document the Passport APIs along the way.

What is Passport?

Passport is a middleware that provides a scaffold for adding session and authentication functionality to your Node application. By itself, Passport has no functionality whatsoever. In order to make it do anything, you need to install plugins and other packages.

Basic Terminology

  • Authentication – The process of verifying a user’s identity using authentication factors such as a username and password.
  • Strategy – A Passport plugin that supports a specific authentication flow, such as OAuth2, OpenID, JWT, etc. A strategy may also be fairly granular in that some only support authentication flows for specific organizations, such as Google or Reddit. You can find Passport strategies here.
  • Session – An HTTP session consists of exactly one request by the client, and one response from the server. This isn’t very useful for an application that needs to remember users, such as a shopping site. What if for every user request, the user had to log in again so the server could remember who they were and what was in their shopping cart? Not a good user experience.
  • Session Cookie – A cookie that holds a session ID. This cookie is key to making a session last longer than a single request and response. After a user is successfully authenticated, the server issues a session cookie to the client, thus starting a user session. Now, each time the client makes a request to the server, the client includes the session cookie with the request. When the server receives the request, it uses the session cookie to quickly validate the request and look up the user who made it, thus continuing the session.
  • Session Store – A database that contains session data. For those of you with experience only using OAuth2 and JWT authentication, this may be a foreign concept. Session data is written to the session store after the user is authenticated. It’s sole purpose is to allow the server to quickly match the session ID from the session cookie to a specific user. With that in mind, each entry in the session store is very light, usually consisting of only a user ID. If you need to respond with actual user data, the user ID from the session store is used to make a separate database query.

Requirements

  • Node installed.
  • Postman or a similar tool. Postman allows you to make HTTP requests to your Express application.
  • Familiarity with Node, Express, and TypeScript.

Clone the GitHub repository for this tutorial here. Open it in your favorite IDE, and then follow along.

Setup

First install the app and its dependencies.

In the context of the project folder invoke the following command.

npm install

Now lets fire it up. Invoke the following command to start the server.

npm run dev

Next, open Postman and make a GET request to http://localhost:8080/protected. In order to access this endpoint you must be logged in. It should return the following in the body of the response.

{
    "message": "access denied",
    "error": true
}

Good! Now lets log in. Make a POST request with the following two x-www-form-urlencoded parameters in the body of the request to localhost:8080/auth/login.

username=test&password=test

You should receive the following in the body of the response.

{
    "message": "successfully signed in"
}

Now go back and make another GET request to http://localhost:8080/protected. You should now get the following response

{
    "message": "access granted"
}

There are a few other endpoints for you to play around with, including logout and signup.

Step 1: Passport Dependencies

Let’s go over the main Passport dependencies.

cookie-session – This package supports sessions. An alternative is to use the express-session package. Both work well with Passport, however, I like cookie-session better since it replaces the session store database required by express-session (and the setup and overhead that that entails) with a cookie. So, this package creates two cookies: a session cookie and a session store cookie. The only drawback is that it’s slightly less secure than using a session store database since the session store cookie is accessible by the client. Note that the session store cookie is encoded and signed, so it’s still a relatively safe bet. If either of the cookies is manipulated anywhere but on the server, the session is automatically terminated.

passport-local – This is the Passport strategy that we’re using in our sample application. It supports a basic authentication flow: the user submits a username and password via a web form, and then it’s up to the application to verify the user based on the submitted credentials.

express.json() and express.urlencoded() – Express body parsing middleware that’s required by most Passport strategies.

Step 2: Session

First lets look at how to set up the session middleware. Open the session middleware module, src/middleware/session.ts.

import { Application } from 'express';
import cookieSession from 'cookie-session';
import passport from 'passport';

export default (app: Application) => {
    // session middleware
    app.use(cookieSession({
        keys: [(process.env.SESSION_TOKEN || '')],
        maxAge: 3600000,
    }));
    app.use(passport.session());

    // middleware that fixes passport/cookie-session compatibility
    // passport calls session.regenerate and session.save but
    // cookie-session does not support those methods
    app.use((req, res, next) => {
        if (req.session && !req.session.regenerate) {
            (req.session as Record<string, any>).regenerate = (cb: Function) => cb();
        }
        if (req.session && !req.session.save) {
            (req.session as Record<string, any>).save = (cb: Function) => cb();
        }
        next();
    });

    // session data handling
    passport.serializeUser((user, done) => done(null, user));
    passport.deserializeUser((user, done) => done(null, user as undefined));
};

First we initialize the session middleware, which in our case, is cookie-session. There are several configuration options for this middleware that may be helpful in specific use cases, however, only two options are required. The first is keys. This is an array of tokens that cookie-session uses for signing cookies. In most cases, you just need one token, and in this particular case, the token is stored as an environment variable. You can use any string as a key. The second configuration property is maxAge. This is the number of milliseconds that the session cookie remains valid. This is required since browsers save cookies, even if they’re identified as session cookies. An explicit expiration helps to prevent this security vulnerability.

That second piece of middleware is a hack required by cookie-session when used with Passport 0.6.0 and above. You can avoid this hack by using an older version of Passport (or possibly a newer version if this bug gets fixed).

Now let’s go over each Passport session method in detail.

done()

This is a parameter that’s provided to several Passport methods. It’s super important, so before we look at Passport methods, let’s first take a look at done and how it works. In some documentation, it’s referred to as cb instead of done.

It’s similar to the Express next() method, so if you’re familiar with that, you’ll be familiar with how this works. It is invoked to indicate that an operation is complete and to pass control to the next Passport process. The signature that you use to invoke the method indicates if the operation was a success, failure, or threw an error. It takes three parameters.

  • error – If there was an error, the value should be an error object or a string describing the error. If there was no error, the value should be null.
  • user – If the operation succeeded, the value should be a user object. If the operation failed, the value should be false.
  • info – Optional. An object that describes a success or failure.

Example – A success looks like this:

done(null, myUserObject, { message: 'operation succeeded' });

Example – A failure looks like this:

done(null, false, { message: 'operation failed' });

Example – An error looks like this:

done({ message: 'there was an error' });
session()

A Passport middleware that covers every application endpoint with Passport session handling. It takes no parameters and must be bound to the application before other Passport middleware and methods.

app.use(passport.session());

There’s an older syntax which you may find in some Passport documentation. It looks like this:

app.use(passport.authenticate('session'));
serializeUser()

A required Passport method that transforms user data output from the strategy after authentication and writes it to the session store. It’s invoked only once after successful authentication. It takes one parameter; a function that is provided with two parameters.

  • user – User data passed to it from the strategy.
  • done – The Passport done callback.

Example – Populate the session store with data directly from the strategy. The data is untouched.

passport.serializeUser((user, done) => done(null, user));

Example – Populate the session store with a subset of data from the strategy.

passport.serializeUser((user, done) => {
    done(null, { id: user.id, username: user.name })});

Example – Add user data from a database to the session store. It’s important to note that this same functionality can be put in the strategy instead of this method. You’ll see examples using both methods, but it’s entirely up to you where to put the code.

passport.serializeUser(async (user, done) => {
    const myUserData = await myGetUserData(user.username);
    done(null, myUserData);
});
deserializeUser()

Passport adds session data to the Express req instance, making it available to the rest of the application. deserializeUser is a required Passport method that queries the session store and writes the response to req.user. It is invoked for each request, so it should be quick and not do any heavy lifting. It takes one parameter; a function that is provided with two parameters.

  • user – Data returned from the session store.
  • done – The Passport done callback.

Example – Use deserializeUser to populate the Express req.user instance with data directly from the session store.

passport.deserializeUser((user, done) => done(null, user));

Step 3: Authentication

Now let’s take a look at how the authentication middleware is set up. Open the authentication middleware module, src/middleware/authentication.ts.

import passport from 'passport';
import MockStrategy from '../../__mocks__/mockStrategy';
import { Strategy as LocalStrategy } from 'passport-local';

function getStrategy() {
    let strategy;
    switch (process.env.NODE_ENV) {
        case 'production':
            strategy = new LocalStrategy(
                (username: string, password: string, done: Function) => {
                    if (username === 'fu' && password === 'bar') done(null, { username });
                    else done({ message: 'authentication failed' });
                }
            );
            break;
        default:
            strategy = new MockStrategy(
                ({ username, password }: Record<string, any>, done: Function) => {
                    if (username === 'test' && password === 'test') done(null, { username});
                    else done({ message: 'authentication failed' });
                }
            );
    }
    return strategy;
}

export default () => {
    const strategy = getStrategy();
    passport.use('local', strategy);
};

First I’ll briefly go over the code in this module, and then follow it up with detailed documentation of the methods. A switch statement is used to instantiate a strategy that is appropriate to the environment. A production strategy is instantiated in the production environment, and a mock strategy is instantiated in test and development environments. Note that the production strategy is a super simple demonstration strategy that authenticates a user who simply submits a username of “fu” and a password of “bar.” A true production ready strategy would look more like the sample in the Strategy() documentation that follows.

use()

A Passport method that binds a strategy to the Express application. It takes two parameters.

  • name – An optional string parameter that overrides the default strategy name. The strategy name is used by the Passport authenticate() method. The default name for Passport strategies is the second part of the plugin name. For example, if the plugin is named passport-local, then the default name for that strategy is local.
  • strategy – A strategy instance.
passport.use(myStrategyInstance);
Strategy()

All Passport strategies consist of a constructor which is used to create a strategy instance. It takes two parameters.

  • configuration – An optional configuration object. Configuration options vary from one strategy to the next, but the default configuration often fits most use cases.
  • verifyCallback – A function that is invoked after the strategy’s authentication flow has completed. Its purpose is to pass the resultant data from the authentication flow to the session store. Depending on the strategy, this data might be a user ID, an access token, or other user data. Each strategy provides a different data set. The parameters provided to the verifyCallback also vary by strategy, however, the Passport done method is always the last parameter.

Let’s look at the syntax for instantiating a typical strategy.

import Strategy from 'my-passport-strategy';
import myGetAuthenticatedUser from './my-user-database/my-utilities';

const myStrategyInstance = new Strategy(
    // configuration object
    {
        someSetting: 'some value',
        someOtherSetting: 'some other value',
    },
    // verifyCallback
    async (username, password, done) => {
        try {
            const myUser = await myGetAuthenticatedUser(username, password);
            if (myUser ) done(null, myUser );
            else done(null, false, { message: 'incorrect username or password' }
        } catch (error) {
            done({ message: 'server error' });
        }
    }
);

In this sample, we set two options in the configuration object. Then in the verifyCallback we call a utility method that attempts to authenticates the user. If a valid user is returned by the utility method, the done method is invoked with the user data. If a user cannot be found or cannot be authenticated, the done method is invoked with a failure message. And if there’s an error thrown, we catch it and invoke the done method with an error message.

Step 4: Authentication Endpoints

Now let’s see how the Express application makes use of the features that the Passport session and authentication middleware have provided. Open the authentication routes module, src/routes/auth.routes.ts.

import { Router } from 'express';
import passport from 'passport';

const authRoute = Router();

authRoute.post(
    '/login',
    (req, res, next) => {
        if (req.isAuthenticated()) next({ message: 'already signed in' });
        else next();
    },
    passport.authenticate('local'),
    (req, res) => {
        res.json({ message: 'successfully signed in' });
    },
);

authRoute.post(
    '/logout',
    (req, res, next) => {
        if(!req.isAuthenticated()) next({ message: 'already signed out' });
        else next();
    },
    (req, res, next) => {
        req.logout((error) => {
            if (error) next({ status: 500, message: 'sign out failed' });
            else res.json({ message: 'successfully signed out' });
        });
    }
);

authRoute.post(
    '/signup',
    (req, res, next) => {
        if (req.isAuthenticated()) next({ message: 'already signed in' });
        else next();
    },
    (req, res, next) => {
        const { username, password } = req.body;
        if (username && password) {
            const newUser = { username, password };
            req.login(newUser, (error) => {
                if (error) next({ status: 500, message: 'sign up failed' });
                else res.json({ message: 'successfully signed up' });
            });
        } else next({ message: 'missing credentials' });
    }
);

authRoute.all('/*', (req, res, next) => next({ message: 'auth endpoint does not exist' }));

export default authRoute;

This module is an Express Router. It contains all of the endpoints for the authentication process; login, logout, and signup. Now let’s go over the Passport methods used in this module.

authenticate()

A Passport method that starts a strategy’s authentication flow. It’s implemented as a route handler for a login route. It takes three parameters.

  • name – A required parameter that specifies the name of the strategy to use. The default name for Passport strategies is the second part of the plugin name. For example, if the plugin is named passport-local, then the default name for that strategy is local. Note that it’s possible to override the default name in the Passport authentication middleware, so if you did that, be sure to use the name you chose!
  • config – An optional configuration object for the authentication flow.
    • successRedirect – A redirect path for successful authentication. This is useful if your Express application serves web pages as opposed to an API.
    • failureRedirect – A redirect path for authentication failure. This is useful if your Express application serves web pages as opposed to an API.
    • session – Boolean. If false, session handling is disabled. Default is true. It is useful to disable Passport session handling when using some strategies.
  • callback – An optional function that allows manual control over authentication error handling. Using this parameter requires the use of an entirely new syntax with the authenticate method.

Example – Basic syntax. When an HTTP POST request is received, the strategy’s authentication flow is started. If the authentication flow is successful, a session starts and the second handler sends the success message. If the authentication flow fails or throws an error, Passport passes the failure or error to the Express error handler.

myRoute.post(
    '/login', 
    passport.authenticate('myStrategy'),
    (req, res) => res.json({ message: 'successfully signed in' })
);

Example – Use with the callback parameter. It requires this somewhat complex syntax, but it allows complete control over what happens if the authentication flow fails. Wrap the authenticate method in an Express handler, which provides the callback with access to the Express req, res, and next parameters. Note that starting a session must be handled manually via the logIn method (description of this method is in the next section), and that authenticate must be executed as a curried function. I’ve included this example since some documentation may show this syntax.

myRoute.post(
    '/login',
    (req, res, next) => {
        passport.authenticate(
            'local',
            (error: Error, user: Record<string, any>) => {
                req.logIn(user, (error) => {
                    if (error) next({ message: 'authentication error' });
                    else res.json({ message: 'successfully signed in' });
                });
            }
        )(req, res, next)
    },
);
logIn()

A Passport method that establishes a session. It’s automatically invoked by the Passport authenticate method after successful authentication, but it may also be manually invoked for specific use cases. It takes two parameters.

  • user – Required. User data that’s added to the session store. It may be a string or an object.
  • callback – Required. A function that is invoked after the session starts. If an error occurred, the function is provided with an error object.

Example – Use on a singup endpoint.

myRoute.post(
    '/signup',
    async (req, res, next) => {
        const { username, password } = req.body;
        if (username && password) {
            const newUser = await myPutNewUser(username, password);
            req.login(newUser, (error) => {
                if (error) next({ message: 'sign up error' });
                else res.json({ message: 'successfully signed up' });
            });
    } else next({ message: 'missing credentials' });
}); 
logOut()

A Passport method that terminates a session. It takes one parameter.

  • callback – Required. A function that is invoked after the session is terminated. If an error occurred during termination, the function is provided with an error object.

Example – Use on a signout endpoint.

myRoute.post(
    '/logout',
    (req, res, next) => {
        req.logout((error) => {
            if (error) next({ message: 'sign out error' });
            else res.json({ message: 'successfully signed out' });
        });
    }
);

Step 5: User Endpoints

Enabling authentication and session handling in your application allows it to remember users and serve dynamic content specific to each user.

Open the home route, src/routes/user.route.ts.

import { Router } from 'express';

const userRoute = Router();

userRoute.get(
    '/',
    (req, res, next) => {
        if (req.isAuthenticated()) next();
        else next({ message: 'access denied' });
    },
    (req, res) => res.json({ message: req.user })
);

userRoute.all('/*', (req, res, next) => next({ message: 'user endpoint does not exist' }));

export default userRoute;

This is another Express Router module. It contains a user endpoint that returns the user object. This isn’t useful by itself, but it illustrates that each Express route has access to the currently logged in user. You can use that data to make a database query and return content relevant to the user.

myRoute.get(
    '/shoppingcart',
    async (req, res) => {
        const cart = await myGetCartContents(req.user.userId);
        res.json(cart);
    }
);

Step 6: Protected Endpoints

Another important aspect of enabling authentication and session handling is the ability to protect areas of the site that should only be accessible to authorized users.

Open the home route, src/routes/home.route.ts.

import { Router } from 'express';

const homeRoute = Router();

homeRoute.get('/', (req, res) => res.json({ message: 'home' }));

homeRoute.get(
    '/protected',
    (req, res, next) => {
        if (req.isAuthenticated()) next();
        else next({ message: 'access denied' });
    },
    (req, res) => res.json({ message: 'access granted' })
);

homeRoute.all('/*', (req, res, next) => next({ message: 'endpoint does not exist' }));

export default homeRoute;

This is another Express Router module. It contains a protected endpoint.

isAuthenticated()

A Passport method that returns a boolean which indicates if a session is currently underway. It’s a simple way to check if a user is currently logged in.

Example – Create a protected endpoint.

myRoute.get(
    '/protected',
    (req, res, next) => {
        if (req.isAuthenticated()) next();
        else next({ message: 'access denied' });
    },
    (req, res) => res.json({ message: 'access granted' })
);

Step 7: Testing

Typically, supertest is used to test Node Express endpoints, which is what we do with our sample application. What’s different is that testing endpoints that expect a current session requires special attention. To address this, we make a mock strategy. You’ve already seen a hint of this back when we initialized the authentication middleware. We set it up so that the application automatically uses the mock strategy outside of the production environment.

Mock Strategy

Now lets take a look at the mock strategy itself. If you want to skip this part and just get on with testing, that’s fine. You can find a flexible and robust premade mock strategy here. But if you want to go just a tiny bit deeper, open /__mocks__/mockStrategy.ts.

import { Request } from 'express';
import { Strategy } from 'passport';

export default class MockStrategy extends Strategy {
    verifyCallback: Function;

    constructor(verifyCallback: Function) {
        super();
        this.verifyCallback = verifyCallback;
    }

    authenticate(req: Request) {
        this.verifyCallback(
            // user data
            req.body,
            // done callback
            (error: Error, user: Record<string, any>, info: Record<string, any>) => {
                if (error) this.error(error);
                else if (!user) this.fail(info);
                else this.success(user, info);
            }
        );
    }
}

A strategy is a subclass that extends the Passport Strategy prototype. It has only one property and one method. The property is verifyCallback and the method is authenticate, both of which you should already be familiar with. If you forgot, take another look at Step 3: Authentication and Step 4: Authentication Endpoints.

The class’s authenticate method invokes verifyCallback, which itself invokes the done callback. Yeah, a little confusing, but take a look at the example above and all will be clear. The done callback must invoke either an error, fail, or success method, based on the parameters provided to it.

It’s important to note that this mock strategy is as basic as it gets. It’s just here to help with testing. This particular mock strategy might not even work with your specific setup, so I wouldn’t just willy nilly copy it into your existing app and expect everything to go smoothly.

Test Suite

Now lets take a look at one of our test suites. Open /__tests__/home.test.ts.

import request from 'supertest';
import app from '../src/app';

describe('homeRoutes', () => {
    const testClient = request(app);
    const authClient = request.agent(app);

    beforeAll(async () => {
        // sign in
        await authClient
            .post('/auth/login')
            .send({ username: 'test', password: 'test' });
    });

    it('root endpoint should return data', async () => {
        await testClient
            .get('/')
            .expect(200, { message: 'home' });
    });

    it('protected endpoint should return failure message if not logged in', async () => {
        await testClient
            .get('/protected')
            .expect(400, { message: 'access denied', error: true });
    });

    it('protected endpoint should return data if logged in', async () => {
        await authClient
            .get('/protected')
            .expect(200, { message: 'access granted' });
    });
});

Of special interest is the last test block. This tests a protected endpoint which expects a user to be signed in. To do this we initialize a supertest client named authClient using request.agent() as opposed to request(). We do this because request.agent saves session cookies and request does not. We then simply log in using the test credentials, which provides us with a client that has a running session. Having two clients, one with a session and one without, allows us to test endpoints for success and failure.

Conclusion

Passport consists of essentially two features, session and authentication. First the session and authentication middleware are configured, and then the features that they add to Express are implemented via properties added to the Express req instance and the Passport authenticate method.

What I’ve found the most challenging about using Passport is the dearth of proper documentation. I hope this tutorial has improved upon it!