Authentication in node js REST API using JWT & Passport js

In current article I will discuss node js REST API basic authntication / authorization. Some good npm modules helps us to do it.

jsonwebtoken: Node js wrapper to handle json web tokens 
passport : standard module for authentication / authorization 
passport-jwt for JWT based passport based authorization
passport-local for Password based passport based authentication

So first install it in your express based App

$ npm install jsonwebtoken passport passport-jwt passport-local

Also install bcrypt for password hashing / comparing.

First we should have our model ready ( I am using mongoose base models ). let called models/user.js

var mongoose = require('mongoose');
const bcrypt = require('bcrypt');
var UserSchema = new mongoose.Schema({
  firstName: { type: String },
  lastName: { type: String },
  email: { type: String },
  password: { type: String }
});
UserSchema.pre('save', function (next) {
  var user = this;
  bcrypt.hash(user.password, 10, function (err, hashedPassword){
    if (err) {
      return next(err);
    }
    user.password = hashedPassword;
    next();
 });
});

UserSchema.methods.comparePassword = function (password, callback) {
  bcrypt.compare(password, this.password, function (err, isMatch) {
    if (err) {
      return callback(err);
    }
    callback(null, isMatch);
 });
};

mongoose.model('User', UserSchema);
module.exports = mongoose.model('User');

We have a pre save hook in user model, So when saving password into mongo db, Password bcrypt hashed automatically. Also their are comparePassword method, which we will use for password comparing during authentication.

Now we create a library file for handling passport js specific handling. Let us create services/passport.js

const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const userModel = require('../models/user');

passport.use('local', new localStrategy({
  usernameField: 'email',
  passwordField: 'password'
}, function(email, password, callback) {
  return userModel.findOne({email: email}).then(user => {
    if(!user) {
      return callback(true, false, {msg: 'Incorrect email'})
    }

    return user.comparePassword(password, function (err, isMatch) {
      if (isMatch && !err) {
        return callback(false, user, {msg: 'Success'})
      } else {
        return callback(true, false, {msg: 'Authentication failed. Wrong password.'});
      }
    });
  });
}));

In above file we defined two passport strategy, One local strategy for password authentication other jwt strategy for token authorization. In first function we first check email & then check password using model’s comparePasword call.

Let see how our express controller handle authentication request. lets called controllers/auth.js

var jwt = require('jsonwebtoken');
var passport = require('passport');
require('../services/passport');
var User = require('../models/user');

router.post('/signIn', function (req, res, next) {
  passport.authenticate('local', {session: false}, (err, user, info) => {
    if (err && !user) {
      res.status(401).send({success: false, msg: 'Login Failed'}); 
    } else {
      req.login(user, {session: false}, function(err) {
        if(err) {
          res.status(401).send({success: false, msg: err}); 
        } else {
          var token = jwt.sign({ id: user.id, email: user.email, fullName: user.firstName + ' ' + user.lastName }, config.jwt.secret, {
            expiresIn: 86400 // expires in 24 hours
          });
          return res.json({success: true, token: token}); 
        }
      });
    }
  })(req,res,next);
});

Here we are using passport authentication with local strategy to login user. After successfully login, Code generate a JWT Token as our api session token using sign method and send along with response.

Now next part is authorization. Add following code into passport.js for authorization method.

const jwt = require('jsonwebtoken');
const passportJWT = require('passport-jwt');
const extractJwt = passportJWT.ExtractJwt;
const jwtStrategy = require('passport-jwt').Strategy;
const jwtSecret = 'SOMETOKEN';

passport.use('jwt', new jwtStrategy({
 jwtFromRequest: extractJwt.fromAuthHeaderAsBearerToken(),
 secretOrKey: jwtSecret
}, function(jwtPayload, callback) {
 return userModel.findById(jwtPayload.id)
 .then(user => {
   return callback(null, user)
 })
 .catch(err => {
   return callback(err);
 })
}));

 

This function uses JWT’s fromAuthHeaderAsBearerToken to get token from authorization header from rest request.
Request header must be like that for proper authorization

authorization: Bearer TOKEN

