Saturday 20 December 2014

Authenticating Node.js Applications With Passport

Implementing robust authentication strategies for any application can be a daunting task and Node.js applications are no exception to this.
In this tutorial, we will develop a Node.js application from scratch and use a relatively new but very popular authentication middleware - Passport to take care of our authentication concerns.
Passport's documentation describes it as a "simple, unobtrusive authentication middleware for Node" and rightly so. 
By providing itself as a middleware, Passport does an excellent job at separating the other concerns of a web application from its authentication needs. It allows Passport to be easily configured into any Express-based web application, just like we configure other Express middleware such as logging, body-parsing, cookie-parsing, session-handling, etc.
This tutorial assumes a basic understanding of Node.js and Express framework and try to keep focus on authentication, although we do create a sample Express app from scratch and progress via adding routes to it and authenticating some of those routes.
Passport provides us with 140+ authentication mechanisms to choose from. You can authenticate against a local/remote database instance or use the single sign-on using OAuth providers for Facebook, Twitter, Google, etc. to authenticate with your social media accounts, or you can choose from an extensive list of providers which support authentication with Passport and provide a node module for that.
But worry not: You do not need to include any strategy/mechanism that your application does not need. All these strategies are independent of each other and packaged as separate node modules which are not included by default when you install Passport's middleware:  npm install passport
In this tutorial, we will use the Local Authentication Strategy of Passport and authenticate the users against a locally configured Mongo DB instance, storing the user details in the database. For using the Local Authentication Strategy, we need to install the passport-local module: npm install passport-local
But wait: Before you fire up your terminal and start executing these commands, let's start by building a Express app from scratch and add some routes to it (for login, registration and home) and then try to add our authentication middleware to it. Note that we will be using Express 4 for the purposes of this tutorial, but with some minor differences Passport works equally well with Express 3, as well.
If you haven't already, then go ahead and install Express & express-generator to generate a boilerplate application by simply executing express passport-mongo on the terminal. The generated application structure should look like this:
Initial Application Structure
Let's remove some of the default functionality that we won't be making use of - go ahead and delete the users.js route and remove its references from the app.js file.
Open up package.json and add the dependencies for passport and passport-local module.
1
2
"passport": "~0.2.0",
"passport-local": "~1.0.0"
Since we will be saving the user details in MongoDB, we will use Mongoose as our object data modeling tool. Another way to install and save the dependency to package.json is by entering:
1
npm install mongoose --save
package.json should look like this:
Added Mongoose Dependencies
Now, install all the dependencies and run the boilerplate application by executing npm install && npm start. It will now download and install all of the dependencies and will start the node server. You can check the basic Express app at http://localhost:3000/ but there is nothing much to see.
Very soon, we are going to change that by creating a full-fledged express app that asks for shows a registration page for a new user, the login of a registered user, and authenticates the registered user by using Passport.
Since we will be saving the user details in Mongo, let's create a User Model in Mongoose and save that in models/user.js in our app.
1
2
3
4
5
6
7
8
9
var mongoose = require('mongoose');
 
module.exports = mongoose.model('User',{
        username: String,
    password: String,
    email: String,
    gender: String,
    address: String
});
Basically, we are creating a Mongoose model using which we can perform CRUD operations on the underlying database.
If you do not have Mongo installed locally then we recommend that you use cloud database services such as Modulus or MongoLab. Creating a working MongoDB instance using these is not only free but is just a matter of few clicks.
After you create a database on one of these services, it will give you a database URI likemongodb://<dbuser>:<dbpassword>@novus.modulusmongo.net:27017/<dbName> which can be used to perform CRUD operations on the database. It's a good idea to keep the database configuration in a separate file which can be pull up as and when needed. As such, we create a node module db.js which looks like:
If you're like me, you are using a local Mongo instance then it's time to start the mongod daemon and the db.js should look like
1
2
3
module.exports = {
}
 Now we use this configuration in app.js and connect to it using Mongoose APIs: 
1
2
3
var dbConfig = require('./db.js');
var mongoose = require('mongoose');
mongoose.connect(dbConfig.url);
Passport just provides the mechanism to handle authentication leaving the onus of implementing session-handling ourselves and for that we will be using express-session. Open up app.js and paste the code below before configuring the routes:
1
2
3
4
5
6
// Configuring Passport
var passport = require('passport');
var expressSession = require('express-session');
app.use(expressSession({secret: 'mySecretKey'}));
app.use(passport.initialize());
app.use(passport.session());

