read

May 25, 2014: Updated tutorial for Express 4.x.


To see password reset in action, check out this Live Demo from the Hackathon Starter project.

Let’s begin by installing the Express application generator. That will allow us to create a new Express project skeleton from the command line.

sudo npm install -g express-generator

Note: Do not use sudo if you are on Windows.

To create a new Express project run the following command:

express myapp

Next, install NPM dependencies:

cd myapp && npm install

Before proceeding any further, let’s cleanup this project from all that garbage created by the Express generator.

Delete bin and routes folders, as well as views/error.jade template. Then replace app.js with the following contents:

var express = require('express');
var path = require('path');
var favicon = require('static-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var app = express();

// Middleware
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(favicon());
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.get('/', function(req, res) {
  res.render('index', { title: 'Express' });
});

app.listen(app.get('port'), function() {
  console.log('Express server listening on port ' + app.get('port'));
});

If you run the app now, you should see the following “Welcome to Express” page.

We will be using Nodemailer for sending password reset emails, Mongoose for interacting with MongoDB and Passport for user authentication. Additionally we will need bcrypt-nodejs for hashing user passwords and async library to avoid dealing with nested callbacks by using with the help of async.waterfall method. Also, as of Express 4.0+ you have to install session middleware separately.

To install these modules run the following command:

npm install --save async express-session mongoose nodemailer passport passport-local bcrypt-nodejs

Note: By passing --save flag, those packages will be automatically added to package.json. I can’t recall how many times I have installed packages locally, but then forgot to add them to package.json.

Next, add these modules at the top of app.js:

var session = require('express-session')
var mongoose = require('mongoose');
var nodemailer = require('nodemailer');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var bcrypt = require('bcrypt-nodejs');
var async = require('async');
var crypto = require('crypto');

Note: We didn’t have to install crypto library as it is part of Node.js. We will be using it for generating random token during a password reset.

Add the session middleware right after app.use(cookieParser()):

app.use(session({ secret: 'session secret key' }));

From here on, we will be working entirely inside app.js, while ocassionally switching to templates. I am only doing it for the purposes of this tutorial, in order to keep things simple. If you are building a mediums-sized application (or larger), it would be in your best interests to modularize your code.

Since we are using Mongoose to interact with MongoDB, we will first need to create a User model before we can do anything. But even before that, we need to have a Schema. Let’s start by defining the User schema. Add this right after the module dependencies.

var userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  resetPasswordToken: String,
  resetPasswordExpires: Date
});

Each schema maps to a MongoDB collection. And each key - username, email, password, etc., defines a property in our MongoDB documents. For example, this is how our User document would look in a database:

> db.users.findOne()
{
	"__v" : 0,
	"_id" : ObjectId("530c17c1fb8c96752498e120"),
	"email" : "sahat@me.com",
	"password" : "$2a$05$ANZrgWJqVo9j1tqgCMwe2.LCFnU43bUAYW9rA3Nsx4WchPM.cELEi",
	"username" : "sahat"
}

Note: Properties resetPasswordToken and resetPassword are not part of the above document, because they are set only after password reset is submitted. And since we haven’t specified default values, those properties will not be set when creating a new user.

Besides specifying a structure of documents, Mongoose schemas also define instance methods and middleware. And that’s exactly what we will use next.

It would not be smart to store passwords in plaintext, clearly visible. If your database is compromised, you (or your users) are pretty much screwed. To avoid that, we will need to hash user’s passwords. And that’s where bcrypt comes in. Now, consider a scenario where you have a signup page, an account management page where users can update their existing password and a reset password page where users can set a new password. Do you really want to implement the same password hashing logic in all three places? Instead, you should use Mongoose middleware to hash a password on save().

userSchema.pre('save', function(next) {
  var user = this;
  var SALT_FACTOR = 5;

  if (!user.isModified('password')) return next();

  bcrypt.genSalt(SALT_FACTOR, function(err, salt) {
    if (err) return next(err);

    bcrypt.hash(user.password, salt, null, function(err, hash) {
      if (err) return next(err);
      user.password = hash;
      next();
    });
  });
});

Note: This code snippet was taken from a passport-local example.

Next, to perform password verification when user tries to sign-in, we will use the following Mongoose instance method:

userSchema.methods.comparePassword = function(candidatePassword, cb) {
  bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
    if (err) return cb(err);
    cb(null, isMatch);
  });
};

To use our userSchema, we need to convert it into a Model we can work with. Add this line right after the instance method we have just defined:

var User = mongoose.model('User', userSchema);

Before we can interact with the database, we must first connect to one. If you already have MongoDB installed on your machine, and it is up and running, then simply add this line somewhere in your app.js. I typically place it right before (or after) var app = express();.

mongoose.connect('localhost');

Or, if you do not have MongoDB installed on your computer, you may use this demo database that I have created just for this tutorial:

