A Step-by-Step Guide to Setting Up a Node.js API With Passport-JWT

 Authentication and authorization are a huge part of applications. Whenever there’s an API route without protection or checks, an application can easily become a target for hackers. That’s why we need a secure token — the JSON Web Token (JWT).


The Basics of JWT

I will not go too deeply into JWT, but here are all the basics.

https://cdn-images-1.medium.com/max/800/0*a5Oo7gACg1vPfTMo.png

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JSON web tokens encode and decode your user’s info. They’re used for authorization and information exchange.

They consist of three parts — header, payload, and signature — separated by dots (.) like this: xxxxx.yyyyy.zzzzz

Read more about JSON web tokens here.


Before You Begin

I’ll assume your computer already has npm and testing with Postman, if not check out this video.

Here’s the code if you have any trouble with the process and join Trivin’s slack if you need to ask any questions.


Server Setup

Skip this step if you prefer to use your own server.

If you don’t have a project, we’ll use Trivin to set up project templates. In this article, we’ll use it to create a simple-node-server.

$ npm i trivin -g
$ trivin server simple-node-server -g -i

This will create a simple but well-structured node-server, initialize Git, and install all the project dependencies.


Installation

$ npm i passport passport-jwt winston cors express-validator jsonwebtoken

Supporting Files Setup

$ mkdir store/ 
$ touch store/passport.js store/config.js store/utils.js controller/constant.js

Constant.js

  • First, there’s something I really like to do in the constant.js file. Instead of writing a lot of strings, I create variables for strings that I’m likely to re-use.
  • Allow TextEditor to auto-complete for me and reduce typos in strings.
  • Add these to the constant.js file:
export const EMAIL_IS_EMPTY = 'EMAIL_IS_EMPTY';
export const PASSWORD_IS_EMPTY = 'PASSWORD_IS_EMPTY';
export const PASSWORD_LENGTH_MUST_BE_MORE_THAN_8 =
  'PASSWORD_LENGTH_MUST_BE_MORE_THAN_8';
export const WRONG_PASSWORD = 'WRONG_PASSWORD';
export const SOME_THING_WENT_WRONG = 'SOME_THING_WENT_WRONG';
export const USER_EXISTS_ALREADY = 'USER_EXISTS_ALREADY';
export const USER_DOES_NOT_EXIST = 'USER_DOES_NOT_EXIST';
export const TOKEN_IS_EMPTY = 'TOKEN_IS_EMPTY';
export const EMAIL_IS_IN_WRONG_FORMAT = 'EMAIL_IS_IN_WRONG_FORMAT';

utils.js

  • A file that stores all the functions and validations that are used throughout the project.
  • It makes your code in the API Controller files much cleaner.
import sha256 from 'sha256';
import { check } from 'express-validator';
import {
  PASSWORD_IS_EMPTY,
  PASSWORD_LENGTH_MUST_BE_MORE_THAN_8,
  EMAIL_IS_EMPTY,
  EMAIL_IS_IN_WRONG_FORMAT,
} from './constant';
export const generateHashedPassword = password => sha256(password);
export function generateServerErrorCode(res, code, fullError, msg, location = 'server') {
  const errors = {};
  errors[location] = {
    fullError,
    msg,
  };
return res.status(code).json({
    code,
    fullError,
    errors,
  });
}
// ================================
// Validation:
// Handle all validation check for the server
// ================================
export const registerValidation = [
  check('email')
    .exists()
    .withMessage(EMAIL_IS_EMPTY)
    .isEmail()
    .withMessage(EMAIL_IS_IN_WRONG_FORMAT),
  check('password')
    .exists()
    .withMessage(PASSWORD_IS_EMPTY)
    .isLength({ min: 8 })
    .withMessage(PASSWORD_LENGTH_MUST_BE_MORE_THAN_8),
];
export const loginValidation = [
  check('email')
    .exists()
    .withMessage(EMAIL_IS_EMPTY)
    .isEmail()
    .withMessage(EMAIL_IS_IN_WRONG_FORMAT),
  check('password')
    .exists()
    .withMessage(PASSWORD_IS_EMPTY)
    .isLength({ min: 8 })
    .withMessage(PASSWORD_LENGTH_MUST_BE_MORE_THAN_8),
];