This is needed as we want our user sessions to be persistent in nature. Before running the app, we need to install express-session and add it to our dependency list in package.json. To do that type npm install --save express-session
Passport also needs to serialize and deserialize user instance from a session store in order to support login sessions, so that every subsequent request will not contain the user credentials. It provides two methods serializeUser and deserializeUser for this purpose:
1
2
3
4
5
6
7
8
9
passport.serializeUser(function(user, done) {
  done(null, user._id);
});
 
passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
});
 We will now define Passport's strategies for handling login and signup. Each of them would be an instance of  the Local Authentication Strategy of Passport and would be created using the passport.use() function. We use connect-flash to help us with error handling by providing flash messages which can be displayed to user on error.
The login strategy looks like this:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// passport/login.js
passport.use('login', new LocalStrategy({
    passReqToCallback : true
  },
  function(req, username, password, done) {
    // check in mongo if a user with username exists or not
    User.findOne({ 'username' :  username },
      function(err, user) {
        // In case of any error, return using the done method
        if (err)
          return done(err);
        // Username does not exist, log error & redirect back
        if (!user){
          console.log('User Not Found with username '+username);
          return done(null, false,
                req.flash('message', 'User Not found.'));                
        }
        // User exists but wrong password, log the error
        if (!isValidPassword(user, password)){
          console.log('Invalid Password');
          return done(null, false,
              req.flash('message', 'Invalid Password'));
        }
        // User and password both match, return user from
        // done method which will be treated like success
        return done(null, user);
      }
    );
}));
The first parameter to passport.use() is the name of the strategy which will be used to identify this strategy when applied later. The second parameter is the type of strategy that you want to create, here we use the username-password or the LocalStrategy. It is to be noted that by default the LocalStrategy expects to find the user credentials in username & password parameters, but it allows us to use any other named parameters as well. The passReqToCallback config variable allows us to access the request object in the callback, thereby enabling us to use any parameter associated with the request.
Next, we use the Mongoose API to find the User in our underlying collection of Users to check if the user is a valid user or not. The last parameter in our callback : done denotes a useful method using which we could signal success or failure to Passport module. To specify failure either the first parameter should contain the error, or the second parameter should evaluate to false. To signify success the first parameter should be null and the second parameter should evaluate to a truthy value, in which case it will be made available on the request object
Since passwords are inherently weak in nature, we should always encrypt them before saving them to the database. For this, we use bcrypt-nodejs to help us out with encryption and decryption of passwords.
1
2
3
var isValidPassword = function(user, password){
  return bCrypt.compareSync(password, user.password);
}
If you are feeling uneasy with the code snippets and prefer to see the complete code in action, feel free to browse the code here.
Now, we define the next strategy which will handle registration of a new user and creates his or her entry in our underlying Mongo DB:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
passport.use('signup', new LocalStrategy({
    passReqToCallback : true
  },
  function(req, username, password, done) {
    findOrCreateUser = function(){
      // find a user in Mongo with provided username
      User.findOne({'username':username},function(err, user) {
        // In case of any error return
        if (err){
          console.log('Error in SignUp: '+err);
          return done(err);
        }
        // already exists
        if (user) {
          console.log('User already exists');
          return done(null, false,
             req.flash('message','User Already Exists'));
        } else {
          // if there is no user with that email
          // create the user
          var newUser = new User();
          // set the user's local credentials
          newUser.username = username;
          newUser.password = createHash(password);
          newUser.email = req.param('email');
          newUser.firstName = req.param('firstName');
          newUser.lastName = req.param('lastName');
 
          // save the user
          newUser.save(function(err) {
            if (err){
              console.log('Error in Saving user: '+err); 
              throw err; 
            }
            console.log('User Registration succesful');   
            return done(null, newUser);
          });
        }
      });
    };
     
    // Delay the execution of findOrCreateUser and execute
    // the method in the next tick of the event loop
    process.nextTick(findOrCreateUser);
  });
);
Here, we again use the Mongoose API to find if any user with the given username already exists or not. If not, then create a new user and saves the user information in Mongo. Else return the error using the done callback and flash messages. Note that we use bcrypt-nodejs for creating the hash of the password before saving it:
1
2
3
4
// Generates hash using bCrypt
var createHash = function(password){
 return bCrypt.hashSync(password, bCrypt.genSaltSync(10), null);
}
If we were to see a birds eye view of our application, it would look like:
Birds Eye View of Our Application
We now define our routes for the application in the following module which takes the instance of Passport created in app.js above. Save this module in routes/index.js
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module.exports = function(passport){
 
  /* GET login page. */
  router.get('/', function(req, res) {
    // Display the Login page with any flash message, if any
    res.render('index', { message: req.flash('message') });
  });
 
  /* Handle Login POST */
  router.post('/login', passport.authenticate('login', {
    successRedirect: '/home',
    failureRedirect: '/',
    failureFlash : true 
  }));
 
  /* GET Registration Page */
  router.get('/signup', function(req, res){
    res.render('register',{message: req.flash('message')});
  });
 
  /* Handle Registration POST */
  router.post('/signup', passport.authenticate('signup', {
    successRedirect: '/home',
    failureRedirect: '/signup',
    failureFlash : true 
  }));
 
  return router;
}
The most important part of the above code snippet is the use of passport.authenticate() to delegate the authentication to login and signup strategies when a HTTP POST is made to /login and /signup routes respectively. Note that it is not mandatory to name the strategies on the route path and it can be named anything.
Next, we create the following two views for our application:
  1. layout.jade contains the basic layout & styling information  
  2. index.jade contains the login page containing the login form and giving option to create a new account
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
extends layout
 
