Node.js authentication series: Passport local strategy using email and password.

Before getting started, we can rule out that there are many authentication strategies that we can implement in our application. This depends on the use-case, we can have local authentication strategies, that use in most of the time the e-mail and password of the user. In other cases we can use third party providers to authenticate our users, like login with Facebook buttons.

The full app is available on GitHub the article will go through only important parts.

I don't want to get into details for each method, we will discuss them at in the appropriate article. I would like to focus on the e-mail and password authentication. This is the most simple, yet in many cases wrongly implemented. Don't get me wrong probably this implementation will become obsolete someday, due to frequent changes and loop holes in security.

Many people don't take security seriously enough when building their application from scratch. Many companies have invested millions in proper security systems for their platforms. So this should raise an alarm when we talk about the security of our application.

Short list of best practices

This is my list of best practices when dealing with e-mail and password authentication (suggestions are much appreciated):

  • Do not store passwords in plain text in DB.
  • Generate password hash using a key stretching/derivation function.
  • Generate a random salt for each user.
  • Do not return password and salt in results from DB query.
  • Return password and salt only if explicitly requested.
  • Do not persist user sensitive data in sessions, like password and salt.
  • Try to be "slow" when computing hashes.
  • Throttle multiple requests for login request.
  • Throttle bad login attempts.
  • Consider using something like helmet.

About the authentication strategy

Probably you all have seen a simple login form with e-mail and password fields. Almost every website or application has a similar login mechanism. The magic behind this is very simple, a user inserts the e-mail and password used on registration, which is matched against the persisted values in the database.

Obviously the implementation is a bit more complex. First, we don't store the user's password in the database, instead we store a one way derived key (or hash) of the password in a stretched form. One way means that we cannot revert the process and get the initial password from the hash. Stretched means, that regardless of the user's password length we are always going to have a string of, let's say 40 characters.

Secondly, we want to have something unique for each user, to combine it with the user's password, so that we don't obtain the same hash for the same password for two users, we will call this the salt. Like we and salt and pepper in food, to get different flavors.

Application structure

    app
    -- controllers
    ---- authenticaiton.js
    ---- account.js
    -- models
    ---- user.js
    -- helpers
    ---- password.js
    -- middlewares
    ---- authentication.js
    -- routes
    ---- authentication.js
    ---- account.js
    ---- main.js
    -- views
    ---- home.html
    ---- signin.html
    ---- signup.html
    ---- account.html
    config
    -- environments
    -- strategies
    ---- local.js
    -- index.js
    -- express.js
    -- passport.js
    -- mongoose.js
    -- routes.js
    -- models.js
    public
    tests
    -- unit
    -- integration
    package.json
    server.js

This is a conceptual application structure, I've used my express starter app. We'll only focus on some parts of the application and the rest of the application will be available on GitHub.

Hashing the password

We discussed that we cannot store the user's password in plain text in the database, because of security reasons. Beside that, we have to combine the password with a salt.

In order to hash the password we can use Node's built in crypto module. An alternative would be to use bcrypt for node.js, but I will let you figure out what suits your needs.

Generally people think that password hashing should be fast. But, its the other way around, when hashing password, I want it to be slow as possible, not slow as a lazy cat, but slow enough to be expensive to compute.

Why expensive to compute? Well because MD5 is super fast, too fast, you can generate all the possible passwords of 6 characters long in tens of seconds. You could use MD5 like this, M5(password + salt), but will not help you. With the proper hardware you could crack and try as many possibilities as you want.

Solution?

Password stretching, and password stretching. Let's say that to generate our hash, it takes 200ms. To generate 10 passwords using the same algorithm would take 2000ms, which is 2 seconds, obviously we can use parallel computing to reduce the time needed, but this is not the case. Hence the computing necessity increases, and becomes time consuming.

Be advised that this can be a double edged blade, which means it can have side effects, if it takes too long to generate the hash. Try what best suits your needs, there is no one solution for all.

