6 minute read

Implement Multiple Local User Authentication Strategies in Passport.js

Jul 11, 2018 / Javascript

Last Updated: Thursday, March 14th, 2019

Table of Contents:

  1. Introduction
  2. Naming Local Strategies
  3. Extending passport.serializeUser(); and passport.deserializeUser();

Introduction:

User authentication is complicated.

Writing user authentication from scratch is even more complicated. You’ll need to make complex security considerations that can delay progress on your Node.js application.

The good news is that the npm package Passport.js abstracts a good amount of this complication away from us, empowering us to create solutions for user authentication that are modular, maintainable, and extensible.

The bad news is that the documentation is less than helpful. It gives great explanation of the API, but skips over very useful module extensibility features.

This article serves to demonstrate one capability offered by Passport.js which is not outlined in the documentation:

Authenticate multiple local user types with multiple local strategies. Each strategy will be using different user models with different user roles, while at the same time utilizing Passport’s native serialization methods to authenticate and authorize user sessions.

Part One: Name the Local Strategies

Although it isn’t required, Passport.js’s passport.use(); method does take one optional parameter that is not mentioned in the documentation. The documentation should be changed so that it includes:

 /** 
 * @param {string} strategyName
 * @param {LocalStrategy} LocalStrategy
 */
passport.use([ strategy-name,] new LocalStrategy);

Essentially, Passport uses “strategies” to authenticate users making requests to the server. The passport.use method is similar to Express’s app.use in that it mounts a specified strategy to the Passport object, which is called when the route handler callback invokes the authenticate(); method (e.g., app.get( "/path", passport.authenticate('strategyName') ); ).

Knowing this, you can add many different named strategies to your app’s Passport object to be used in other parts of your application, and Passport is programmed to validate requests using those strategies anytime you want via the authenticate(); method.

In a typical application that only needs one type of user authentication, you would see something like:

app.js

...
const passport = require("passport");
...

require('./config/passport')(passport); 

config/passport.js

const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;

module.exports = function(passport) {
  passport.use('local-signup', new LocalStrategy({ 
    // logic that checks the request's data against the application database
    // for an existing username, if no username exists, create
    // and save it to the database along with a hashed 
    // version of the password
  });

  ...

}

routes.js

const express = require("express");
const passport = require("passport");

app.post('/login', passport.authenticate('local-signup', { successRedirect: '/', failureRedirect: '/login' }));

In the app.js file, we are importing the passport object, and then passing it as an argument into our config/passport.js file which will initialize the specified strategies in the app’s Passport object.

In order to change the code above so that you can use multiple local strategies, is to add a passport.use method with names of the strategies we want to include. I have included an example below:

app.js

...
const passport = require("passport");
...

require('./config/passport')(passport); 

config/passport.js

const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;

module.exports = function(passport) {
  passport.use('local-user-signup', new LocalStrategy({ 
    // Include logic that searches the application database
    // for an existing username, if no username exists, create
    // and save it to the database along with a hashed 
    // version of the password
  });

  passport.use('local-otherUser-signup', new LocalStrategy({ 
    ... 
  });

  passport.use('local-user-login', new LocalStrategy({ 
    // Include logic that searches the app database for an 
    // an existing username, if it exists, match passwords
    // and login. If either username does not exist, or 
    // password does not match existing username, throw error. 
  });

  passport.use('local-otherUser-login', new LocalStrategy({ 
    ... 
  });

  ...

}

routes.js

const express = require("express");
const passport = require("passport");

app.post('/signup', passport.authenticate('local-signup', { ... });

Issue #50 from the Passport.js repository on GitHub shows Jared Hanson explaining the same concept:

https://github.com/jaredhanson/passport/issues/50

Extending passport.serializeUser(); and passport.deserializeUser();

Many of you who are reading this article have already completed the steps above, and are now faced with an issue:

You are unable to use the named local authentication strategies from the config/passport.js file that you created above, and your application is crashing silently.

If this is the case, you may have not yet extended the Passport serialization methods, and you will not be able to successfully use the multiple local authentication strategies until you do so.

Before moving on, take a look at the serialization methods in the documentation. The user ID (you provide as the second argument of the “done” function in the serializeUser method) is saved in the session and is later used to retrieve the whole object via the deserializeUser function. serializeUser determines, which data of the user object should be stored in the session. The result of the serializeUser method is attached to the session as req.session.passport.user = {}. The first argument of deserializeUser corresponds to the key of the user object that was given to the done function (see 1.). So your whole object is retrieved with help of that key.

If you do not make your user ID unique across each user Model, your serializeUser function will not know which database to query to retrieve the user you are trying to deserialize.

There are a few ways to fix this, but I included a session ID constructor below:

config/passport.js

const LocalStrategy = require('passport-local').Strategy;
const Guest = require('../models/guest');
const Resident = require('../models/resident');

function SessionConstructor(userId, userGroup, details) {
  this.userId = userId;
  this.userGroup = userGroup;
  this.details = details;
}

module.exports = function(passport) {

  passport.serializeUser(function (userObject, done) {
    // userObject could be a Model1 or a Model2... or Model3, Model4, etc.
    let userGroup = "model1";
    let userPrototype =  Object.getPrototypeOf(userObject);

    if (userPrototype === Model1.prototype) {
      userGroup = "model1";
    } else if (userPrototype === Resident.prototype) {
      userGroup = "model2";
    }

    let sessionConstructor = new SessionConstructor(userObject.id, userGroup, '');
    done(null,sessionConstructor);
  });

  passport.deserializeUser(function (sessionConstructor, done) {

    if (sessionConstructor.userGroup == 'model1') {
      Model1.findOne({
          _id: sessionConstructor.userId
      }, '-localStrategy.password', function (err, user) { // When using string syntax, prefixing a path with - will flag that path as excluded.
          done(err, user);
      });
    } else if (sessionConstructor.userGroup == 'model2') {
      Model2.findOne({
          _id: sessionConstructor.userId
      }, '-localStrategy.password', function (err, user) { // When using string syntax, prefixing a path with - will flag that path as excluded.
          done(err, user);
      });
    } 

  });

  passport.use( ... );

}