JWT Like ACDC

Dec 18 2016

JWT

JSON Web Token (JWT) is a self-contained way for securely transmitting information between parties as a JSON object.

Since JWT can encode users' data, it's a great way to handle API authentication & authorization. However, JWT are stateless, so we can't use standard Sessions. But don't despair, with a little bit of configuration, we can manage all our authentication & authorization requirements in a single place.

Passport

Passport is authentication middle ware. It is designed to serve a singular purpose: authenticate requests. Authentication is done by defining "strategies."

What we're gonna do?

We'll secure our app endpoints with JWT. Once a user authenticates himself or herself by supplying a password, we'll generate a JWT for that user, who can then interact with our API by supplying this token at his or her requests headers/cookies.

I assume you are already familiar with Express, Sequelize, Mocha & Chai.

User Model

First, we'll define our User

// user_spec.js


const db = require('../../../server/models/index.js');
const expect = require('chai').expect;


describe("User", () => {
  // clean the db before each test
  beforeEach((done) => {
    db.sequelize.sync({ force: true, logging: false }).then(() => { done(); });
  });


  it('should be defined', () => {
    expect(db.User).to.be.ok;
  });
});


And create the migration & model:

// 20161216233015-create-user.js
module.exports = {
  up: function(queryInterface, Sequelize) {
    return queryInterface.createTable('Users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      email: {
        type: Sequelize.STRING
      },
      active: {
        type: Sequelize.BOOLEAN
      },
      password_digest: {
        type: Sequelize.STRING
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: function(queryInterface, Sequelize) {
    return queryInterface.dropTable('Users');
  }
};




// user.js
module.exports = function(sequelize, DataTypes) {
  var User = sequelize.define('User', {
    email: {
      type: DataTypes.STRING,
      allowNull: false,
      validate: {
        isEmail: true,
        notEmpty: true,
        len: [1,255]
      }
    },
    active: DataTypes.BOOLEAN,
    password_digest: {
      type: DataTypes.STRING,
      validate: {
        notEmpty: true
      }
    },
    password: {
      type: DataTypes.VIRTUAL,
      allowNull: false,
      validate: {
        notEmpty: true
      }
    },
    password_confirmation: {
      type: DataTypes.VIRTUAL
    }
  }, {
    indexes: [{unique: true, fields: ['email']}],
    classMethods: {
      associate: function(models) {
      }
    }
  });


  return User;
};


User controller

We'll define a controller functionality that retrieves users from our DB. Nothing fancy here:

// user_controller.js
import db from '../models/index';


const User = db.User;


export const index = async (req, res) => {
  try {
    User.findAll({
      attributes: ['id', 'email'],
    }).then((users) => {
      res.json(users.map(u => u.dataValues));
    });
  }
  catch(err) {
    console.error(err);
  }
};


Router

// routes/index.js
import express from 'express';
import passport from 'passport';
var router = express.Router();


import models from '../models/index';
import * as userController from '../controllers/user_controller';




router.get('/', (req, res, next) => {
  res.render('layout', { title: 'The index page!' , DEV_SERVER: process.env.DEV_SERVER, nodemon_live_reload: change_me_to_see_live_reload });
});




router.get('/user/index', userController.index);


export default router;


User Password

We now can perform GET requests, and view our users' data, but so can everyone else. This is not ideal.

We want to identify our users by their emails/passwords. Before creating & updating our users we'll make sure: 

  • emails are lowercase  
  • emails are unique  
  • we use hash passwords
// user_spec.js
  .
  .
  .
  describe('create', () => {
    describe('when params are valid', () => {
      it('should insert to db', async () => {
        const args = { email: '[email protected]', password: '123456', password_confirmation: '123456' };
        await db.User.create(args);
        const users = await db.User.findAll({where: {email: '[email protected]'}});
        expect(users.length).to.eql(1);
      });
    });
  });


// user.js
var bcrypt = require('bcrypt-nodejs');


'use strict';
module.exports = function(sequelize, DataTypes) {
  .
  .
  .


  const hasSecurePassword = (user, options, callback) => {
    if (user.password != user.password_confirmation) {
      throw new Error("Password confirmation doesn't match Password");
    }
    bcrypt.hash(user.get('password'), null, null, function(err, hash) {
      if (err) {
        return callback(err);}
      user.set('password_digest', hash);
      return callback(null, options);
    });
  };


  User.beforeCreate((user, options, callback) => {
    user.email = user.email.toLowerCase();
    if (user.password)
      hasSecurePassword(user, options, callback);
    else
      return callback(null, options);
  })
  User.beforeUpdate((user, options, callback) => {
    user.email = user.email.toLowerCase();
    if (user.password)
      hasSecurePassword(user, options, callback);
    else
      return callback(null, options);
  })


  return User;
};


Password Authentication Strategy

We'll use  passport-local  to define our by_password strategy.  
This strategy receives emails/passwords as input, and tries to validate the user.

// passport_strategies.js
import passport from 'passport';
import bcrypt from 'bcrypt-nodejs';


const db = require('../models');
const LocalStrategy = require('passport-local').Strategy;


module.exports = (app) => {
  app.use(passport.initialize());
  // Serialize Sessions
  passport.serializeUser((user, done) => {
    done(null, user);
  });


  // Deserialize Sessions
  passport.deserializeUser((user, done) => {
    done(null, user);
  });


  /* ******************* PASSWORD ******************* */
  const verifyByPassword = (email, password, done) => {
    db.User.findOne({ where: { email } })
      .then((dbuser) => {
        if (!dbuser) {
          done(false, null);
        } else {
          bcrypt.compare(password, dbuser.password_digest, (err, isvalid) => {
            if (isvalid) {
              done(null, dbuser);
            } else {
              done(err, null);
            }
          });
        }
      })
      .error((err) => {
        console.log('LocalStrategy err', err);
        done(err, null);
      });
  };
  const by_password_options = {
    usernameField: 'email',
    passwordField: 'password',
  };


  const by_password = new LocalStrategy(by_password_options, verifyByPassword);
  passport.use('by_password', by_password);
};


Now that we can authenticate users, we want to generate JWT tokens for them.  In this example, we just encode the user's ID as well as additional data, for example, his or her permissions/role in our system, or by the token expiry:

//jwtHelper.js
import jwt from 'jsonwebtoken';


const jwtHelper = {};


jwtHelper.getJwtSecret = () => {
  if (!process.env.JWT_SECRET && process.env.NODE_ENV === 'production') {
    console.error("passport_strategies: process.env.JWT_SECRET is not set while running in production mode!");
    return null;
  }
  return process.env.JWT_SECRET || 'JWT_SECRET';
};


jwtHelper.generateJwt = (user) => {
  return jwt.sign({
    id: user.id,
  }, jwtHelper.getJwtSecret());
};
export default jwtHelper;




All we need to do now is define an endpoint the user can get his JTW from:

// auth_controller.js
import jwtHelper from '../helpers/jwt_helper'


export const authenticated = async (req, res) => {
  try {
    res.json({
      token: `JWT ${jwtHelper.generateJwt(req.user)}`,
    });
  }
  catch(err) {
    console.error(err);
  }
}


// routes/index.js
import * as authController from '../controllers/auth_controller';
.
.
.
router.post('/authenticate', passport.authenticate('by_password'), authController.authenticated);


JWT Authentication Strategy

Passport was kind to us, and supplied  ExtractJwt.fromAuthHeader  to extract the JWT token from the requests header. It assumes the client will add the token in the  Authorization  header with the value  JWT <...token...>

One important note is that after we decode the JWT token, we do not pass that as our user object.  Instead, we fetch the latest user data from the DB in case the token is out of date (for example, the user was deleted by system admin):

  import jwtHelper from '../helpers/jwt_helper'
  const JwtStrategy = require('passport-jwt').Strategy;
  const ExtractJwt = require('passport-jwt').ExtractJwt;
  .
  .
  .
  
  const jwtOptions = {
    // Telling Passport to check authorization headers for JWT
    jwtFromRequest: ExtractJwt.fromAuthHeader(),
    // Telling Passport where to find the secret
    secretOrKey: jwtHelper.getJwtSecret(),
  };


  // Setting up JWT login strategy
  const jwtLogin = new JwtStrategy(jwtOptions, (payload, done) => {
    db.User.findOne({ where: { id: payload.id } })
    .then((user) => {
      if (user) {
        done(null, user);
      } else {
        done(null, false);
      }
    });
  });


  passport.use('jwt', jwtLogin);


Now, we can protect our  /user/index  endpoint:

// routes/index.js
import * as authController from '../controllers/auth_controller';
.
.
.
router.get('/user/index', passport.authenticate('jwt'), userController.index);




Cookie JWT Authorization

Our JWT strategy is great for API. It supplies all the protection we have from the standard API tokens system, with the advantages of JWT encoded data (such as permissions, expiry).

However, this doesn't help us much when creating standard Ajax requests from the web app. It forces us to add an Authorization header to every request at the client side.

We can avoid that if we will store the JWT token on the browser cookies.

Luckily, passport is very modular, and we can expand our JWT strategy to do just that. It will therefore look for the JWT token on the request header. And if not found, it will search for it on our cookie.

// passport_strategies.js
  .
  .
  .
  const cookieExtractor = (req) => {
    if (req && req.cookies && req.cookies.jwt_token) { return req.cookies.jwt_token; }
    return null;
  };
  const jwtOptions = {
    jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeader(), cookieExtractor]),
    secretOrKey: jwtHelper.getJwtSecret(),
  };