mongoose.connect(mongodb://demo:demo@ds027759.mongolab.com:27759/demo);

Now, let’s move on to Passport configuration. You need to configure three pieces to use Passport for authentication:

  1. Authentication strategies
  2. Application middleware
  3. Sessions

To setup a Local strategy (username and password), add the following code anywhere after the User model declaration:

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.' });
    user.comparePassword(password, function(err, isMatch) {
      if (isMatch) {
        return done(null, user);
      } else {
        return done(null, false, { message: 'Incorrect password.' });
      }
    });
  });
}));

Note: This code snippet is almost identical to the one found on Passport | Configure page.

Next, we need to add the Passport middleware to our Express configuration. It is important that you place these two lines after app.use(session({ secret: 'session secret key' })). More often than not, order matters when it comes to Express middleware.

app.use(passport.initialize());
app.use(passport.session());

For example, this is how it would look all together:

// Middleware
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(favicon());
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
app.use(session({ secret: 'session secret key' }));
app.use(passport.initialize());
app.use(passport.session());
app.use(express.static(path.join(__dirname, 'public')));

And lastly, we need to add serialize and deserialize passport methods. You can read more about it on Passport | Configure page, but essentially it allows you to stay logged-in when navigating between different pages within your application.

Add this code before or after your LocalStrategy:

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

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

At this point your app.js should look, more or less, something like this:

var express = require('express');
var path = require('path');
var favicon = require('static-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var session = require('express-session')
var mongoose = require('mongoose');
var nodemailer = require('nodemailer');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var bcrypt = require('bcrypt-nodejs');
var async = require('async');
var crypto = require('crypto');

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.' });
    user.comparePassword(password, function(err, isMatch) {
      if (isMatch) {
        return done(null, user);
      } else {
        return done(null, false, { message: 'Incorrect password.' });
      }
    });
  });
}));

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

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

var userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  resetPasswordToken: String,
  resetPasswordExpires: Date
});

userSchema.pre('save', function(next) {
  var user = this;
  var SALT_FACTOR = 5;

  if (!user.isModified('password')) return next();

  bcrypt.genSalt(SALT_FACTOR, function(err, salt) {
    if (err) return next(err);

    bcrypt.hash(user.password, salt, null, function(err, hash) {
      if (err) return next(err);
      user.password = hash;
      next();
    });
  });
});

userSchema.methods.comparePassword = function(candidatePassword, cb) {
  bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
    if (err) return cb(err);
    cb(null, isMatch);
  });
};

var User = mongoose.model('User', userSchema);

mongoose.connect('localhost');

var app = express();

// Middleware
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(favicon());
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
app.use(session({ secret: 'session secret key' }));
app.use(passport.initialize());
app.use(passport.session());
app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.get('/', function(req, res) {
  res.render('index', { title: 'Express' });
});

app.listen(app.get('port'), function() {
  console.log('Express server listening on port ' + app.get('port'));
});

We are done with the configuration step, so let’s move on to defining our routes: /login, /logout, /signup. We will add a few more routes for resetting a password shortly.

Update the / route to include user: req.user property and add the new /login route:

app.get('/', function(req, res){
  res.render('index', {
    title: 'Express',
    user: req.user
  });
});

app.get('/login', function(req, res) {
  res.render('login', {
    user: req.user
  });
});

Our GET /login route simply renders a page. When the login operation completes, user will be assigned to req.user. To check if user is signed-in or not, inside templates, we have to pass { user: req.user } explicitly. For instance, you may want to display Login and Create Account links to guests and Logout link to authenticated users.

We will come back to that in a moment, but for now let’s create a login template. Inside the views folder create login.jade with the following content:

extends layout

block content
  form(method='POST')
    legend Login
    .form-group
      label(for='username') Username
      input.form-control(type='text', name='username', autofocus)
    .form-group
      label(for='password') Password
      input.form-control(type='password', name='password')
    button.btn.btn-primary(type='submit') Login
    a.btn.btn-link(href='/forgot') Forgot Password?

Note: If this is your first time working with Jade templates, I recommend to take a look at this interactive Jade Syntax Documentation.

Let’s switch over to layout.jade so we can add jQuery and Bootstrap libraries. Inside head block add these three lines:

link(rel='stylesheet', href='//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css')
script(src='//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js')
script(src='//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js')

And while we are here, let’s also add a Navbar in layout.jade. Place this code inside body tag, but before block content:

.navbar.navbar-inverse.navbar-static-top(role='navigation')
  .container
    .navbar-header
      button.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse')
        span.sr-only Toggle navigation
        span.icon-bar
        span.icon-bar
        span.icon-bar
      a.navbar-brand(href='/') Project name
    .collapse.navbar-collapse
      ul.nav.navbar-nav
        li.active
          a(href='/') Home
        if user
          li
            a(href='/logout') Logout
        else
          li
            a(href='/login') Login
          li
            a(href='/signup') Signup

