Skip to content

Matthew Volk

Implement Multiple Local User Authentication Strategies in Passport.js

JavaScript, Authentication2 min read

Last Updated: June 9th 2020

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 — however the Passport 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 explicitly 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.

Creating Local Strategies for Two User Types

Although it isn't required, Passport.js's passport.use(); method does take one optional parameter that is not mentioned in the documentation:

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

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') );).

So, 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
1const passport = require("passport");
2require("./config/passport")(passport);
config/passport.js
1const passport = require("passport");
2const LocalStrategy = require("passport-local").Strategy;
3
4module.exports = function(passport) {
5 passport.use('local-signup', new LocalStrategy({
6 // logic that checks the request's data against the application database
7 // for an existing username, if no username exists, create
8 // and save it to the database along with a hashed
9 // version of the password
10 });
11}
routes.js
1const express = require("express");
2const passport = require("passport");
3
4app.post(
5 "/login",
6 passport.authenticate("local-signup", {
7 successRedirect: "/",
8 failureRedirect: "/login",
9 })
10);

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
1const passport = require("passport");
2require("./config/passport")(passport);
config/passport.js
1const passport = require("passport");
2const LocalStrategy = require("passport-local").Strategy;
3
4module.exports = function(passport) {
5 passport.use('local-signup', new LocalStrategy({
6 // Include logic that searches the application database
7 // for an existing username, if no username exists, create
8 // and save it to the database along with a hashed
9 // version of the password
10 });
11
12 passport.use('local-otherUser-signup', new LocalStrategy({
13 // Include logic that searches the application database
14 // for an existing username, if no username exists, create
15 // and save it to the database along with a hashed
16 // version of the password
17 });
18}
routes.js
1const express = require("express");
2const passport = require("passport");
3
4app.post('/signup-1', passport.authenticate('local-signup', { ... });
5app.post('/signup-2', passport.authenticate('local-otherUser-signup', { ... });

If you'd like more detail and examples than what is provided above, issue #50 from the Passport.js repository on GitHub shows Jared Hanson explaining the same concept.

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

Once you've completed the steps above, you won't yet be able to use the named local authentication strategies from the config/passport.js file that you created above, and your application is likely crashing silently.

The next step is to extend the Passport serialization methods with a function that both checks which Passport strategy was used to generate that client, and then generates a unique code to serialize that user with so that they can be deserialized with each request thereafter.

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
1const LocalStrategy = require('passport-local').Strategy;
2const Guest = require('../models/guest');
3const Resident = require('../models/resident');
4
5function SessionConstructor(userId, userGroup, details) {
6 this.userId = userId;
7 this.userGroup = userGroup;
8 this.details = details;
9}
10
11module.exports = function(passport) {
12
13 passport.serializeUser(function (userObject, done) {
14 // userObject could be a Model1 or a Model2... or Model3, Model4, etc.
15 let userGroup = "model1";
16 let userPrototype = Object.getPrototypeOf(userObject);
17
18 if (userPrototype === Model1.prototype) {
19 userGroup = "model1";
20 } else if (userPrototype === Resident.prototype) {
21 userGroup = "model2";
22 }
23
24 let sessionConstructor = new SessionConstructor(userObject.id, userGroup, '');
25 done(null,sessionConstructor);
26 });
27
28 passport.deserializeUser(function (sessionConstructor, done) {
29
30 if (sessionConstructor.userGroup == 'model1') {
31 Model1.findOne({
32 _id: sessionConstructor.userId
33 }, '-localStrategy.password', function (err, user) { // When using string syntax, prefixing a path with - will flag that path as excluded.
34 done(err, user);
35 });
36 } else if (sessionConstructor.userGroup == 'model2') {
37 Model2.findOne({
38 _id: sessionConstructor.userId
39 }, '-localStrategy.password', function (err, user) { // When using string syntax, prefixing a path with - will flag that path as excluded.
40 done(err, user);
41 });
42 }
43
44 });
45
46 passport.use( ... );
47
48}
© 2020 by Matthew Volk. All rights reserved.