Testing

That's it. We can now use tools such as cUrl or Postman to check our setup:  

  • POST  /authenticate  with email/password params should return a JWT token.  
  • GET  /user/index  with the token in the cookies or Authorization header will give us the user list.

Let's add some integration tests for that:

//authentication_routes_spec.js
import request from 'supertest';
import server from '../../../server/server';
import db from '../../../server/models';


describe('authentication', () => {
  describe('POST /authenticate', () => {
    const user_params = {
      email: '[email protected]',
      password: 'password',
      password_confirmation: 'password',
    };


    const expectJwtTokenOn = (body) => {
      if (!body.token) { throw new Error('missing token'); }
      if (body.token.split('.').length !== 3) { throw new Error('invalid JWT'); }
    };


    before((done) => {
      db.sequelize.sync({ force: true, logging: false }).then(() => { done(); });
    });
    before((done) => {
      db.User.create(user_params).then(() => { done(); });
    });


    describe('when email and password are valid', () => {
      it('responds with status 200 and valid JWT token', (done) => {
        request(server)
          .post('/authenticate')
          .send({ email: user_params.email, password: user_params.password })
          .expect(200)
          .expect((res) => {
            expectJwtTokenOn(res.body);
          })
          .end(done);
      });
    });


    describe('when email and password are invalid', () => {
      it('responds with 401 Unauthorized', (done) => {
        request(server)
          .post('/authenticate')
          .send({ email: '[email protected]', password: user_params.password })
          .expect(401)
          .end(done);
      });
    });
  });
});


