User authentication with Hapi, Passport and Mongoose

Update July 17th, 2014:
Travelogue, which we use here, is being discontinued. I was also unable to update the dependencies (Hapi from 3 to 6, for example) due to Travelogue being incompatible which made this quite useless. The repository has been updated to use Hapi-auth-cookie instead of Travelogue, Yar and Passport. For more info on how to update read: http://emptymind.me/user-authentication-with-hapi-hapi-auth-cookie-and-mongoose/

TL;DR: The source code is located at https://github.com/Kevin-A/AuthProject. Feel free to do with it whatever you like.

There's a rather large chance you get to deal with authenticating users when you're creating a web application/site, that's no surprise. It was exactly the situation I found myself in, here I'll explain the steps I took to make it work.

For this specific situation I needed users to authenticate themselves with the server. When authenticated they can access different pages as opposed to guests.

The server will be created using Hapi, for authentication we'll use Travelogue which offers better integration for Passport. The user data will be stored in a Mongo database, using Mongoose and the Passport local mongoose strategy.

The system

We need a few pages for registration and logging in. Both will show a simple form. We also need to handle the action of registering and logging. Their URIs will be:

  • GET /auth/register returns a registration form
  • GET /auth/login returns the login form
  • POST /auth/register handles the registration form
  • POST /auth/login handles the login form

Setting up

I assume you have Node installed, if not, get it from: nodejs.org.

Create a folder somewhere that will contain our project. I'll use authProject. Once done, enter the folder and open the terminal. First we create project with the following command:

npm init  

It will now ask you for some information regarding your project. I just spammed Enter for this example.

Installing dependencies

Now it's time to install the required dependencies, enter the following command in your terminal:

npm install --save hapi@4.x.x joi@3.x.x mongoose passport passport-local passport-local-mongoose yar travelogue  

Crap, that's a lot. I've mentioned a few of them, but not all. Joi is a validation module for Hapi, Yar is a cookie module (cookie jar, cookie, jar, get it? ha) for Hapi. Also, we specify Hapi version 4.x.x, because 5 is rather new which means Travelogue start whining. I bet this'll be fixed soon.

In the root of your project, create a new file index.js. This file will create the server and is the starting point for handling the incoming requests. Insert the code below.

index.js

var Hapi = require('hapi');

// Create a server with a host and port
var server = Hapi.createServer(1337);

// Print some information about the incoming request for debugging purposes
server.ext('onRequest', function (request, next) {  
    console.log(request.path, request.query);
    next();
});

server.route({  
    method: 'GET',
    path: '/',
    handler: function (request, reply) {
        reply('<h1> Hi! </h1>');
    }
});

// Start the server
server.start(function() {  
    console.log("The server has started on port: " + server.info.port);
});

Nothing much happens. We import Hapi, create a server that listens on port 1337, print some request information on each request and finally start the server. You can start your server in the terminal with node index.js. Open your browser and visit localhost:1337, it should say "Hi!". We specified this response in the route.add() part where we told it to listen to GET requests to / and respond with <h1> Hi! <h1>.

You may want to install Nodemon and start your server with nodemon index.js. What this does is it starts your server and watches for file changes. When you change, say, index.js and save, the server will restart. Install it globally with npm install -g nodemon.

Adding pages

Now that we can return stuff, let's create the login and registration page. Before doing that we'll reorganise a little. Create a new file: routes.js which will contain all our routes. Also, create a folder controllers in your root folder. Enter the controllers folder and create pages.js which will contain the handlers for our pages.

Updating the routes

Your routes.js file should look something like this:

var Pages = require('./controllers/pages');  
/**
 * Contains the list of all routes, i.e. methods, paths and the config functions
 * that take care of the actions
 */
exports.endpoints = [  
    { method: 'GET',    path: '/',            config: Pages.index    },
    { method: 'GET',    path: '/login',       config: Pages.login    },
    { method: 'GET',    path: '/register',    config: Pages.register },
];

