Tech Incent
Node

How to implement node(express) JWT token auth API

how-to-implement-node-express-jwt-token-auth

In this tutorial, I will implement the JWT authorization with the express application to protect endpoints. So resources will be login authorization, user registration, refresh token can generate new access and refresh token

What are authentication and authorization?

Authentication and authorization are used to protect applications and endpoints to secure applications. Particularly when it comes to getting access to applications. Yet, there is a significant distinction between gaining entry into a system (authentication) and what you can do while inside (authorization).

What is Authentication?

Authentication is the process of verifying who a user is, through the credentials and using those credentials to confirm the user’s identity. This is the first step in any security process

You are already aware of the authentication process, logging into your computer) or logging into an application/website. Yet, the truth is that most “things” connected to the Internet require you to prove your identity by providing credentials.

What is Authorization?

Authorization in the application is the process of giving the user permission to access a specific resource or endpoints, The authorization process always follows the authentication procedure.

Giving someone permission to download a particular file and access endpoints on providing credentials

App initial setup

Generate nodejs app

mkdir node-jwt && cd node-jwt
npm init -y

Install dependencies

npm i express jsonwebtoken dotenv bcrypt joi mongoose
npm i nodemon -D

I install dependencies like express, jsonwebtoken, dotenv, joi, mongoose and for the development process, I install nodemon to restart the server when the file changes.

Jsonwebtoken will generate tokens, dotenv help us to configure esally environment variables. Joi help us to validate post data. bcrypt help us to store the password as hash form.

I am going to use MongoDB for the database, so for MongoDB, I install mongoose dependency.

Create directories and files

I create folders to the structure of the express app.

mkdir config db models middlewares routes

Create files

touch config/index.js db/connect.js models/user.model.js middlewares/auth.js routes/index.js routes/auth.routes.js .env

Create server files

touch app.js routes.js
Project folder and file system
sajal@sajal-PC:~/techincent/node-jwt$ tree -L 2 -I node_modules
.
├── app.js
├── config
│   └── index.js
├── db
│   └── connect.js
├── middlewares
│   └── auth.js
├── models
│   ├── refresh-token.model.js
│   └── user.model.js
├── package.json
├── package-lock.json
├── routes
│   ├── auth.route.js
│   └── index.js
└── services
    └── jwt-service.js

6 directories, 11 files

Create node server and connect to mongo database

Set envirments veriables for project

At first, we are going to set the environment variables config. In the .env file, add environment variables.

APP_PORT=8000
DB_URL=mongodb://localhost:27017/jwt-example-db
NODE_ENV=development
SECRET_KEY=5cc4f1f6d0f7b86d4b2d3fe97cf80870393e9c0a0848d7aa45fe9920
REFRESH_KEY=27dbf432b305a6d445cf78bb9ab89ec14e1c00715ddd31eb0c2b2299

I am gonna using my local machine MongoDB database. and my database name jwt-example-db.

Then import environment variable to project in config/index.js file

It’s better to use module import all environments variables in project config. which is good to maintain the project.

const dotenv = require('dotenv');

dotenv.config();

module.exports = {
  APP_PORT,
  DB_URL,
  NODE_ENV,
  SECRET_KEY,
  REFRESH_KEY
} = process.env;

Create database connect

In the db/connect.js file, add the MongoDB configuration connect function

const mongoose = require('mongoose');
const { DB_URL } = require('../config');

exports.connect = () => {
  return mongoose.connect(DB_URL, { useNewUrlParser: true, useUnifiedTopology: true })
    .then(() => {
      console.info("Database connected...")
    })
    .catch((err) => {
      console.error(err);
      process.exit(1);
    })
}

Configrations root route file

In the routes/index.js, create a basic route

module.exports = function (app) {
  
  app.get('', (req, res, next) => {
    res.json({'message': 'Hello! developer'})
  })
  
  // Auth routes will come below
}

Connect routes and start applicaion

In the app.js file,

const express = require('express');
const { APP_PORT } = require('./config');
const dbConnect = require('./db/connect');

const app = express();

