Remaining Stateless - Using Redis for token blacklisting in Node JS
Tags: redis, jwt
Published: 2019-07-22 09:27:25

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

     ```javascript
npm install redis  
```

  - Import Redis and use redisClient

     ```javascript
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

    ```javascript
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.

```javascript 
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(),
      });
    }
  }
```
#### 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](https://redis.io/documentation) , especially [caching] (https://redis.io/topics/lru-cache).

A Redis implementation for Windows is available in this [Redis](https://github.com/basilcea/askLite) folder.
Feel free to reach out to me on [ob_cea](https://twitter.com/OB_CEA) or on the threads below.