Passport.js Setup

  • A node.js library that helps you with the authentication.
  • Add this to your store/passport.js:
import { Strategy, ExtractJwt } from 'passport-jwt';
import { config, underscoreId } from './config';
import { User } from '../database/models';
export const applyPassportStrategy = passport => {
  const options = {};
  options.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
  options.secretOrKey = config.passport.secret;
  passport.use(
    new Strategy(options, (payload, done) => {
      User.findOne({ email: payload.email }, (err, user) => {
        if (err) {
          return done(err, false);
        }
        if (user) {
          return done(null, {
            email: user.email,
            _id: user[underscoreId]
          });
        }
        return done(null, false);
      });
    })
  );
};
  • store/config.js is where I keep all of my configurations of the app:
export const config = {
  passport: {
    secret: '<Add_Your_Own_Secret_Key>',
    expiresIn: 10000,
  },
  env: {
    port: 8080,
    mongoDBUri: 'mongodb://localhost/test',
    mongoHostName: process.env.ENV === 'prod' ? 'mongodbAtlas' : 'localhost',
  },
};
export const underscoreId = '_id';

Modify app.js to use it with passport:

import express from 'express';
import logger from 'winston';
import bodyParser from 'body-parser';
import cors from 'cors';
import passport from 'passport';
import mongoose from 'mongoose';
import { config } from './store/config';
import { applyPassportStrategy } from './store/passport';
import { userController } from './controller';

const app = express();

// Set up CORS
app.use(cors());

// Apply strategy to passport
applyPassportStrategy(passport);
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// API Route
app.use('/', userController);

/**
 * Get port from environment and store in Express.
 */
const { port, mongoDBUri, mongoHostName } = config.env;
app.listen(port, () => {
  logger.info(`Started successfully server at port ${port}`);
  mongoose
    .connect(mongoDBUri, { useNewUrlParser: true, useUnifiedTopology: true })
    .then(() => {
      logger.info(`Conneted to mongoDB at ${mongoHostName}`);
    });
});

Run Your App

$ npm start

Let’s now go back and improve user.controller.js by applying passport-jwt to our API.


Apply Passport-jwt to Register/Login API

https://cdn-images-1.medium.com/max/800/0*1d_RhWhLZJvy0WFk.png

Image Source: dotnettricks.com

import express from 'express';
import jwt from 'jsonwebtoken';
import { validationResult } from 'express-validator';
import { config } from '../store/config';
import {
  generateHashedPassword,
  generateServerErrorCode,
  registerValidation,
  loginValidation,
} from '../store/utils';
import {
  SOME_THING_WENT_WRONG,
  USER_EXISTS_ALREADY,
  WRONG_PASSWORD,
  USER_DOES_NOT_EXIST,
} from '../store/constant';
import { User } from '../database/models';
const userController = express.Router();
function createUser(email, password) {
  const data = {
    email,
    hashedPassword: generateHashedPassword(password),
  };
  return new User(data).save();
}
/**
 * GET/
 * retrieve and display all Users in the User Model
 */
userController.get('/', (req, res) => {
  User.find({}, (err, result) => {
    res.status(200).json({
      data: result,
    });
  });
});
/**
 * POST/
 * Register a user
 */
