Async Await for Passport.js Authentication in Express.js

03 Sep 2018

The goal of this blog post is for you to know exactly how to implement the LocalStrategy for PassportJS using async/await patterns in your express.js application.

In the passport.js documentation one of the first steps to getting started is to implement your first "strategy" and register it with passport.

var passport = require('passport')
  , LocalStrategy = require('passport-local').Strategy;

passport.use(new LocalStrategy(
  function(username, password, done) {
    User.findOne({ username: username }, function (err, user) {
      if (err) { return done(err); }
      if (!user) {
        return done(null, false, { message: 'Incorrect username.' });
      }
      if (!user.validPassword(password)) {
        return done(null, false, { message: 'Incorrect password.' });
      }
      return done(null, user);
    });
  }
));

LocalStrategy is something most developers need to implement. It is one way to let users log in with a username (or email) and password.

There are other strategies like Facebook, Twitter, Google, and over 300 more available as pluggable modules.

Passport.js Local Strategy Verify Callback

Let's break this down one by one. There is only one argument. The type of this argument is a function. The signature for this function is username, password, and a done callback.

You as the developer need to indicate to the done callback when an error has occurred. You also should indicate if the username is not found, the password incorrect, or, if all things go well, the result user record found.

Error occurred while finding a user

If you get a database error or something else that isn't a missing user or bad password you should indicate this.

if (err) { return done(err); }

User with username not found in database

If the username Passport.js gave you doesn't match any records in your database it's not considered an error.

However, no user was found. Your function for finding users should follow the standard node callback pattern or equivalent.

if (!user) {
  return done(null, false, { message: 'Incorrect username.' });
}

User found but password did not match

After checking to see the passwords did not match you should callback in this manner:

if (!user.validPassword(password)) {
  return done(null, false, { message: 'Incorrect password.' });
}

All things went well!

Simply callback with

return done(null, user);

Passport Local Strategy Async Await Example

The first thing I want to show you is it is 100% possible to use the async await features of node with passport and express.

Local Strategy using async/await pattern

const LocalStrategy = require('passport-local').Strategy;
const UserController = require('../controllers/user');


let strategy = new LocalStrategy(
  async function(email, password, done) {
    let user;
    try {
      user = await UserController.findOneByEmail(email);
      if (!user) {
        return done(null, false, {message: 'No user by that email'});
      }
    } catch (e) {
      return done(e);
    }

    let match = await user.comparePassword(password);
    if (!match) {
      return done(null, false, {message: 'Not a matching password'});
    }

    return done(null, user);

  }
);

UserController is a collection of (so far) static methods for users that I'm building for an upcoming course that does CRUD like operations.

The first thing I want to show you is the async function passed in the constructor to LocalStrategy. This works because LocalStrategy expects a function and a AsyncFunction is a function.

I do the same exact operations as before but this time I'm free to use my await statements where I need to get an asynchronous value. Once for looking up the user by email address and the other for comparingPassword. I am using the asynchronous version of bcrypt.

With the strategy in place we can use it in our express application.

Local Strategy async/await authentication

const express = require('express');
const app = express();
const passport = require('passport');
const session = require("express-session");
const bodyParser = require("body-parser");

//defined above
passport.use(strategy);

app.use(session({ secret: "cats" }));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());

//Could be async if we wanted it to
passport.serializeUser((user, done) => {
  done(null, user.email);
});

//ASYNC ALL THE THINGS!!
passport.deserializeUser(async (email, done) => {
  try {
    let user = await UserController.findOneByEmail(email);
    if (!user) {
      return done(new Error('user not found'));
    }
    done(null, user);
  } catch (e) {
    done(e);
  }
});

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

app.get('/me', async (req, res) => {
  res.send(req.user);
});

app.listen(3000, () => { console.log('listening on 3000') });

In passport.deserializeUser we use an async function because we need to find one user by email. Passport parses the serialized user out of the cookie and hands us the email address to look up a user with.

I once again use my UserController.findOneByEmail AsyncFunction that we can await for a user. If we find a user we simply send the callback with the user. If not, send err.

In this example let's assume we already have registered users.

To log in an existing user we can do something like:

curl -X POST http://localhost:3000/login -d 'username=will+passportasyncexample@wlaurance.com' -d 'password=hi12345' -v

Make sure to have the -v flag at the end. This is verbose mode for curl. Having this flag tells curl to print extra information including the headers. It's here where we can copy the session cookie.

Example response:

< HTTP/1.1 302 Found
< X-Powered-By: Express
< Location: /
< Vary: Accept
< Content-Type: text/plain; charset=utf-8
< Content-Length: 23
< set-cookie: connect.sid=s%3AjymJwvpoqICj7cNsZ7gLaLVxo-47EV5x.NGyz%2FPI0IWZKkoRjUS8bnxqgjys1FsI%2BNntzWWHOJNE; Path=/; HttpOnly
< Date: Tue, 04 Sep 2018 03:40:23 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
Found. Redirecting to /%

From the set-cookie header we can re-use this cookie value in future requests to be the user we logged in as.

curl -H 'cookie: connect.sid=s%3AjymJwvpoqICj7cNsZ7gLaLVxo-47EV5x.
NGyz%2FPI0IWZKkoRjUS8bnxqgjys1FsI%2BNntzWWHOJNE'
http://localhost:3000/me | jq .

{
  "first_name": "will",
  "last_name": "laurance",
  "email": "will+passportasyncexample@wlaurance.com",
  "created": "2018-09-04T02:45:58.367Z",
  "modified": "2018-09-04T02:45:58.554Z",
  "password_hash": "$2b$10$IFB4f2tDH0aE6iSU8/t0pOGXpCGG9Z7ZDuZtiGrvDnSXtnTT0L4fi",
  "id": 1023
}

This is a simple express application that demonstrates how Express.js + Passport.js can be used with async/await.