const routes = require('./routes');


app.use(express.json());
app.use(express.urlencoded({ extended: false }))

app.listen(APP_PORT || 8000, () => {
  console.info(`Server listen at http://localhost:${APP_PORT || 8000}`)

  // Database
  dbConnect.connect();
  
  // Routes
  routes(app);
  
})

you can see the single routes function which is connect all routes to this project

Add node app terminal script command

In the package.json file, add app server start command

  ...
 "scripts": {
    "start": "nodemon app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
 ...

Start node server

npm start

Great, Bravo successfully start node serve, you can test simple API server with http://localhost:8000

jwt-basic-server
http://localhost:8000

Create auth routes

In the routes/auth.route.js. create user login, register, and refresh token routes. All three routes will be posted to get clint input data

const Joi = require('joi');
const express = require('express');
const router = express.Router();
const JwtService = require('../services/jwt-service');
const User = require('../models/user.model');

const { REFRESH_KEY } = require('../config');

router.post('/login', async (req, res, next) => {
  // Logic will be here
})

router.post('/register', async (req, res, next) => {
  // logic will be here
}),

router.post('/refresh', async (req, res, next) => {
  // logic will be here
})

module.exports = router

Connect auth route to project root route, go to routes/index.js file, and add auth routes

const authRoutes = require('./auth.route');

module.exports = function (app) {
  ...
  // Auth routes
  app.use('/auth', authRoutes);
}

Implement JWTServices

In the services/jwt-service.js. define JwtService class which contain two static method sign, verify. One “sign” method helps to generate jwt token. In the other verify help us to verify tokens. Both methods use SECRET_KEY as the default key.

const jwt = require('jsonwebtoken');
const { SECRET_KEY } = require('../config');

class JwtService {
  static sign(payload, expiry='6h', secret=SECRET_KEY) {
    return jwt.sign(payload, secret, { expiresIn: expiry })
  }
  static verify(token, secret=SECRET_KEY) {
    return jwt.verify(token, secret)
  }
}

module.exports = JwtService

Implement register route logic

In routes/auth.route.js file

// ...
router.post('/register', async (req, res, next) => {
  const formData = req.body;
  const registerSchema = Joi.object({
    name: Joi.string().required(),
    email: Joi.string().email().required(),
    number: Joi.number(),
    password: Joi.string().required(),
    password_repeat: Joi.ref('password'),
  })

  const { error } = registerSchema.validate(formData);
  if (error) {
    return res.status(422).json({ status: 422, message: error?.message });
  }

  // Check user exists
  // console.log('Checking user is exists')
  try {
    const isExists = await User.exists({ email: formData.email });
    if (isExists) {
      return res.status(409).json({ status: 409, message: 'User exists! This email is taken' });
    }
  } catch (err) {
    return next(err)
  }

  // Create user
  // console.log('Create user')
  const userPayload = {
    name: formData.name,
    email: formData.email,
    number: formData.number,
    password: formData.password
  }
  try {
    let user = new User(userPayload);
    // const user = await User.create(userPayload);
    user = await user.save();
    const payload = {
      _id: user._id,
      name: user.name,
      role: user.role,
      email: user.email
    }
    const access_token = JwtService.sign(payload);
    const refresh_token = JwtService.sign(payload, '1y', REFRESH_KEY);
    // Store refresh token
    await RefreshToken.create({ token: refresh_token });
    res.status(201).json({ status: 201, message: 'User created', access_token, refresh_token, data: user })
  } catch (err) {
    return res.status(500).json({ status: 500, message: err.message });
  }
}),
// ...
express-jwt-register-test
Register API Test

Note: Make sure your API request headers contain, “Content-Type”: “application/json”

When you post register API. It will create a user in “user” table and a refresh token in “refreshtokens” table. which mean this store refresh token valid to request new access_token

Implement login route logic

In routes/auth.route.js file

// ...
router.post('/login', async (req, res, next) => {
  // Request validation
  const loginSchema = Joi.object({
    email: Joi.string().email().required(),
    password: Joi.string().required()
  })
  const { error } = loginSchema.validate(req.body);
  if (error) {
    return res.status(422).json({ status: 422, message: error.message });
  }

  // Start database query
  try {
    const user = await User.findOne({ email: req.body.email }).select("-updateAt -__v");
    if (!user) {
      return res.status(401).json({ status: 401, message: 'User is not found' });
    }

    // Check password
    const isPasswordMatch = await user.comparePassword(req.body.password);
    if (!isPasswordMatch) {
      return res.status(401).json({ status: 401, message: 'Your password is wrong' });
    }

    // Generate token
    const payload = {
      _id: user._id,
      name: user.name,
      role: user.role,
      email: user.email
    }
    const access_token = JwtService.sign(payload);
    const refresh_token = JwtService.sign(payload, '30d', REFRESH_KEY)
    // Store refresh token
    await RefreshToken.create({ token: refresh_token });
    res.json({ access_token, refresh_token, data: user, message: 'Sign in success', status: 200 })
    res.end();
  } catch (err) {
    return res.status(500).json({ status: 500, message: err.message });
  }
})
// ...
express-jwt-login-test
Login API Test

Implement refresh token route logic

In routes/auth.route.js file

// ...
router.post('/refresh', async (req, res, next) => {
  const tokenSchema = Joi.object({
    refresh_token: Joi.string().required()
  })
  const { error } = tokenSchema.validate(req.body);
  if (error) {
    return res.status(422).json({ status: 422, message: error.message });
  }

  try {
    const refreshTokenObj = await RefreshToken.findOne({ token: req.body.refresh_token });
    console.log('refresh token', refreshTokenObj)
    if (!refreshTokenObj) {
      return res.status(401).json({ status: 401, message: 'Invalid, Token is not found' });
    }
    let userId;
    try {
      const { _id } = JwtService.verify(refreshTokenObj.token, REFRESH_KEY);
      userId = _id
    } catch (err) {
      return res.status(401).json({ status: 401, message: 'Invalid refresh token' })
    }

    const user = await User.findOne({ _id: userId }).select("-password -updatedAt -__v")

    if (!user) {
      return res.status(401).json({ status: 401, message: 'No user found!' })
    }
    const payload = {
      _id: user._id,
      name: user.name,
      email: user.email,
      role: user.role
    }
    const access_token = JwtService.sign(payload);
    const refresh_token = JwtService.sign(payload, '30d', REFRESH_KEY)
    // Store refresh token
    await RefreshToken.create({ token: refresh_token });
    res.status(201).json({ access_token, refresh_token, user })
  } catch (err) {
    console.log(err)
    return res.status(500).json({ status: 500, message: err.message });
  }
})
// ...
refresh token
Refresh tokens API test

Create authorization middleware

In the middlewares/auth.js file

const JwtService = require("../services/jwt-service");

const authorizationHandler = (req, res, next) => {
  const headerAuthorization = req.headers.authorization;
  if (!headerAuthorization) {
    return res.status(401).json({ status: 401, message: 'Unauthorize user!' });
  }
  // It's for Bearer token header
  const token = headerAuthorization.split(' ')[1];
  try {
    const { _id, email, role } = JwtService.verify(token);
    const user = { _id, email, role }
    req.user = user;
    return next()
  } catch (err) {
    return res.status(401).json({ status: 401, message: 'Invalid token' });
  }
}

module.exports = authorizationHandler;

Create user list protected endpoint

In the routes/index.js file, add users endpoint and applied “authorizationHandler” middleware

// ...
const User = require('../models/user.model');
const authorizationHandler = require('../middlewares/auth');

module.exports = function (app) {
  // ...
  app.get('/users', authorizationHandler, async (req, res, next) => {
    try {
      const users = await User.find();
      return res.json({ status: 200, message: 'Fetch user list', data: users })
    } catch (err) {
      return res.status(500).json({ status: 500, message: err.message })
    }
  })
  // ...
}
Let’s test users protected endpoint

Without access_token

v
Without valid access_token

Let’s try with access_token

User list with valid token

Get tutorial code in Github, Click here