Getting started with #SailsJS - Part 3 - Sails Policies and #JWT

In the previous post, we added some validations and custom actions for sign up and login the user. In this post, we will include the implementation to generate the JWT token which has to be passed in the request header to access protected routes. This implementation has 2 parts to it:

  1. Sails service for generating and verifying the JWT token.
  2. Sails policy to check all routes to see if the authorization header contains a valid JWT token.
1. JWT Token service

We will first start by writing a Sails service which can be used across all controllers in the application. Ideally, you can move any common code that is not specific to any of the controllers to a service so that it can be re-used.

Let's start by installing the jsonwebtoken package which provides the required methods to generate and verify JWT. Run the following command inside the project directory:

$ npm install jsonwebtoken --save

Create a new file in the project_home/api/services folder with the name jwToken.js in which we will add the code for the JWT token service. Add the following code in the jwToken file:

/**
 * Service to generate JWT
 */
var jwt = require('jsonwebtoken');

module.exports = {
	'sign': function(payload) {
		return jwt.sign({
			data: payload
		}, sails.config.secret, {expiresIn: 30});
	},
	'verify': function(token, callback) {
		jwt.verify(token, sails.config.secret, callback);
	}
};

We need a secret value to encrypt the payload. This can be added in the sails config. Add the following key to the project_home/config/env/{environment}.js file:

module.exports = {
  secret: 'mysecret'
};

This value will be globally accessible using sails.config.secret once the server is started. {environment}.js corresponds to the respective environment - development, production, test. To sign the payload, we need to pass the payload, secret used to encrypt and the config options. In our case, we are passing the expiry time, you can look up the documentation for jsonwebtoken for the options available.

Next, we need to use this service to generate the JWT token by passing the payload. We need to call this in the login function where we validate the email and password. If the email and password is a match, we can generate the token and return it in the response so that the client can make authenticated requests using the token.

var bcrypt = require('bcrypt');

module.exports = {
    ...
    'login': function(req, res) {
            //Compare the password
            bcrypt.compare(req.body.password, user.encryptedPassword, function(err, result) {
                if(result) {
                	//password is a match
                	return res.json({
                        user:user,
                        token: jwToken.sign(user)//generate the token and send it in the response
                    });
                } else {
                	//password is not a match
                	return res.forbidden({err: 'Email and password combination do not match'});
                }
            });
        });
    }
};
2. Protecting routes using the JWT

Now that we have successfully generated the token, we need to protect the resources by checking if a valid token is being passed in the authorization header in the request. We will first write a new action in the UserController then try to add out header based authentication to this action. The 'check' route simply returns a 200 OK response in our case. Add the following code in UserController:

module.exports = {
    ...
    'check': function(req, res) {
        return res.json();
    },
    ...
};

We have not yet added the check for the authorization header so you should be able to call this action from a REST client on this route /user/check.

Next we will write a Sails policy to check all the routes for the authentication header and return an appropriate response if it is not authorized. Create a file isAuthorized.js in the project_home/api/policies folder. Add the following code in the file:

module.exports = function(req, res, next) {
	var token;
	//Check if authorization header is present
	if(req.headers && req.headers.authorization) {
		//authorization header is present
		var parts = req.headers.authorization.split(' ');
		if(parts.length == 2) {
			var scheme = parts[0];
			var credentials = parts[1];
			
			if(/^Bearer$/i.test(scheme)) {
				token = credentials;
			}
		} else {
			return res.json(401, {err: 'Format is Authorization: Bearer [token]'});
		}
	} else {
		//authorization header is not present
		return res.json(401, {err: 'No Authorization header was found'});
	}
	jwToken.verify(token, function(err, decoded) {
		if(err) {
			return res.json(401, {err: 'Invalid token'});
		}
		req.user = decoded;
		next();
	});
};

In this policy, we are expecting the authorization header to be present and having the value in the following format:

Bearer<SPACE><Token>

We will read the request header and then split the value in the authorization header. The first value present in the array index 0 is the scheme and the second value in the array at index 1 is the token. We will then verify the token passing the it to the jwToken service.

Next step is to apply the policy to all the routes. Note that signup and login actions should not have this policy applied. To do this, add the following code in the project_home/config/policies.js:

module.exports.policies = {
  '*': ['isAuthorized'], // Everything resctricted here
  'UserController': {
    'signup': true, // We dont need authorization here, allowing public access
    'login': true // We dont need authorization here, allowing public access
  }
};

Try calling the check action from a REST client without a valid token, it should now throw a 401 error. Pass the token you receive from the login action response in check and you will receive a 200 OK response. You can also return the user object in the response to check. Just modify the check action as follows:

'check': function(req, res) {
        //console.log(req.user);
        return res.json(req.user);
    }

All the code has been updated in the repository. Checkout the latest code in the git repo.