Implementing the password helper

We are going to use crypto.pbkdf2() method, which is a key derivation function. It uses an HMAC digest function, like SHA1, to create a derived key from the password using a salt in a number of iterations.

One important part is the number of iterations. The method uses iterations to create the derived key, which will make it expensive as computing time. If in time the hardware to compute hashes improves we can increase the number of iterations to generate the hash.

We are going to create a file called app/helpers/password.js and add the following content:

'use strict';

var LEN = 256;  
var SALT_LEN = 64;  
var ITERATIONS = 10000;  
var DIGEST = 'sha256';

var crypto = require('crypto');

module.exports.hash = hashPassword;

function hashPassword(password, salt, callback) {  
  var len = LEN / 2;

  if (3 === arguments.length) {
    crypto.pbkdf2(password, salt, ITERATIONS, len, DIGEST, function(err, derivedKey) {
      if (err) {
        return callback(err);
      }

      return callback(null, derivedKey.toString('hex'));
    });
  } else {
    callback = salt;
    crypto.randomBytes(SALT_LEN / 2, function(err, salt) {
      if (err) {
        return callback(err);
      }

      salt = salt.toString('hex');
      crypto.pbkdf2(password, salt, ITERATIONS, len, DIGEST, function(err, derivedKey) {
        if (err) {
          return callback(err);
        }

        callback(null, derivedKey.toString('hex'), salt);
      });
    });
  }
}

The hashPassword() function takes three argmunts: a password, an optional salt and a node.js style callback function. The salt is optional, because we can use this function in two scenarios.

First scenario is, when we want to hash the user's password for the first time, the salt does not exists in this case, and we will generate a random salt using crypto.randomBytes() method to generate a cryptographically strong, pseudo-random string.

The second scenarios will occur, when we already have a user in the database, and a salt exists for that user. In this scenario we are going to use the same salt to generate a hash for the input, to get the same hash as output.

Passport local authentication strategy

We are going to use Passport.js to authenticate users. Passport has many strategies already implemented. First we are going to configure passport for our needs, create a file called config/passport.js, and add the following content:

'use strict';

var passport = require('passport');  
var mongoose = require('mongoose');  
var User = mongoose.model('User');

module.exports.init = function(app) {  
  passport.serializeUser(function(user, done) {
    done(null, user.id);
  });

  passport.deserializeUser(function(id, done) {
    User.findById(id, done);
  });

  // load strategies
  require('./strategies/local')();
};

Secondly, we are going to implement the local strategy, by creating the following file, config/strategies/local.js:

'use strict';

var passport = require('passport');  
var LocalStrategy = require('passport-local').Strategy;  
var mongoose = require('mongoose');  
var User = mongoose.model('User');

module.exports = function() {  
  passport.use('local', new LocalStrategy({
      usernameField: 'email',
      passwordField: 'password'
    },
    function(email, password, done) {
      User.authenticate(email, password, function(err, user) {
        if (err) {
          return done(err);
        }

        if (!user) {
          return done(null, false, { message: 'Invalid email or password.' });
        }

        return done(null, user);
      });
    }
  ));
};

As you can see we are using a custom .authenticate() method from our mongoose User model. Do not worry, we will implement this method shortly. Passport let's us define a middleware to mount a strategy, we are going to call it 'local'. We created a new instance of Passport's local strategy and added our own callback when the strategy is called.

Mongoose User model

We are using MongoDB to persist data, to ease things we are going to use mongoose to define collections and communicate with Mongo. Create a file called app/models/user.js, and add the following lines of code:

'use strict';

var mongoose = require('mongoose');  
var passwordHelper = require('../helpers/password');  
var Schema = mongoose.Schema;  
var _ = require('lodash');