Notice the if/else statement. Recall what I said earlier about passing { user: req.user } to a template. This essentially allows us to display different content, depending on whether user is defined or not.

Also, to make things prettier, let’s add some padding to our page content by wrapping block content with .container element.

.container
  block content

Here is how your layout.jade should look at this point:

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css')
    script(src='//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js')
    script(src='//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js')
  body
    .navbar.navbar-inverse.navbar-static-top(role='navigation')
      .container
        .navbar-header
          button.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse')
            span.sr-only Toggle navigation
            span.icon-bar
            span.icon-bar
            span.icon-bar
          a.navbar-brand(href='/') Project name
        .collapse.navbar-collapse
          ul.nav.navbar-nav
            li.active
              a(href='/') Home
            if user
              li
                a(href='/logout') Logout
            else
              li
                a(href='/login') Login
              li
                a(href='/signup') Signup

    .container
      block content

Try visiting the /login route. If you are not using something like nodemon, you will need to manually restart the node.js server before you see new changes. Looks better now, doesn’t it?

If we try to submit a form right now, we will get an error, because we haven’t created POST /login route yet. Let’s do that next.

Back in app.js add the following route:

app.post('/login', function(req, res, next) {
  passport.authenticate('local', function(err, user, info) {
    if (err) return next(err)
    if (!user) {
      return res.redirect('/login')
    }
    req.logIn(user, function(err) {
      if (err) return next(err);
      return res.redirect('/');
    });
  })(req, res, next);
});

Note: This code snippet was taken from a passport-local example.

You now have a fully working login form, except there is no way to test it since we haven’t created any users yet. This would be a right time to create a signup page.

Add the following route to app.js:

app.get('/signup', function(req, res) {
  res.render('signup', {
    user: req.user
  });
});

In your views folder create signup.jade file with the following contents:

extends layout

block content
  form(method='POST')
    legend Signup
    .form-group
      label(for='username') Username
      input.form-control(type='text', name='username', autofocus)
    .form-group
      label(for='email') Email
      input.form-control(type='text', name='email')
    .form-group
      label(for='password') Password
      input.form-control(type='password', name='password')
    .form-group
      label(for='confirm') Confirm Password
      input.form-control(type='password', name='confirm')
    button.btn.btn-primary(type='submit') Signup

Note: Confirm Password currently doesn’t do anything. In a real-world scenario you would compare req.body.confirm with req.body.password to see if they are equal. Take a look at express-validator if you are interested in learning more about data validation.

This is how our /signup page would look like, if you followed along the tutorial:

Just as with the login form, we will need to create a POST route to handle the form on the signup page.

app.post('/signup', function(req, res) {
  var user = new User({
      username: req.body.username,
      email: req.body.email,
      password: req.body.password
    });

  user.save(function(err) {
    req.logIn(user, function(err) {
      res.redirect('/');
    });
  });
});

Here we create a new User object with the values passed into the form. On a successful database save, user is immediately logged-in, then redirected to the home page.

Oh, one last thing, let’s add the logout route:

app.get('/logout', function(req, res){
  req.logout();
  res.redirect('/');
});

At this stage you have a basic, but functional application with Home, Login and Signup pages. We have everything but the password reset feature, which was the entire point of this tutorial.

Create a new route in app.js and a corresponding template, forgot.jade:

app.get('/forgot', function(req, res) {
  res.render('forgot', {
    user: req.user
  });
});
extends layout

block content
  form(method='POST')
    legend Forgot Password
    .form-group
      label(for='email') Email
      input.form-control(type='text', name='email', autofocus)
    button.btn.btn-primary(type='submit') Reset Password

Before we proceed any further, let’s add flash messages to notify users about success and error messages. Go ahead and run:

npm install express-flash --save

and then add it to app.js:

var flash = require('express-flash');

Finally, add the flash() function with the rest of your Express middleware. I have placed it right after app.use(session({ secret: 'session secret key' })), although it might still work if you place it elsewhere.

app.use(flash());

To display flash messages, inside layout.jade let’s add the following code to the .container element, right before block content:

.container
  if messages.error
    .alert.alert-danger
      div= messages.error
  if messages.info
    .alert.alert-info
      div= messages.info
  if messages.success
    .alert.alert-success
      div= messages.success
  block content

Ok, so far so good. Now, it’s going to get slightly more complicated. Add the following route to handle the form on /forgot page:

