DEV Community

Cover image for Remaining Stateless - Using Redis for token blacklisting in Node JS
Ogbonna Basil
Ogbonna Basil

Posted on • Edited on

Remaining Stateless - Using Redis for token blacklisting in Node JS

JSON web tokens are stateless. That means the server does not maintain the state of the user. No information about who is sending a specific request is saved in the database or session. JWT provides a token to handle such authentication

A major drawback of not maintaining state is that a token is active until it's set expiration date. Therefore even if the user has logged out on the front-end and local storage cleared, anyone one having access to the token can access authenticated routes for that user until the token expires.

A number of solutions have been provided for this drawback:

  • Reducing the JWT expiration duration to a very short time and using refresh tokens with an expiration date or 2 weeks or longer. When the JWT expires, the refresh token is used to generate a new JWT for the user while the user is still logged in.
    One major drawback to this is that the admin can revoke the refresh token at any time and if this happens at the time at the point where the JWT expires, the user has to log in again because you have both a revoked token and an expired JWT.

  • A second approach is to save a blacklisted token on logout in a column of the user table and use it for validation, destroying the previous token when it expires.

    • This is not scalable.
    • It defeats the whole purpose of JWT being stateless as the state is being maintained.
  • A third approach is to invalidate the token for all users by changing the JWT Secret Key. This is likely going to piss off all users as they all have to login again.

  • A fourth approach is to blacklist the token on logout in Redis

What is Redis

Redis is an in-memory data structure store used as a database, cache or message broker. You can use data structures like strings, hashes, lists, sets, sorted sets e.t.c

Why use in-memory instead of database

In-memory relies on main memory for computer data storage and are faster than database management systems that use disk-management systems because the internal optimization algorithms are simpler and execute fewer CPU instructions. Getting data in memory eliminates seek time when querying the data, which provides faster and more predictable performance than disk.
This is more scalable compared to using a DBMS.

Using Redis for validating token in Node

  • Install npm redis package

    npm install redis  
    
  • Import Redis and use redisClient

    import redis from redis  // ES6 +
    import { exec} from 'child_process';// to start the redis database in development 
    /*// for windows user import {execFile} from 'child_process';        
    // for ES5 users
    const redis = require('redis')*/
    // if in development mode use Redis file attached
    // start redis as a child process
    if (process.env.NODE_ENV === 'development') {
    const puts = (error, stdout) =>{
    console.log(error)
    console.log(stdout)
    }
    exec('redis/src/redis-server redis/redis.conf', puts);  
    }
    /* for window implementation 
    execFile('redis/redis-server.exe',(error,stdout)=>{
    if(error){
    throw error
    }
    console.log(stdout)
    })
    */
    export const redisClient = redis.createClient(process.env.REDIS_URL);
    // process.env.REDIS_URL is the redis url config variable name on heroku. 
    // if local use redis.createClient()
    redisClient.on('connect',()=>{
    console.log('Redis client connected')
    });
    redisClient.on('error', (error)=>{
    console.log('Redis not connected', error)
    });
    
  • Use Redis for validating token.

    Pass your Redis action as part of your authentication

    import jwt from 'jsonwebtoken';
    import pool from '../path/to/file';
    import { redisClient } from '../path/to/file';
    import 'dotenv';
    const auth = {
    // eslint-disable-next-line consistent-return
    async checkToken(req, res, next) {
    const token = req.headers.authorization.split(' ')[1]|| req.params.token;
    // check if token exists
    if (!token) {
      return res.status(401).send({
        status: 401,
        error: 'You need to Login',
      });
    }
    /* .......redis validation .........*/
    try {
    
        const result = await redisClient.lrange('token',0,99999999)
        if(result.indexOf(token) > -1){
          return res.status(400).json({
            status: 400,
            error: 'Invalid Token'
        })
      }
       /*
         const invalid = (callback) => {
         redisClient.lrange('token', 0, 999999999, (err, result) => 
         callback(result));
      };
        invalid((result) => {
      // check if token has been blacklisted
         if (result.indexOf(token) > -1){
           return res.status(400).json({
            status: 400,
            error: 'Invalid Token',
         });
       }
    })
    */
    /* ...... ............................*/
     const decrypt = await jwt.verify(token, process.env.SECRET);
      const getUser = 'SELECT * FROM users WHERE id= $1';
      const { rows } = await pool.query(getUser, [decrypt.id]);
      // check if token has expired
      if (!rows[0]) {
        return res.status(403).json({
          status: 403,
          error: ' Token Not accessible',
        });
      }
    
      next();
    } catch (error) {
      return res.status(501).json({
        status: 501,
        error: error.toString(),
      });
    }
    },
    };
    export default auth;
    