var UserSchema = new Schema({  
  email:  {
    type: String,
    required: true,
    unique: true
  },
  name: {
    type: String
  },
  password: {
    type: String,
    required: true,
    select: false
  },
  passwordSalt: {
    type: String,
    required: true,
    select: false
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// compile User model
module.exports = mongoose.model('User', UserSchema);

The schema is fairly simple, so no explanation is required. One thing to note is that we added { select: false } property to the password and passwordSalt fields. This will prevent them from showing in query results.

Next we are going to add the authentication method, by appending it to the model file after the UserSchema:

UserSchema.statics.authenticate = function(email, password, callback) {  
  this.findOne({ email: email }).select('+password +passwordSalt').exec(function(err, user) {
    if (err) {
      return callback(err, null);
    }

    // no user found just return the empty user
    if (!user) {
      return callback(err, user);
    }

    // verify the password with the existing hash from the user
    passwordHelper.verify(password, user.password, user.passwordSalt, function(err, result) {
      if (err) {
        return callback(err, null);
      }

      // if password does not match don't return user
      if (result === false) {
        return callback(err, null);
      }

      // remove password and salt from the result
      user.password = undefined;
      user.passwordSalt = undefined;
      // return user if everything is ok
      callback(err, user);
    });
  });
};

Before in the user schema we set the password and passwordSalt field to be hidden from the query output. But this time we explicitly request them, so that we can construct the hash using the persisted salt. Afterwards we check if the persisted password hash matched the one generated on the fly.

Authentication controller

We are going to create a controller file called, app/controllers/authentication.js, which is going to consume the local strategy from passport. Add the following lines of code:

'use strict';

var passport = require('passport');

module.exports.signin = signinUser;

function signinUser(req, res, next) {  
  // add validation before calling the authenicate() method

  passport.authenticate('local', function(err, user, info) {
    if (err || !user) {
      return res.status(400).send(info);
    }

    req.logIn(user, function(err) {
      if (err) {
        return next(err);
      }

      // you can send a json response instead of redirecting the user
      //res.status(200).json(user);

      res.redirect('/');
    });
  })(req, res, next);
};

The controller exports a .signin() method that calls the local strategy, and redirects the user to the root path of the application, in case of a successful sign in. We could add validation before executing the local strategy, but we are going to keep it simple as possible for now.

Defining the authentication route

In order for everything to work we need to expose an endpoint, create a file called app/routes/authentication.js:

'use strict';

var express = require('express');  
var router = express.Router();  
var authCtrl = require('../controllers/authentication');

router.post('/signin', authCtrl.signin);

module.exports = router;

I'm using the Router class from Express to create a new route instance and later on mount it to the main express application. This way you can easily reuse defined routes and add prefixes as desired.

A simple way to mount the preceding route in your Express app, is with the following code:

app.use('/', require('./app/routes/authentication'));

Considerations for session storage

Passport requires to be initialized and also call the passport.session() method to persist sessions. This method requires the express session to be present. For the express session I use mainly two configurations, one for development and one for production.

The reason why I use two is because by default session information is stored in memory, you don't want this for a production environment, because of memory leaks. Another reason for this could be shared sessions states, when you run your node application in cluster mode.

A simple solution would be to use a persistent storage, because we already use MongoDB, we could simply use it for our session store. There is a very nice implementation called connect-mongo. An alternative solution would be to use connect-redis to store sessions in Redis.

Sample of implementaion found in config/express.js:

  var sessionOpts = {
    secret: config.session.secret,
    key: 'skey.sid',
    resave: false,
    saveUninitialized: false
  };

  switch(env) {
    case 'production':
      sessionOpts.store = new MongoStore({
        url: config.mongodb.uri
      });
    case 'staging':
    case 'test':
    case 'development':
    default:
      app.use(session(sessionOpts));
  }

  /**
   * Use passport session
   */
  app.use(passport.initialize());
  app.use(passport.session());

The full source code can be found on here.

Thanks for reading.

comments powered by Disqus