In this article we will learn how to generate and make use of JWT refresh token to request for new access token without having the need to login every time the access token expires. We will also learn how to use angular interceptor in this refresh process to check for access token expiry and immediately trigger the refresh process behind the scenes.

In pervious two articles we implemented JWT authentication in Nodejs and Angular allowing user to register, login and authenticate. You can go through them first as this article is the continuation of those articles and is built on top of the same source code with some more new changes. Here are the links: Implementing JWT Authentication in NodeJS, User Registration, Login and JWT Authentication in Angular.

Checkout the complete source code for Using JWT refresh token in NodeJS and Angular authentication here.

Application Flow

lets first see, what will be the flow of the application.

  • On login from angular frontend, a post request is made to nodeJS backend and we receive access and refresh JWT tokens.
  • We store JWT refresh and access tokens in local storage.
  • User uses this JWT access token to access home until it expires.
  • When JWT access token expires we make a call to backend with JWT refresh token to request for new access token.
  • We then replace the old JWT access token with new one and continue to access home.
  • This process of fetching new access token when its expired is repeated until refresh token expires and user needs to login again to get new pair of access and refresh tokens.
Generating JWT Refresh and Access Tokens on login in NodeJS

In previous article on Implementing JWT Authentication in NodeJS we only generated JWT access token on login and passed it to the client. Now we will also generate JWT refresh token and pass it as well along with access token.

Code is almost the same as the one we implemented previously with some changes. Let’s go through the code.

Below is our app.js file, entry point to the application.

const mongoose = require('mongoose');
require('./models/users.model')
const express = require('express');
require('dotenv').config();
const port = process.env.PORT || "8000";
const usersRoute = require('./routes/index.js');
const cors =  require('cors');

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

const dbURI = process.env.DB_URI;

mongoose
    .connect(dbURI, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
    })
    .then(() => console.log("Database Connected"))
    .catch((err) => console.log(err));

mongoose.Promise = global.Promise;

app.use('/', usersRoute);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  res.status(err.status || 404).json({
    message: "No such route exists"
  })
});

// error handler
app.use(function(err, req, res, next) {
  res.status(err.status || 500).json({
    message: err.message
  })
});

module.exports = app;

app.listen(port, () => {
    console.log(`Listening to requests on http://localhost:${port}`);
  });

The only change done here is on Line 5, where we are importing dotenv as we will be storing and reading secrets and other app constants from environment variables.

Below is the index.js file in route folder.

const express = require('express');
const router = express.Router();
const registrationCtrl = require('../controllers/registration.controller');
const loginCtrl = require('../controllers/login.controller');
const homeCtrl = require('../controllers/home.controller')
const { isAuthenticate } = require('../middlewares/authenticate');

router.post('/users/register',registrationCtrl.register)
router.post('/users/login',loginCtrl.login)
router.get('/home', isAuthenticate, homeCtrl.home)

module.exports = router

On login, Line 9 will be triggered and login.controller will be called.

Below is the login.controller.js file in controllers folder.

const authService = require('../services/auth.service')

module.exports.login = async (req,res) => {
    try{
       await authService.login(req,res)
    }catch(err){
        res.status(err.status).send(error.message)
    }
}

We have separated the service logic from controller to make everything look clean. As you see, we are calling authService.login which contains the service logic, lets see what it contains.

Below is auth.service.js file in service folder.

const mongoose = require('mongoose')
const User = mongoose.model('User')
const bcrypt = require('bcrypt')
const tokenService = require('./token.service')

module.exports = {

    register : async (req,res) => {
        try{
            const ifExists = await User.findOne({ username:req.body.username })
            if (ifExists)
                return res.status(403).json("User already exists")
            const user = new User({
                username:req.body.username,
                password: bcrypt.hashSync(req.body.password,bcrypt.genSaltSync(10))
            })
            const savedUser = await user.save()
            console.log('user registered succesfully',savedUser)
            return res.status(201).json(savedUser)
        }catch(err){
            console.log(err)
            return res.status(500).json('Internal Server Error')
        }
    },

    login : async (req,res) => {
        try{
            await User.findOne({
                username:req.body.username,
            }).exec(async (err,user)=>{
                if(err){
                    res.status(400).json(err)
                }
                else{
                    if(!user){
                        console.log('invalid user')
                        return res.status(401).send("user does not exist!")
                    }
                    if(bcrypt.compareSync(req.body.password,user.password)){
                        console.log('user logged in!',user)
                        let accessToken = await tokenService.getAccessToken(user.username)
                        let refreshToken = await tokenService.getRefreshToken(user.username)
                        return res.status(201).json({accessToken,refreshToken})
                    }
                    else{
                        console.log('user login failed!',user)
                        return res.status(400).json('Unauthorized: Wrong Password!!')
                    }  
                }
            })
        }catch(err){
            console.log(err.message)
            return res.status(500).json('Internal Server Error')
        }
    }
}

