How to Invalidate a JWT in Node.js Using Redis Cache

How to Invalidate a JWT in Node.js Using Redis Cache

This post discusses the vulnerability of the inability to invalidate JSON Web Tokens or JWT and implements a method to fix it.

Scalable web application development is getting more popular, and various tools are available to make the development process simpler and faster. The security of these web applications, however, cannot be overstated. The tools or technologies that simplify the process of building web apps may unintentionally leave gaps for attackers to exploit. It is the developer's responsibility to look for these vulnerabilities that may leave a back door for attackers to use in the system; one of such vulnerabilities is the invalidity of JSON Web Token or JWT. In this tutorial, we will review the vulnerability of JWT invalidity, as well as build a system that invaldates the token which fixes the vulnerability.

Prerequisites

In this tutorial, Node.js and Redis cache will be used to build the JWT invaldation system. You don't need to know how to use Redis; I'll walk you through the setup, but prior knowledge is helpful. The following will be needed to follow through this tutorial:

  • Basic understanding of Node.js modules and npm
  • JavaScript async/await
  • JavaScript try...catch

Introduction to JSON Web Token

JSON Web Token, often known as the JWT, is a stateless sort of token that is used to securely communicate between two parties. JWT is frequently used in server-side web development using Node.js to authorize clients to access resources on a server or an API endpoint.

The jsonwebtoken module on npm is one of the packages commonly used to implement JWT in Node.js. This module allows you to sign, verify, and decode JWT in the Node.js development environment.

Why Invalidate JWT?

The jsonwebtoken module allows us to specify an expiry duration for the JWT to be signed, after which the token becomes invalid. This is currently the only way to invalidate a JWT with this npm module. This option enables attackers to use tokens that are no longer in use but have not yet expired. To improve the security of our system and prevent unauthorized access to the server, a system that invalidates any token that is no longer in use but whose expiration date has not yet passed should be implemented.

To illustrate, consider a simple system in which a client must be authorized before accessing resources on a server. When the client logs out, the token is deleted from the client-side, but this token may still be valid, i.e. it hasn't expired. If this token is accessed by a third party, it can be used to access the resources on the server without the client's knowledge. This is a significant security flaw in our system that must be addressed.

In this article, I will show you a simple approach that can be implemented to invalidate JWT and address the security flaw identified earlier with the jsonwebtoken module.

Invalidating JWT with a Checklist System

One method for invalidating a JWT is to create a checklist system that contains valid and invalid JWT that have been created. This system is developed to check the status of the JWT before being used by the system and also, delete the expired ones in order to make the system more efficient. The fact that JWT are stateless means that they do not need to be saved in a database as it is self-contained and often cached on the client-side, hence the Redis caching system is a good fit for the criteria.

The system to invalidate JWT using checklist is implemented below;

Setup Environment

  • if you have Node installed skip this step else download node from here and install it on your machine. If you prefer to use a package manager to install, read this for all operating systems.
  • To check if you have Node.js installed, run the command below:
    node -v
    
  • Node.js comes bundled with Npm, you don't need to install it again. Run the command below to confirm this:
    npm -v
    
  • Download and Install Redis from here on your local machine. To check if Redis is installed properly, run the command below:
    redis-server
    
    The command above starts the Redis server. In another terminal, run the command below to confirm if Redis is working:
    redis-cli ping
    //PONG
    
  • Now that we have Node.js and Redis cache setup, let's initialize our project by creating a project directory (name according to preference) or continue in an existing directory.
  • Create a file and name it tokenCache.js (or according to preference) using the command below:
    touch tokenCache.js
    
  • Initialize Npm with the line of code that follows or skip this step if you have it initialized already in your directory.
    npm init -y
    

    Install Dependencies

  • Install Node-Redis from npm using the commad below; this is a Redis client for Node.js. Basically, it allows for the interaction with Redis using Node.js on a local machine. You can check out other clients recommended by Redis here.
    npm i redis
    
  • Install jsonwebtoken from npm using the command below:
    npm i jsonwebtoken
    

Implementation of the Checklist System

Step 1

In the tokenCache.js file a connection to the Redis cache on the local machine will be created using the code snippet below:

const redis = require("redis");
const jwt = require("jsonwebtoken")