userController.post('/register', registerValidation, async (req, res) => {
  const errorsAfterValidation = validationResult(req);
  if (!errorsAfterValidation.isEmpty()) {
    res.status(400).json({
      code: 400,
      errors: errorsAfterValidation.mapped(),
    });
  } else {
    try {
      const { email, password } = req.body;
      const user = await User.findOne({ email });
      if (!user) {
        await createUser(email, password);
        // Sign token
        const newUser = await User.findOne({ email });
        const token = jwt.sign({ email }, config.passport.secret, {
          expiresIn: 10000000,
        });
        const userToReturn = { ...newUser.toJSON(), ...{ token } };
        delete userToReturn.hashedPassword;
        res.status(200).json(userToReturn);
      } else {
        generateServerErrorCode(res, 403, 'register email error', USER_EXISTS_ALREADY, 'email');
      }
    } catch (e) {
      generateServerErrorCode(res, 500, e, SOME_THING_WENT_WRONG);
    }
  }
});
/**
 * POST/
 * Login a user
 */
userController.post('/login', loginValidation, async (req, res) => {
  const errorsAfterValidation = validationResult(req);
  if (!errorsAfterValidation.isEmpty()) {
    res.status(400).json({
      code: 400,
      errors: errorsAfterValidation.mapped(),
    });
  } else {
    try {
      const { email, password } = req.body;
      const user = await User.findOne({ email });
      if (user && user.email) {
        const isPasswordMatched = user.comparePassword(password);
        if (isPasswordMatched) {
          // Sign token
          const token = jwt.sign({ email }, config.passport.secret,         
          {
            expiresIn: 1000000,
          });
          const userToReturn = { ...user.toJSON(), ...{ token } };
          delete userToReturn.hashedPassword;
          res.status(200).json(userToReturn);
        } else {
          generateServerErrorCode(res, 403, 'login password error', WRONG_PASSWORD, 'password');
        }
      } else {
        generateServerErrorCode(res, 404, 'login email error', USER_DOES_NOT_EXIST, 'email');
      }
    } catch (e) {
      generateServerErrorCode(res, 500, e, SOME_THING_WENT_WRONG);
    }
  }
});
export default userController;
  • Instead of using the user’s email and hashed password for authorization, which may not be secured during the communication between the client and the server.
  • We use the JWT token for authorization. This way, we can ensure the security of the password and user’s email to be encrypted.

Testing

  • At this point, I’ll assume that you know how to use Postman.
  • Use the POST/ method and enter localhost:8080/register and localhost:8080/login.
  • After you test your Register API, you’ll successfully get a result similar to below. Copy the token to your clipboard.

https://cdn-images-1.medium.com/max/800/1*EuWQ1Mo_cJwQzsI_V2rSRQ.png

Register API Successful return a token and user’s email + id

Authorization

Let’s see if you want to go to a specific link that requires the user to login. Then, you can simply add an authorization in the API.

Let’s look at an example.

  • In user.controller.js, I include a simple '/' API that retrieves the list of all users — but I don’t want to retrieve all the users unless I log in as a user.
  • Another example is Facebook. If you want to go to the news feed and retrieve all your posts, you need to be logged in.
  • Here’s an example when you go to a secured API Route without a JWT Token (aka, you haven’t logged in):

https://cdn-images-1.medium.com/max/800/1*Oft6XR83WafE79nMBVEjUA.png

An example with no JWT attached to the API


Authorization with Passport JWT

Add these highlights to your user.controller.js:

import express from 'express';
import jwt from 'jsonwebtoken';
import passport from 'passport';
import { validationResult } from 'express-validator';
...
/**
 * GET/
 * retrieve and display all Users in the User Model
 */
userController.get(
  '/',
  **passport.authenticate('jwt', { session: false }),**
  (req, res) => {
    User.find({}, (err, result) => {
      res.status(200).json({
        data: result,
      });
    });
  }
);
...
export default userController;

Now, test the API with Postman. Click on “Authorization”, and choose type “Bearer Token.” Then, paste your token in the token field and run:

https://cdn-images-1.medium.com/max/800/1*y5u1ccu31puCLog8TPeOug.png

With JWT, you’ll be able to retrieve all the users


Well Done!

You’re now able to authorize and secure all other routes that require the user to login before using the API.


Comments

Popular posts from this blog

Flutter for Single-Page Scrollable Websites with Navigator 2.0

A Data Science Portfolio is More Valuable than a Resume

Better File Storage in Oracle Cloud