Here we have both register and login service logic. There is no change in register logic. Let’s see what changed in login logic.

Here first we check if the user exists, if the user exists we are using bcrypt to check if the password matches with with one in database. When both condition satisfies, we are generating JWT refresh and access tokens by calling token service where we have kept our token related logic.

Below is token.service.js file in service folder.

const jwt = require('jsonwebtoken')
const access_secret = process.env.ACCESS_TOKEN_SECRET
const refresh_secret = process.env.REFRESH_TOKEN_SECRET

module.exports.getAccessToken = (username) => {
    return new Promise((resolve,reject)=>{
        jwt.sign({username},access_secret,{expiresIn:60},(err,token)=>{
            if(err) reject(err)
            resolve(token)
        })
    })
}

module.exports.getRefreshToken = (username) => {
    return new Promise((resolve,reject)=>{
        jwt.sign({username},refresh_secret,{expiresIn:300},(err,token)=>{
            if(err) reject(err)
            resolve(token)
        })
    })
}

module.exports.verifyAccessToken = (accessToken) => {
    return new Promise((resolve,reject)=>{
        jwt.verify(accessToken,access_secret,(err,decode)=>{
            if(err) reject(err)
            resolve(decode.username)
        })
    })
}

Here we are generating JWT refresh and access tokens. Only change in refresh token here is the expiry time. Usually refresh token expiry time is much longer than access token expiry time as we want refresh token to be valid for longer duration to let user logged in for longer time without prompting the user to login in short intervals based on access token expiry time.

Here for testing purpose we have given access token expiry time as 60 seconds and refresh token expire time as 300 seconds. This means that refresh token can be used for 5 minutes to request for new access token, after 5 minutes user will have to login again as refresh token will expire and user will not be able to request for new access token.

Verifying JWT Refresh token to generate new access token

Now let’s see how to use this JWT refresh token to generate new access tokens. We will hit the refresh endpoint from client to request for new access token. Let’s add it in our routes.

Below is the code for index.js in routes folder.

const express = require('express');
const router = express.Router();

const registrationCtrl = require('../controllers/registration.controller');
const loginCtrl = require('../controllers/login.controller');
const homeCtrl = require('../controllers/home.controller')
const refreshCtrl = require('../controllers/refresh.controller')
const { isAuthenticate } = require('../middlewares/authenticate');

router.post('/users/register',registrationCtrl.register)
router.post('/users/login',loginCtrl.login)
router.get('/home', isAuthenticate, homeCtrl.home)
router.post('/refresh',refreshCtrl.refresh)

module.exports = router

Here we have added refresh endpoint and directed it to refresh controller.

Below is the the code for refresh.controller.js file in controllers folder.

const tokenService = require('../services/token.service')

module.exports.refresh = async (req,res) => {
    try{
        const { token } = req.body
        console.log(token)
        if(token){
            const username = await tokenService.verifyRefreshToken(token)
            const accessToken = await tokenService.getAccessToken(username)
            res.send({accessToken})
        }
        else{
            res.status(403).send('token Unavailable!!')
        }
    }catch(err){
        console.log(err)
        res.status(500).json(err)
    }
}

Here we are first verifying the refresh token, if successful, gives the username which we stored in JWT in return. Using that username we create new access token and pass it to the client. Let’s check the token verification logic.

Below is the code for token.service.js in service folder.

const jwt = require('jsonwebtoken')
const access_secret = process.env.ACCESS_TOKEN_SECRET
const refresh_secret = process.env.REFRESH_TOKEN_SECRET

module.exports.getAccessToken = (username) => {
    return new Promise((resolve,reject)=>{
        jwt.sign({username},access_secret,{expiresIn:60},(err,token)=>{
            if(err) reject(err)
            resolve(token)
        })
    })
}

module.exports.getRefreshToken = (username) => {
    return new Promise((resolve,reject)=>{
        jwt.sign({username},refresh_secret,{expiresIn:300},(err,token)=>{
            if(err) reject(err)
            resolve(token)
        })
    })
}

module.exports.verifyAccessToken = (accessToken) => {
    return new Promise((resolve,reject)=>{
        jwt.verify(accessToken,access_secret,(err,decode)=>{
            if(err) reject(err)
            resolve(decode.username)
        })
    })
}

