With the advent of Single Page Applications, server side authentication doesn't quite cut it anymore. JSON Web Tokens are a handy way to bridge the gap.
At a high level overview, a JWT is an encoded bundle of JSON data, generally representing a user, signed and encoded by the server. Each JWT is usually composed of three parts. A header, a payload, and a signature. The header contains relevant information regarding the encoding type as well as identifying the JSON as a web token. The payload can be whatever JSON data you choose, generally user information. The signature is built off a 'secret' key stored on the server. The JWT holds onto this signature and it can be checked to make sure the JWT is the real deal. All this is regular, old JSON, that is then encoded and sent down to the client. Read more about JWTs here.
The general use case for JWTs is for user authentication. On a successful login, the JWT is sent down from the server and is stored on the client. Whenever the client wishes to take a restricted action on the server, the JWT is sent up with the request. The JWT is verified on the server, and if everything passes, the client is allowed to take the requested action.
The beauty of the JWT is is accessible on the client, which makes it useful for SPAs. The server only needs to be queried to get a new token or to take serve side actions. The client can block certain frontend routes be checking if a JWT is present or not. In addition, the JWT is also encoded JSON, so its data can be referenced to grab user information without having to query the server again.
In this post, I'll walk through the relevant parts of a JWT implementation I put through on a recent project. The full project made use of Angular on the frontend, Express as a server framework and Bookshelf as my database ORM.
JWTs on the Server
I opted to put all the JWT logic into an authController.js
file and export it as a module. I used the jsonwebtoken npm module for handling the nitty gritty as well as lodash
for some data manipulation.
Importing the secret is also a crucial part. The secret is a string of my creation which is used to build the JWT's signature and is essential when encoding or decoding a token. I hid my secret away in a nicely gitignored module to keep it private and then I export it wherever necessary throughout the server.
// authController.js
var jwt = require('jsonwebtoken');
var _ = require('lodash');
var secret = require('../config/auth.config').secret;
module.exports = {
authorize: function(req, res, next){
//...
},
createToken: function(user) {
//...
}
};
There are two functions being exposed in authController.js
. The first, createToken
does just that. Given a user object, in this case the user object is user data from the database, it will create and return a signed JWT token with an expiration. Tokens are easily decoded, so never send sensitive information with them! At minimum, omit the password. More on JWT security later. This token will be sent down to the client to be stored and attached to later requests. Check out more about how to use the JWT module here.
...
createToken: function(user) {
return jwt.sign(_.omit(user.attributes, 'password'), secret, {
expiresIn: 24 * 60 * 60
});
}
...
The authenticate
function is where the server side magic happens. authenticate
acts as an Express middleware function and can be easily placed on any routes you need to restrict access to.
The function has two main jobs. First, to check if a JWT exists on the request
object passing through the middleware, and second to verify against my app's secret that it has a valid signature. If everything passes, the token is authentic, the next()
function is called, and the request
passes on to the next middleware. If the JWT is not found or is invalid, an error response is sent to the client, and subsequent server actions are denied.
...
authorize: function(req, res, next) {
var token = req.body.token || req.query.token || req.headers['x-access-token'];
if (token) {
jwt.verify(token, secret, function(err, decoded) {
if (err) {
console.error(err);
return res.status(403).send('error authorizing token');
} else {
req.token = decoded;
return next();
}
});
} else {
console.error('not authorized');
return res.sendStatus(403);
}
},
...
The next step, is to place the authenticate
middleware function on any restricted routes. This will force any requests going through that route to pass authentication in order complete any actions. Express' .get
and .post
convenience functions take any number of middleware callback functions as arguments, so we can just stick our authenticate
function as the first callback argument, and the actual action as the second callback function.
Here's an example from my userRouter.js
file. It shows an unrestricted and restricted route. I'll need to import my auth
module which we created above.
var auth = require('../controllers/authController');
...
// Get all users - Unrestricted Route
app.get('/', userController.getAllUsers);
// Get user by id - Restricted Route
app.get('/:id', auth.authorize, userController.getUserById);
Using the jsonwebtoken
npm module and setting up a some middleware and helper functions, it's very straightforward to get up and running with server side user authentication using JWTs.
JWTs Client Side
JWTs can be used to restrict client side routes. Keep in mind that this is not entirely fool proof as these tokens are accessible to anyone in local storage and easily decoded. Make sure not to store sensitive information, especially passwords, in JWTs and to keep any really sensitive information and actions exclusively on the server.
That said, they are a very convenient way to have a user logged in and to restrict frontend routes the average user can visit.
I used Angular on the frontend of my project and saved all relevant authentication logic in an Auth
service.
For brevity, I'll exclude the factory specific code. I wrote a number of helper functions, but they all revolve around saving the token to local storage, and checking if it exists to see if the user is logged in.
// auth.js
...
const storageKey = 'village.id_token';
/**
* Checks if a user has a token
* @return {[boolean]} [if user is authorized]
*/
const authorized = () => {
return !!getToken();
};
/**
* Returns raw jwt from local storage
* @return {[string]} [returns token or false if doesn't exist]
*/
const getToken = () => {
return $window.localStorage.getItem(storageKey) || false;
}
/**
* Save raw token to local storage. Returns decoded token
* @param {[string]} token [jwt to save]
* @return {[object]} [decoded jwt]
*/
const saveToken = (token) => {
$window.localStorage.setItem(storageKey, token);
return decodeToken();
}
/**
* Deletes jwt from storage
* @return {[boolean]} [true]
*/
const logOut = () => {
$window.localStorage.removeItem(storageKey);
loggedInUser = null;
return true;
}
With the token in storage, we can decode it and access the data it contains. In this case, I was using it to hold on to user specific information. To easily decode the tokens, I included angular-jwt and injected jwtHelper
as a dependency into my service. I wrote in a helper function to handle decoding the token stored in local storage.
// auth.js
...
/**
* Decodes raw jwt and returns payload
* @return {[object]} [returns decoded payload]
*/
const decodeToken = () => {
let token = getToken();
if (token) {
return jwtHelper.decodeToken(token);
} else {
return false;
}
}
By making use of these functions, it's easy to save a new token when a user logs in, to check if a user is already logged in, and get information about that user by decoding an existing token.
In order for the user to take actions on the server, this token must be sent up with any outgoing request. This allows the server to securely verify if the user is logged in with a valid JWT and subsequently allowed to take restricted server side actions. With jQuery and ajax, it's a matter of adding the token as a header. For Angular, it's possible to automatically attach the token on any outgoing requests by making use of $httpProvider.interceptors
. The 'interceptors' is an array of factories, or factory style callbacks, exposing function methods to be run on http requests and responses. In this case, we're only interested in outgoing requests, so we'll only define a single method, request
on our factory. This configuration is done on the main module declaration in my Angular app.
Check out more about $httpProvider.interceptors
here.
...
// Set up JWT authentication
.config(['$httpProvider', function($httpProvider){
// Intercept outgoing http requests and attach jwt token
$httpProvider.interceptors.push('AttachJWT');
}])
.factory('AttachJWT', ['$window', function($window){
return {
// Attach jwt token if it exists
request: (object) => {
let jwt = $window.localStorage.getItem('village.id_token');
if(jwt){
object.headers['x-access-token'] = jwt;
}
object.headers['Allow-Control-Allow-Origin'] = '*';
return object;
}
};
}])
It's also possible to restrict users that are not logged in from accessing certain Angular routes. By listening on the $rootScope
for a $stateChangeStart
event with ui-router
, or $routeChangeStart
with ng-router
, you can interrupt any restricted route changes if the user is not logged in.
This implementation will check if the target state has an authenticate
property set to true. If so, it will check if there is a JWT present on the client. If there is a JWT, the route change continues, if not, the user is redirected back to the 'sign in' state.
The listener is set up in Angular's run block right after declaring and configuring the main module.
...
.run(function($rootScope, $location, $state, Auth) {
$rootScope.$on('$stateChangeStart', function(evt, toState, toParams) {
if (toState && toState.authenticate && !Auth.authorized()) {
console.log('not logged in');
evt.preventDefault();
$state.go('signin');
}
});
The authenticate
property can be placed on the state when it is declared.
$stateProvider.state('dashboard', {
url: '/dashboard/',
params: {
userId: null
},
template: '<dashboard></dashboard>',
authenticate: true
});
There are the essential elements in creating a user authentication system using JWTs. JWTs are a very convenient method to authenticate users in a modern SPA, although they do have their drawbacks. This is a simple implementation that only checks for the existence of a JWT on the client and a valid signature on the server. You must take precautions when using JWTs as they still present a number of security risks. Read more about potential issues here.
Despite that, they are a very convenient way to hang on to user data and authorization without having to constantly query the server.