exports.endpoints defines all the routes our server responds to. Earlier we defined one for / that returned "Hi!" by specifying a handler. Here we specified config instead. config is a bit like a handler on steroids in that it contains more options you can set. You'll see later, but first we hook up these routes to our index.js file.

In your index.js file add this line to the top:

var Routes = require('./routes');  

And replace / handler, including the server.route with:

server.route(Routes.endpoints);  

Starting the server doesn't work yet as it is unable to find the handlers. We'll fix that next.

Creating the pages

Open your controllers/pages.js file and add the following code:

/**
 * Handles a call to / and shows some text with links to login and registration
 */
exports.index = {  
    handler: function (request, reply) {
        var data =
        '<h1> Hi there! </h1>' +
        '<p> Would you like to <a href="login">login</a> or <a href="register">register</a>? </p>';

        reply(data);
    }
}

/**
 * Handles a call to /login and shows a login form
 */
exports.login = {  
    handler: function (request, reply) {

        var form =
        '<h1> Login </h1>' +
        '<form method="post" action="login">' +
        '   <p><input type="text"     name="email"    value="" placeholder="E-mail"></p>' +
        '   <p><input type="password" name="password" value="" placeholder="Password"></p>' +
        '   <p><input type="submit"   value="Login"></p>' +
        '</form>';

           reply(form);
    }
}

/**
 * Handles a call to /register and shows a registration form
 */
exports.register = {  
    handler: function (request, reply) {

        var form =
        '<h1> Register </h1>' +
        '<form method="post" action="register">' +
        '   <p><input type="text"     name="email"    value="" placeholder="E-mail"></p>' +
        '   <p><input type="password" name="password" value="" placeholder="Password"></p>' +
        '   <p><input type="submit"   value="Login"></p>' +
        '</form>';

        reply(form);
    }
}

It absolutely does not deserve an award for beauty (we could have used files, styling etc.) but it'll do for our case.

Handling the form data

We'll now add a few routes to handle POST requests received by the forms. Open routes.js and add the following:

    { method: 'POST',   path: '/login',       config: Authentication.login },
    { method: 'GET',    path: '/logout',      config: Authentication.logout },
    { method: 'POST',   path: '/register',    config: Authentication.register },

Now would be a wise time to authentication.js to your controllers folder and create a reference in routes.js: var Authentication = require('./controllers/authentication');.

Open the freshly created authentication.js and add:

var Joi = require('joi');  
var User = require('../models/user').User;

/**
 * Responds to POST /login and logs the user in, well, soon.
 */
exports.login = {  
    validate: {
        payload: {
            email: Joi.string().email().required(),
            password: Joi.string().required()
        }
    },
    handler: function (request, reply) {
        reply('Hi your e-mail is "' + request.payload.email + '", that\'s all!');
    }
}

/**
 * Responds to POST /register and creates a new user.
 */
exports.register = {  
    validate: {
        payload: {
            email: Joi.string().email().required(),
            password: Joi.string().required()
        }
    },
    handler: function(request, reply) {
        reply('Hi your e-mail is "' + request.payload.email + '", that\'s all!');
    }
}

It doesn't do anything right now, other than responding with the supplied e-mail address or error when there is one, because we haven't connect a database yet.

Connecting to a database

Alright, we're going to use MongoDB for our storage needs. MongoLab is free for a database small than 500MB, perfect for this.

Head over to MongoLab and create an account a database. I've called mine authdb for the purpose of this guide. On the database page you'll see a connection string that looks something like:

mongodb://<dbuser>:<dbpassword>@[alphanum].mongolab.com:[port]/authdb  

You'll have to create a database user to receive your dbuser/dbpassword combination. Once that is done create a config.js file in the root of your project. Mine looks like:

module.exports = {  
    mongo: {
        username: '<dbuser>',
        password: '<dbpassword>',
        url: '[alphanum].mongolab.com:[port]',
        database: 'authdb'
    }
}

Don't forget to change it with your info. Alright, create a database.js file in your root. This file will contain the logic of connection to your database. If you then want a connection in some other file you just import (require) this file.