(async function () {
    const client = redis.createClient();
    client.on("connect", (err) => {
        console.log("Client connected to Redis...");
    });
    client.on("ready", (err) => {
        console.log("Redis ready to use");
    });
    client.on("error", (err) => {
        console.error("Redis Client", err);
    });
    client.on("end", () => {
        console.log("Redis disconnected successfully");
    });
    await client.connect();
    return client;
})()

Step 2

Import the jsonwebtoken module at the top of the file, which is shown with the code lines below:

const redis = require("redis");
const jwt = require("jsonwebtoken")

Step 3

With Redis instantiated and a connection to the cache established. Next, we create a function that takes the JWT as a parameter and adds it to the checklist, this is shown in the code block below:

const addToken = async(token) => {
    try {
        const check = await client.EXISTS(token); // check if token exists in cache already
        if (check == 1) console.error("Token already exist in cache");
        await client.SET(token, "valid"); // set the JWT as the key and its value as valid
        const payload = await jwt.verify(token, "secret-key") // verifies and decode the jwt to get the expiration date
        await client.EXPIREAT(key, +payload.exp); // sets the token expiration date to be removed from the cache
        return;
    } catch (e) {
        console.error("Token not added to cache")
    }
};

Step 4

Above, a function that adds a token to the checklist has been created. Next, another function that checks for the validity of the token in our cache will be created, see below for the code snippet.

const checkToken = async(token) => {
    try {
        const status = await client.GET(token); // get the token from the cache and return its value
        return status;
    } catch (e) {
        console.error("Fetching token from cache failed")
    }
};

Step 5

Lastly, a function that invalidates a token that is no longer in use but whose expiration date has not yet passed would be created, this is show in the code block below.

const blacklistToken = async(token) => {
    try {
        const status = await client.SET(token, "invalid"); // sets the value of the JWT to be invalid
        if (status == "nil") console.error("Token does not exist in cache");
        const payload = await jwt.verify(token, "secret-key") // verifies and decode the jwt to get the expiration date
        await client.EXPIREAT(token, +payload.exp); // sets the token expiration date to be removed from the cache
        return;
    } catch(e) {
        console.error("Token not invalidated")
    }
};

The code blocks contained in the tokenCache.js should look something like the code blocks below:

const redis = require("redis");
const jwt = require("jsonwebtoken")

(async function () {
    const client = redis.createClient();
    client.on("connect", (err) => {
        console.log("Client connected to Redis...");
    });
    client.on("ready", (err) => {
        console.log("Redis ready to use");
    });
    client.on("error", (err) => {
        console.error("Redis Client", err);
    });
    client.on("end", () => {
        console.log("Redis disconnected successfully");
    });
    await client.connect();
    return client;
})()

const addToken = async(token) => {
    try {
        const check = await client.EXISTS(token); // check if token exists in cache already
        if (check == 1) console.error("Token already exist in cache");
        await client.SET(token, "valid"); // set the JWT as the key and its value as valid
        const payload = await jwt.verify(token, "secret-key") // verifies and decode the jwt to get the expiration date
        await client.EXPIREAT(key, +payload.exp); // sets the token expiration date to be removed from the cache
        return;
    } catch (e) {
        console.error("Token not added to cache")
    }
};

const checkToken = async(token) => {
    try {
        const status = await client.GET(token); // get the token from the cache and return its value
        return status;
    } catch (e) {
        console.error("Fetching token from cache failed")
    }
};

const blacklistToken = async(token) => {
    try {
        const status = await client.SET(token, "invalid"); // sets the value of the JWT to be invalid
        if (status == "nil") console.error("Token does not exist in cache");
        const payload = await jwt.verify(token, "secret-key") // verifies and decode the jwt to get the expiration date
        await client.EXPIREAT(token, +payload.exp); // sets the token expiration date to be removed from the cache
        return;
    } catch(e) {
        console.error("Token not invalidated")
    }
};

Conclusion

The implementation provided may not be production-ready; nonetheless, it is intended to express a method for invalidating JWT and may be fine-tuned by the reader.

This is the approach I use personally, and it can be extended to invalidate access and refresh tokens, among other things. Opinions on flaws and how to improve the efficiency of this approach are appreciated.

Thank you for taking the time to read this; cheers to changing the world one line of code at a time.