It will be used as middleware in our app.js. So let change our controller invokeĀ  from

app.use('/users', require('./controllers/UserController'));

to

const passport = require('passport');
require('./services/passport');
app.use('/users', passport.authenticate('jwt', {session: false}), require('./controllers/UserController'));

This middleware invoke our jwtStrategy and verify token from http authentcation header. So all requests who do not have header / wrong format of header / wrong data in token will be discarded as unauthenticated requests.

Mongoose ORM basic usage

Mongoose is an ODM ( Object Document mapper ) for node js & mongo db. It map mongo collections to node js models to provide object oriented way of db operations. It can be installed by using npm in current project directory.

$ npm install mongoose --save

Connection can be made in out app/controller files by

var mongoose = require('mongoose');
 mongoose.connect('mongodb://HOSTNAME/DBNAME');

Now we need to add model file for our collections.

We will create following models ->

models/test.js

Let see test model code

const mongoose = require('mongoose')
let Schema = mongoose.Schema;

const TestSchema = new Schema(
{
 string_col: {type: String, default: ''},
 number_col: {type: Number, default: 0},
 bool_col: {type: Boolean, default: null},
 mixed_col: {type: Schema.Types.Mixed, default: ''},
 oid_col: {type: Schema.Types.ObjectId, default: null},
 array_col: [{type: String, default: ''}],
 date_col: { type: Date, default: Date.now() }
})

const testModel = mongoose.model('test', TestSchema);
module.exports = testModel;

Here we describe a schema for mongo collection. Key column _id is automatically added and need not to be described. Fields are described by their types. Following types are supported.

a) String
String type defined for string type fields.

b) Number
Number type defined for number (both) type fields.

c) Bool
Bool type can have boolean values true & false.

d) mixed
Mixed type can have any type. It is mainly used when data is uncertain.

e) ObjectId
Mongo key object id field. Mainly used for foreign key implementation from same / other collection key.

f) Date
Date type handle ISO Date formatted data like ISODate(“2018-03-19T12:51:01.585Z”)

g) Array
This type used to insert array type of data. We can define other type as valid values in array like
[{type: String}]
[{type: Number}]
[{type: Mixed}]
[{type: Date}]

We can create multi level array by using following syntax

[[{type: String}]]

If we do not define type and used syntax like [] or [[]]. Field can have any type of data as values of array.

We can used mongoose save API call to save data into collection.

var testModel = require('./models/test');
var testObj = new testModel();
testObj.string_col = 'Kuldeep';
testObj.number_col = 1234;
testObj.mixed_col = {'age': 45};
testObj.array_col = ['John', 'Doe'];
testObj.bool_col = true;
testObj.oid_col = mongoose.Types.ObjectId("5906d4017362098740b23eeb");
testObj.date_col = new Date();
testObj.save((err, res) => {
if (err) return console.error(err);
   console.log('SAVED;')
   mongoose.disconnect(); 
});

This call create new Object for test model with required properties and save call insert data to collection.

If we need to update data we can use either findOneAndUpdate or Save call

USING findOneAndUpdate

testModel.findOneAndUpdate(
  {_id: mongoose.Types.ObjectId("5aafb50bd1443f49a873784c")}, {
  { $set: {
    string_col: "New Str"
  }
 }, {
   upsert: false
 }, (err, res) => {
    if (err) return console.error(err);
    console.log('SAVED;')
    mongoose.disconnect();
}
);

First argument is condition on which single record is chosen to save.
Second argument have fields to update.
Third argument is optional. If set upsert true, It insert the data as new record if no matching record with condition was found.

findOne + Save Method

testModel.findOne({_id: mongoose.Types.ObjectId("5aafb50bd1443f49a873784c")}, (err, testObj) => {
  if (err) return console.error(err);
  testObj.string_col = 'NEW DATA';
  testObj.save((err, res) => {
  if (err) return console.error(err);
     console.log('SAVED;')
     mongoose.disconnect();
  });
});

This method is similar to insert method as we used same save call to update data, Except we do not used new Model, but get object from DB by using findOne call.