app.post('/forgot', function(req, res, next) {
  async.waterfall([
    function(done) {
      crypto.randomBytes(20, function(err, buf) {
        var token = buf.toString('hex');
        done(err, token);
      });
    },
    function(token, done) {
      User.findOne({ email: req.body.email }, function(err, user) {
        if (!user) {
          req.flash('error', 'No account with that email address exists.');
          return res.redirect('/forgot');
        }

        user.resetPasswordToken = token;
        user.resetPasswordExpires = Date.now() + 3600000; // 1 hour

        user.save(function(err) {
          done(err, token, user);
        });
      });
    },
    function(token, user, done) {
      var smtpTransport = nodemailer.createTransport('SMTP', {
        service: 'SendGrid',
        auth: {
          user: '!!! YOUR SENDGRID USERNAME !!!',
          pass: '!!! YOUR SENDGRID PASSWORD !!!'
        }
      });
      var mailOptions = {
        to: user.email,
        from: 'passwordreset@demo.com',
        subject: 'Node.js Password Reset',
        text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
          'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
          'http://' + req.headers.host + '/reset/' + token + '\n\n' +
          'If you did not request this, please ignore this email and your password will remain unchanged.\n'
      };
      smtpTransport.sendMail(mailOptions, function(err) {
        req.flash('info', 'An e-mail has been sent to ' + user.email + ' with further instructions.');
        done(err, 'done');
      });
    }
  ], function(err) {
    if (err) return next(err);
    res.redirect('/forgot');
  });
});

Here we are using async module to avoid nesting callbacks within callbacks within callbacks. We start out by randomly generating a token that looks like this - 94b422c1f87568a06a198da66fe2ef8cc963641d. It doesn’t mean anything, we only care that it is somewhat unique, i.e. no two exact password reset tokens at one time. We then pass that token down the async.waterfall to the next function that looks up a user by the provided e-mail address. If there is an account with such e-mail address, we set resetPasswordToken to that randomly generated token and set resetPasswordExpires 1 hour into the future. In other words, password reset link will be active only for 1 hour, afterwards that link becomes invalid.

Next, we send out an e-mail to the user using Nodemailer and SendGrid. If you prefer not to use SendGrid, change service string to any of the following: Gmail, Mailgun, iCloud, Hotmail. For a full list of service providers see Nodemailer GitHub repo.

You should receive an email that looks something like this:

Clicking on that link won’t do anything since we have not implemented /reset route yet. Let’s do that right now.

app.get('/reset/:token', function(req, res) {
  User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user) {
    if (!user) {
      req.flash('error', 'Password reset token is invalid or has expired.');
      return res.redirect('/forgot');
    }
    res.render('reset', {
      user: req.user
    });
  });
});

It immediately checks if there exists a user with a given password reset token and that token has not expired yet. If user is found, it will display a page to setup a new password.

And the reset.jade template:

extends layout

block content
  form(method='POST')
    legend Reset Password
    .form-group
      label(for='password') New Password
      input.form-control(type='password', name='password', value='', placeholder='New password', autofocus=true)
    .form-group
      label(for='confirm') Confirm Password
      input.form-control(type='password', name='confirm', value='', placeholder='Confirm password')
    .form-group
      button.btn.btn-primary(type='submit') Update Password

This is what you would see in a case of a valid token:

And finally, we need to add a POST controller for the /reset/:token route. It is very similar to the /forgot route.

app.post('/reset/:token', function(req, res) {
  async.waterfall([
    function(done) {
      User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user) {
        if (!user) {
          req.flash('error', 'Password reset token is invalid or has expired.');
          return res.redirect('back');
        }

        user.password = req.body.password;
        user.resetPasswordToken = undefined;
        user.resetPasswordExpires = undefined;

        user.save(function(err) {
          req.logIn(user, function(err) {
            done(err, user);
          });
        });
      });
    },
    function(user, done) {
      var smtpTransport = nodemailer.createTransport('SMTP', {
        service: 'SendGrid',
        auth: {
          user: '!!! YOUR SENDGRID USERNAME !!!',
          pass: '!!! YOUR SENDGRID PASSWORD !!!'
        }
      });
      var mailOptions = {
        to: user.email,
        from: 'passwordreset@demo.com',
        subject: 'Your password has been changed',
        text: 'Hello,\n\n' +
          'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n'
      };
      smtpTransport.sendMail(mailOptions, function(err) {
        req.flash('success', 'Success! Your password has been changed.');
        done(err);
      });
    }
  ], function(err) {
    res.redirect('/');
  });
});

We begin by checking if the password reset token is still valid. It is not unlikely that a user opens the link from their e-mail and leaves the browser open for more than one hour (at which point token should no longer be valid).

If the user is found, update his/her password and $unset resetPasswordToken and resetPasswordExpires fields. User is then immediately signed-in. Right after that an email is sent to the user notifying about the password change.

Upon a successful password reset you would be redirected to the home page with a success flash message:

That’s all I have! I hope you enjoyed this tutorial. For questions & comments send me an email at sahat[at]me[dot]com.