block content
  div.container
    div.row
      div.col-sm-6.col-md-4.col-md-offset-4
        h1.text-center.login-title Sign in to our Passport app
          div.account-wall
            form(class='form-signin', action='/login', method='POST')
              input(type='text', name='username' class='form-control', placeholder='Email',required, autofocus)
              input(type='password', name='password' class='form-control', placeholder='Password', required)
              button(class='btn btn-lg btn-primary btn-block', type='submit') Sign in
              span.clearfix
          a(href='/signup', class='text-center new-account') Create an account
          #message
          if message
            h1.text-center.error-message #{message}
Thanks to Bootstrap, our Login page now looks like
Login Page for Our Passport App
We need two more views for registration details and for the home page of the application:
  1. register.jade contains the registration form
  2. home.jade says hello and shows logged in user's details
If you are unfamiliar with Jade, check out the documentation.
Passport, being a middleware, is permitted to add certain properties and methods on request and response objects and it makes proper use of it by adding a very handy request.logout() method which invalidates the user session apart from other properties.
1
2
3
4
5
/* Handle Logout */
router.get('/signout', function(req, res) {
  req.logout();
  res.redirect('/');
});
Advertisement
Passport also gives the ability to protect access to a route which is deemed unfit for an anonymous user. This means that if some user tries to access http://localhost:3000/home without authenticating in the application, he will be redirected to home page by doing
01
02
03
04
05
06
07
08
09
10
11
12
/* GET Home Page */
router.get('/home', isAuthenticated, function(req, res){
  res.render('home', { user: req.user });
});
 
// As with any middleware it is quintessential to call next()
// if the user is authenticated
var isAuthenticated = function (req, res, next) {
  if (req.isAuthenticated())
    return next();
  res.redirect('/');
}
Passport is not the only player in this arena when its comes to authenticating Node.js applications and there exists alternatives like EveryAuth but the modularity, flexibility, community support and the fact that its just a middleware makes Passport definitely a much better choice.
For a detailed comparison between the two, here is an interesting & informative perspective from the developer of Passport himself

4 comments:

  1. Fix MongoDB Connection Not Working in YII2 through MongoDB Technical Support
    In case you are getting this kind of bumble {"response":"failed","status_code":500,"message":} that infers there is some issue in your MongoDB affiliation. In case you require understand this issue by then contact to MongoDB Online Support or MongoDB Customer Support USA. Here we capably ensure the unfaltering nature of your MongoDB database by giving you top of the line support. Our Support for MongoDB Database Software incredible nature of assistance and help through experienced and dedicated particular pros.
    For More Info: https://cognegicsystems.com/
    Contact Number: 1-800-450-8670
    Email Address- info@cognegicsystems.com
    Company’s Address- 507 Copper Square Drive Bethel Connecticut (USA) 06801

    ReplyDelete
  2. The information you have shared is very useful, thank you so much sharing this valuable post with us
    Node JS Online training
    Node JS training in Hyderabad


    ReplyDelete