Explanation:

The Redis lrange function returns a list of tokens in the array. These token are tokens already blacklisted. if a token used is already blacklisted, the indexOf method returns a value of 0 and above and the 400 response.

Using Redis for Blacklisting in Node

To blacklist a token using Redis

  • Create a route for logout on the backend:

Usually, all that is required is to clear the token from local storage
on logging out but to blacklist the token an endpoint is needed to be
called on logout.

static async logout(req, res) {
    // logout user
    // save token in redis
    const token = req.headers.authorization.split(' ')[1];
    try {
        await redisClient.LPUSH('token', token);
        return res.status(200).json({
          'status': 200,
          'data': 'You are logged out',
        });
    } catch (error) {
      return res.status(400).json({
        'status': 500,
        'error': error.toString(),
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

Explanation:

Redis LPUSH method is similar to the array push method. so basically you add the token to an array named 'token'. On clicking the logout button, the endpoint for the logout is called, the token blacklisted and local storage can then be cleared.

Conclusion:

Redis is a valuable tool. For more uses of Redis read its
documentation , especially caching.

A Redis implementation for Windows is available in this Redis folder.
Feel free to reach out to me on ob_cea or on the threads below.

Top comments (12)

Collapse
 
gileri profile image
Eric G.

Sets are more suited for the case you describe (storing unique, unordered items) as they are faster for your case and avoid accidentally storing duplicates.

The complexity of looking up an item is O(1) with sets, while on average O(n/2) for lists.

Collapse
 
mr_cea profile image
Ogbonna Basil

Your Correct Eric , Sets have a bigO notation of O(1). However every token generated is always unique. Also the push method for a List is also an armotized O(1). But yes you can use sets

Collapse
 
dgroh profile image
Daniel Groh

Why don't u simply use Setx with the expiration date? Then just check if the token exists...Setx will automatically remove the token from the store once it expires even if you don't logout. Why should I keep a list of blacklisted tokens in Redis? Or did I miss the point?

Collapse
 
thegarlynch profile image
thegarlynch

you can also set expiration date for blacklisted item too. it will cause, lesser storage theoritically because you need to invalidate explicitly by logout. but it needs
"appendfsync always". otherwise, blacklisted item unintentionnally disappeared and make it worse than having authenticated token disappeared (since you can always login)

Collapse
 
princebillygk profile image
Prince Billy Graham Karmoker

I am also thinking so it will also remove the chance of duplication with deleting the token on expiration. This comment deserves more votes

Collapse
 
nargonath profile image
Jonas Pauthier

Just reacting to: "the token blacklisted and local storage can then be cleared." at the end explanation. You ought not to use localStorage to store your JWT otherwise you open yourself to XSS attack. You'd better be using httpOnly, secure Cookies to store it: thinktecture.com/en/identity/sames....

Collapse
 
mr_cea profile image
Ogbonna Basil

Thanks Jonas. I addressed these concerns in the other articles in this series

Collapse
 
_criztus profile image
Nmeregini Vincent

Awesome write up man

Collapse
 
mr_cea profile image
Ogbonna Basil

Thanks Vincent

Collapse
 
tabila7070 profile image
Mo Helmi

This is for token and not dealing with refresh token ?

Collapse
 
mr_cea profile image
Ogbonna Basil

Yes Mo. Even though the concept of using refresh tokens was briefly touched, the main focus is on handling authentication tokens themselves for better security.

Collapse
 
tabila7070 profile image
Mo Helmi

thanks