module.exports.verifyRefreshToken = (refreshToken) => {
    return new Promise((resolve,reject)=>{
        jwt.verify(refreshToken,refresh_secret,(err,decode)=>{
            if(err) reject(err)
            resolve(decode.username)
        })
    })
}

Here we have added JWT refresh token verification logic in verifyRefreshToken function where we are verifying the JWT refresh token using secret key stored in .env file and returning the username to controller on successful verification. For generating new access token we are using the same function getAccessToken.

This covers the backend implementation. Let’s see how to handle JWT refresh mechanism in angular client side.

Using Angular Interceptor to handle JWT Refresh request

In User Registration, Login and JWT Authentication in Angular article we used interceptor to append the access token to authorization header in all out going requests. We will modify the same code to take care of JWT token refresh.

Below is the code for auth.interceptor.ts file in src\app\interceptors folder.

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse,
  HttpClient
} from '@angular/common/http';
import {catchError, Observable, switchMap, throwError} from 'rxjs';
import { TokenService } from '../services/token.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  
  refresh=false
  
  constructor(private http:HttpClient, private tokenService : TokenService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {

    const accessToken = this.tokenService.getAccessToken()
    
    if(accessToken){
      
      const req = request.clone({
        setHeaders:{
          authorization : `Bearer ${accessToken}`
        }
      })

      return next.handle(req).pipe(catchError((err: HttpErrorResponse) => {
        if (err.status === 403 && !this.refresh) {
          this.refresh = true;
          const refreshToken = this.tokenService.getRefreshToken()
          return this.http.post('http://localhost:8000/refresh', {token:refreshToken}).pipe(
            switchMap((res: any) => {
              const newAccessToken = res.accessToken
              this.tokenService.storeAccessToken(newAccessToken)
              return next.handle(request.clone({
                setHeaders: {
                  Authorization: `Bearer ${newAccessToken}`
                }
              }));
            })
          ) as Observable<HttpEvent<any>>;
        }
        this.refresh = false;
        return throwError(() => err);
      }));

    }

    return next.handle(request)

  }
}

Here after we have appended the access token to authorization header and sent out the request, we are checking if there is a 403 error which we will get when access token expires. Along with this condition we are also checking if any refresh process is underway by using a refresh variable and setting its value to true when refresh process is ongoing.

Then we call the token service to get the JWT refresh token stored in local storage and pass it to backend call. We get the new access token in return and we store and replace the access token in local storage as well as append it to the authorization header and continue to hit the initial request which failed earlier giving 403 error.

Below is the code for token.service.ts file in src\app\services folder.

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})

export class TokenService {

  constructor() { }

  storeAccessToken = (token:string) => {
    localStorage.setItem('accessToken',token)
  }

  storeRefrshToken = (token:string) => {
    localStorage.setItem('refreshToken',token)
  }

  getAccessToken = () => {
    return localStorage.getItem('accessToken')
  }

  getRefreshToken = () => {
    return localStorage.getItem('refreshToken')
  }

  deleteAccessToken = () => {
    localStorage.removeItem('accessToken')
  }

  deleteRefreshToken = () => {
    localStorage.removeItem('refreshToken')
  }

}

Here, with the help of these functions we are storing, accessing and deleting JWT refresh and access tokens from local storage.

This takes care of our JWT token refresh process. Lets test it now.

Testing the Application for JWT Refresh Token flow

Lets start from user login.

jwt refresh login

As you can see local storage is empty as we have not logged in yet. Lets login.

jwt-refresh-logged-in

As we login we see that we received our JWT refresh and access token stored in local storage.

As we set the expiry time for access token to just one minute. Lets see what happens when we refresh this page after a minute.

refresh access 1

Here we see that when we refresh we get the 403 error from /home as our access token expired but the process does not stop there. Angular interceptor picked up the error and called the refresh endpoint as you see below the refresh request.

As we see here that we are receiving new access token in this refresh request which has now replaced the old access token and stored in local storage.

refresh success

We also see that home is successfully called with new access token in authorization header.

We also gave only 5 minutes expiry time to refresh token. Lets refresh the screen again after 5 minutes and see what happens.

refresh fail

We see here that, first home request failed as our access token expired again the angular interceptor tried to get new access token using refresh token but when it tried that that request also failed as the refresh token also expired and we are being asked to log in again.

As you can see everything is working as expected. This covers over implementation of using JWT refresh tokens.

Summary

In this article we learned how to generate and make use of JWT refresh token to request for new access token allowing user a hassle free experience of staying logged in for longer duration without having the need to login every time the access token expires . We also learned how to use angular interceptor in this refresh process to check for access token expiry and immediately trigger the refresh process behind the scenes.

Leave a Reply

Your email address will not be published. Required fields are marked *