// user_route_spec.js
import request from 'supertest';
import server from '../../../server/server';
import db from '../../../server/models';
import jwtHelper from '../../../server/helpers/jwt_helper';


describe('user routes', () => {
  describe('GET /user/index', () => {
    const user_params = {
      email: '[email protected]',
      password: 'password',
      password_confirmation: 'password',
      active: true,
    };


    const expectJwtTokenOn = (body) => {
      if (!body.token) { throw new Error('missing token'); }
      if (body.token.split('.').length !== 3) { throw new Error('invalid JWT'); }
    };


    before((done) => {
      db.sequelize.sync({ force: true, logging: false }).then(() => { done(); });
    });
    before((done) => {
      db.User.create(user_params).then(() => { done(); });
    });


    describe('when jwt is missing', () => {
      it('responds with status 401', (done) => {
        request(server)
          .get('/user/index')
          .expect(401)
          .end(done);
      });
    });


    describe('when jwt found', () => {
      let jwt_token = null;
      before((done) => {
        db.User.findOne({ where: { email: user_params.email } })
          .then((user) => {
            jwt_token = jwtHelper.generateJwt(user);
            done();
          });
      });
      it('in header', (done) => {
        request(server)
          .get('/user/index')
          .set('Authorization', `JWT ${jwt_token}`)
          .expect(200)
          .end(done);
      });
      it('in cookies', (done) => {
        request(server)
          .get('/user/index')
          .set('Cookie', [`jwt_token=${jwt_token}`])
          .expect(200)
          .end(done);
      });
    });
  });
});


Resources

About JWT  
PassportJS  
Passport Local  
Passport JWT  
Working example

Guy Y.
Software Developer
Back to Blog