Using Passport.JS with Sails.JS

19 Dec 2013

This is the tutorial I wish I had for integrating Passport.js with Sails.js. When creating web applications, you'd love to have a user sign-in and sign-out function, while limiting access to certain functions if the visitor is merely a guest.

Enter Passport.js. This fantastic piece of unobtrusive Express middleware provides many mechanisms of authorization, including the usual username/password, or even social media such as Twitter and Facebook.

Sails.js is based off Express, so we'd expect Passport.js to slide in nicely, and it certainly does. Without further ado, let's get to making an application!

We create our Sails app which we'll name auth. the current version of sails i'm using is 0.9.7.

sails new auth --template=jade

By default Sails.js uses the EJS templating language, which I'm not a huge fan of, so I'm changing it to Jade. Let's install that dependency using NPM:

npm install jade

Next we install Passport.js and the Local strategy:

npm install passport && npm install passport-local

Because we're using Passport.js as an Express middleware, we need to configure that. Create a file in the folder "config" named "passport.js".

// Location: /config/passport.js
var passport    = require('passport'),
  LocalStrategy = require('passport-local').Strategy,
  bcrypt = require('bcrypt');

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

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

passport.use(new LocalStrategy(
  function(username, password, done) {
    User.findByUsername(username).done(function(err, user) {
      if (err) { return done(null, err); }
      if (!user || user.length < 1) { return done(null, false, { message: 'Incorrect User'}); }
      bcrypt.compare(password, user[0].password, function(err, res) {
        if (!res) return done(null, false, { message: 'Invalid Password'});
        return done(null, user);
      });
    });
  })
);

module.exports = {
 express: {
    customMiddleware: function(app){
      console.log('express midleware for passport');
      app.use(passport.initialize());
      app.use(passport.session());
    }
  }
};

In the above piece of code many things are happening:

  1. Modules are being required: passport and its local strategy, as well as bcrypt which is our password encryption module.
  2. serializeUser and deserializeUser is being configured to tell Sails how to parse User into req.session and passport.session.
  3. The custom local strategy that we will be using is being defined, accessible later from our authentication controller.

Next, we'd want to create the User model, and its controller:

sails g model User
sails g controller User

We'll give our User model two fields, a username field and a password field, with optional but commonly added validations.

//User.js
module.exports = {
  attributes: {
    username: {
      type: 'string',
      required: true,
      unique: true
    },
    password: {
      type: 'string',
      required: true
    },
};

We don't want to store our password as plaintext, so we'll use bcrypt. We'll want to install that npm dependency first:

npm install bcrypt

Following which, we'll require the module in our User model. To convert a password from plaintext to an encrypted one, we'll use bcrypt. When a POST request is sent to create a User, the beforeCreate function is called. We will overide the beforeCreate method to encrypt the password before saving:

var bcrypt = require('bcrypt');
module.exports = {
  attributes: {
    username: {
      type: 'string',
      required: true,
      unique: true
    },
    password: {
      type: 'string',
      required: true
    },
  },

  beforeCreate: function(user, cb) {
    bcrypt.genSalt(10, function(err, salt) {
      bcrypt.hash(user.password, salt, function(err, hash) {
        if (err) {
          console.log(err);
          cb(err);
        }else{
          user.password = hash;
          cb(null, user);
        }
      });
    });
  }
};

Finally, in our API, we do not wish to return the password to the client. To do so, we override the toJSON method on the User model, and delete the password attribute from the User object. Behold, our completed User model:

var bcrypt = require('bcrypt');
module.exports = {
  attributes: {
    username: {
      type: 'string',
      required: true,
      unique: true
    },
    password: {
      type: 'string',
      required: true
    },

    //Override toJSON method to remove password from API
    toJSON: function() {
      var obj = this.toObject();
      // Remove the password object value
      delete obj.password;
      // return the new object without password
      return obj;
    }
  },

  beforeCreate: function(user, cb) {
    bcrypt.genSalt(10, function(err, salt) {
      bcrypt.hash(user.password, salt, function(err, hash) {
        if (err) {
          console.log(err);
          cb(err);
        }else{
          user.password = hash;
          cb(null, user);
        }
      });
    });
  }
};

Next, we'll need an auth controller to handle authorizations:

sails g controller Auth

In this Auth controller we need three actions, login (GET and POST -> the "process" action) and logout. We'll use passport in this controller, so we'll need to require it too. We'll be using passport's methods primarily here:

// Location: /api/controllers/AuthController.js
var passport = require("passport");
module.exports = {
  login: function(req,res){
    res.view("auth/login");
  },

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

  logout: function (req,res){
    req.logout();
    res.send('logout successful');
  },
  _config: {}
};

We'll add in the respective routes in our routes file:

// Location: config/routes.js
module.exports.routes = {
  '/': {
    view: 'home/index'
  },
  'get /login': "AuthController.login",
  'post /login': 'AuthController.process',
  'get /logout': 'AuthController.logout',
}

Populate our login page with HTML:

form(action='/login', method='post')
    div
        label Username:
        input(type='text', name='username')
      div
        label Password:
        input(type='password', name='password')
      div
        input(type='submit', value='Log In')

For more information on how passport works visit the documentation here. If a user is signed in, req.isAuthenticated will return true. We'll make use of that in a policy to check for authentication:

// Location: /api/policies/authenticated.js
module.exports = function(req, res, next){
  if (req.isAuthenticated()){
    return next();
  }else{
    return res.send(403, { message: 'Not Authorized' });
  }
}

We can then edit our config/policies.js to require authentication for all controllers, while whitelisting the auth controller:

// Location: config/policies.js
module.exports.policies = {
  '*': "authenticated",
  UserController: {
    "create": true,
    }
  AuthController: {
    '*': true,
  }
}

That should about do it! As homework, create your own sign-up page (I'm lazy). Many thanks to those who helped me along the way (Matt Raykowski yup you!).

UPDATE:

I updated the code for the middleware, such that it works for all my readers. Thanks for everybody's input!

comments powered by Disqus