var Mongoose = require('mongoose');  
var Config = require('./config');

//load database
// Mongoose.connect('mongodb://localhost/test');
Mongoose.connect('mongodb://' + Config.mongo.username + ':' + Config.mongo.password + '@' + Config.mongo.url + '/' + Config.mongo.database);  
var db = Mongoose.connection;

db.on('error', console.error.bind(console, 'connection error'));  
db.once('open', function callback() {  
    console.log("Connection with database succeeded.");
});

exports.Mongoose = Mongoose;  
exports.db = db;  

Try it by adding var Database = require('../database'); to one of your controllers!

Creating the user model

Create a new folder in the root of you project called models, in that folder create user.js.

User.js should look something like:

var Mongoose = require('../database').Mongoose;

//create the schema
var userSchema = new Mongoose.Schema({  
    email:     {    type: String,   required: true },
    password:  {    type: String,   required: true },
    creationDate: { type: Date,     required: true, default: Date.now },
});

//create the model
var User = Mongoose.model('User', userSchema, 'Users');

exports.User = User;  

Hooking up passport

Until now everything has been kind of trivial and nothing had much to do with authentication. This part will handle all that and requires us to update most files.

We'll first hook up our User model to be able to create a strategy, then add the plugin to our Hapi server and finally tackle registration and logging in. The next "chapter" will deal with verifying our status.

Connecting the User model

Open the User model and import the strategy:

var passportLocalMongoose = require('passport-local-mongoose');  

Then add the following after creation of the schema and before creating of the model:

userSchema.plugin(passportLocalMongoose, { usernameField: 'email', hashField: 'password', usernameLowerCase: true });  

What we do here is add the plugin to our User model and setting some information. The usernameField is set to username by default, but I'd rather use email. hashField is normally set to hash, which I changed to password.

The plugin adds some extra functionality to our model. For more information and settings, please check: https://github.com/saintedlama/passport-local-mongoose

Adding the plugin

Travelogue needs some data about our server, so let's expand config.js first. Open it up and add the following below mongo:

    server: {
        hostname: 'localhost',
        port: 1337
    }

Easy enough. Back to index.js for some ... drastic additions.

Start by importing the configuration file: var Config = require('./config');

After the creation of the server (var server = ...) add the following code:

/******************************************/
/********* Travelogue setup ***************/
/******************************************/

// Setup of the plugins to use
var plugins = {  
    yar: {
        cookieOptions: {
            password: 'worldofwalmart', // cookie secret
            isSecure: false // required for non-https applications
        }
    },
    travelogue: Config.server
};

// Initialise plugins
server.pack.require(plugins, function (err) {  
    if (err) {
        throw err;
    }
});

// Set passport as the strategy to use
server.auth.strategy('passport', 'passport');

// Grab a reference to Passport and the Model
var Passport = server.plugins.travelogue.passport;  
var User = require('./models/user').User;

// Follow normal Passport rules to add Strategies
Passport.use(User.createStrategy());  
Passport.serializeUser(User.serializeUser());  
Passport.deserializeUser(User.deserializeUser());

/******************************************/
/******************************************/
/******************************************/

What this does is initialise the Travelogue and Yar plugins and add the Mongoose-local-strategy to our Passport instance. Your server should run without issues now, but not much will be happening as we didn't tell it how to use Passport. We still have to write the code that creates a new User or checks the credentials when logging in. We'll do that next.

Authentication

Authentication happens in the controller we created and which happens to be called Authentication. Let's start with registration.

Do not forget to import the User model: var User = require('../models/user').User;.

Registration

The first thing to do is create a new User object from the payload data. Currently that's only the e-mail address, but it could be names, age, hobbies, etc. too. Replace exports.register with:

/**
 * Responds to POST /register and creates a new user.
 */
exports.register = {  
    validate: {
        payload: {
            email: Joi.string().email().required(),
            password: Joi.string().required()
        }
    },
    handler: function(request, reply) {

        // Create a new user, this is the place where you add firstName, lastName etc. 
        // Just don't forget to add them to the validator above.
        var newUser = new User({ 
            email: request.payload.email
        });

        // The register function has been added by passport-local-mongoose and takes as first parameter
        // the user object, as second the password it has to hash and finally a callback with user info.
        User.register(newUser, request.payload.password, function(err, user) {
            // Return error if present
            if (err) {
                reply(err);
                return;
            }

            reply(user);
        });
    }
}

If you now fill in and submit the form at https://localhost:1337/register you should, after a while, retrieve a load of information including one gigantic hash of the password. VoilĂ , easy as that!

If you want speed up the hashing process or reduce the memory the hash uses you're free to change the iterations and keylen properties in the module's settings. For more information: passport-local-mongoose options

Logging in

In order to log in we have to retrieve the data for the current user from the database, create a hash of the supplied password, compare the too, yadda yadda yadda. I'm kidding, we let Passport do the heavy lifting. The new login function looks like:

/**
 * Responds to POST /login and logs the user in, well, soon.
 */
exports.login = {  
    validate: {
        payload: {
            email: Joi.string().email().required(),
            password: Joi.string().required()
        }
    },
    handler: function (request, reply) {
        var Passport = request.server.plugins.travelogue.passport;
        Passport.authenticate('local')(request, reply);
    }
}

Well that's easy. Due to hashing logging in could take a while. When logged in you should be redirected to the home page. If the entered password was wrong you'll see: {"statusCode":401,"error":"Unauthorized","message":"Unauthorized"}.

All that's left to do is show that we're actually logged in.

Expanding the member's area.

Wouldn't it be nice if we could hide the forms when an authenticated users requests them? Checking whether a user is authenticated can be checked with request.session._isAuthenticated(). Adding the following lines before the creation of form in the pages handler should do the trick:

if (request.session._isAuthenticated()) {  
            reply('Already logged in!');
            return;
        }

Logging out

Before continuing we'll first add the option to log out. Without this option we wouldn't be able to properly test our sessions. We've already created the route in authentication and logging out is done by calling request.session._logout();. Our handler will thus look like:

/**
 * Responds to GET /logout and logs out the user
 */
exports.logout = {  
    auth: 'passport',
    handler: function (request, reply) {
        request.session._logout();
        reply();
    }
}

I see you wondering what the hell auth means. Well, by setting auth to 'passport' we tell the server that a user can only access this when logged in with the passport strategy. When a user is not authenticated he'll be sent to /login. Neat huh?

The member's only area

The member's only area will be accessible by browsing to http://localhost:1337/batmanshideout but how do we do this? We've already used the trick: auth: 'passport'! Add the following route to your routes:

/**
 * Handles a call to /batmanshideout and shows super secret stuff
 */
exports.secret = {  
    auth: 'passport',
    handler: function (request, reply) {
        var data =
        '<h1> Batman\'s super secret hideout! </h1>' +
        '<p> Welcome to this totally secret my friend. Would you like to <a href="logout">leave</a>? </p>';

        reply(data);
    }
}

Try it out for yourself now, did you manage to enter Batman's hideout?

Redirects

We're basically done, but we'll round up with a minor enhancement: Redirection to Batman's hideout upon successful login and redirection to the login form upon registration.

Redirection after successfully logging in is achieved by adding a successRedirect property to Passport's authenticate function in Authentication.login like so:

Passport.authenticate('local', {  
            successRedirect: '/batmanshideout'
        })(request, reply);

Redirection after a successful registration is easily done by changing reply(user); to reply().redirect('/login'); in authentication.register.

While we're at it we might also redirect the user to the home page after logging out. Simply change reply(); to reply().redirect('/');.

Conclusions

Due to the large amount of libraries it's very easy to create an authentication system in which your user data resides in a MongoDB database. Hapi in combination with Travelogue and Yar provide access to session management and local passports. Passport-local-mongoose adds the missing layer between Passport and Mongoose by creating a Passport Strategy.

The source code is located at https://github.com/Kevin-A/AuthProject. Feel free to do with it whatever you like.

comments powered by Disqus