How to invalidate a JWT using a blacklist

How to invalidate a JWT using a blacklist

This article is going to show you how to invalidate JWTs using the token blacklist method.

This article is going to show you how to invalidate JWTs using the token blacklist method. The token blacklist method is used when creating a logout system. This is one of the ways of invalidating JWTs on logout request.

One of the main properties of JWT is that it's stateless and is stored on the client and not in the Database. You don't have to query the database to validate the token. For as long as the signature is correct and the token hasn't expired, it would allow the user to access the restricted resource. This is most efficient when you wish to reduce the load on the database. The downside, however, is that it makes invalidating the existing, non-expired token difficult.

Why blacklist?

One reason you would need to invalidate a token is when you're creating a logout system, and JWT is used as your authentication method. Creating a blacklist is one of the various ways to invalidate a token. The logic behind it is straight forward and easy to understand and implement.

A JWT can still be valid even after it has been deleted from the client, depending on the expiration date of the token. So, invalidating it makes sure it's not being used again for authentication purposes. If the lifetime of the token is short, it might not be an issue. All the same, you can still create a blacklist if you wish.

Creating a blacklist

  1. When your web-server receives a logout request, take the token and store it in an in-memory database, like Redis. We are using this because of speed and efficiency, as you don't want to hit your main database every time someone wants to logout. Also, you don't have to store a bunch of invalidated tokens in your database. Take a look at my approach below;

First, create a middleware to verify the token:

const verifyToken = (request, response, next) => {

// Take the token from the Authorization header
  const token = request.header('Authorization').replace('Bearer ', '');
  if (!token) {
    response.status(403).send({
      message: 'No token provided!',
    });
  }

// Verify the token
  jwt.verify(token, config.secret, (error, decoded) => {
    if (error) {
      return response.status(401).send({
        status: 'error',
        message: error.message,
      });
    }

// Append the parameters to the request object
    request.userId = decoded.id;
    request.tokenExp = decoded.exp;
    request.token = token;
    next();
  });
};

Then,

// This is a NodeJs example. The logic can be replicated in any language or framework.

// 1. The server recieves a logout request
// 2. The verifyToken middleware checks and makes sure the token in the request object is valid
router.post('/logout', verifyToken, (request, response) => {

// 3. take out the userId and toekn from the request
  const { userId, token } = request;

// 4. use the get method provided by redis to check with the userId to see if the user exists in the blacklist
  redisClient.get(userId, (error, data) => {
    if (error) {
      response.send({ error });
    }

// 5. if the user is on the blacklist, add the new token 
// from the request object to the list of 
// token under this user that has been invalidated.

/*
The blacklist is saved in the format => "userId": [token1, token2,...]

redis doesn't accept obejcts, so you'd have to stringify it before adding 
*/ 
    if (data !== null) {
      const parsedData = JSON.parse(data);
      parsedData[userId].push(token);
      redisClient.setex(userId, 3600, JSON.stringify(parsedData));
      return response.send({
        status: 'success',
        message: 'Logout successful',
      });
    }

// 6. if the user isn't on the blacklist yet, add the user the token 
// and on subsequent requests to the logout route the user 
// will be found and the token will be appended to the already existing list.
    const blacklistData = {
      [userId]: [token],
    };
    redisClient.setex(userId, 3600, JSON.stringify(blacklistData));
    return response.send({
        status: 'success',
        message: 'Logout successful',
    });
  });
});
  1. Then, for every request that requires that the user is authenticated, you would check the in-memory database to check if the token has been invalidated or not. Then, send a response based on the result from the check. Take a look at my approach below;
module.exports = (request, response, next) => {

// 1. take out the userId and toekn from the request
  const { userId, token } = request;

// 2. Check redis if the user exists 
  redisClient.get(userId, (error, data) => {
    if (error) {
      return response.status(400).send({ error });
    }
// 3. if so, check if the token provided in the request has been blacklisted. If so, redirect or send a response else move on with the request.
    if (data !== null) {
      const parsedData = JSON.parse(data);
      if (parsedData[userId].includes(token)) {
        return response.send({
          message: 'You have to login!',
        });
      }
      return next();
    }
  });
};

To make the search more efficient, you could remove tokens from the blacklist which have already expired. To do this, we would follow the series of steps below:

  1. verify the authenticity of the token
  2. If successfully verified, append the userId, the token itself and its expiration date to the request object.
  3. Store the token in Redis with the expiration date of the token itself.
    // 1. The server receives a logout request
    // 2. The verifyToken middleware checks 
   // and makes sure the token in the request 
   // object is valid and it appends it to the request object, 
   // as well as the token expiration date

    router.post('/logout', verifyToken, (request, response) => {

    // 3. take out the userId, token and tokenExp from the request
      const { userId, token, tokenExp } = request;

    /** 
    4. use the set method provided by Redis to insert the token

    Note: the format being used is to combine 'blacklist_' as a prefix to the token and use it as the key and a boolean, true, as the value. We also set the expiration time for the key in Redis to the same expiration time of the token itself as stated above
    **/
      redisClient.setex(`blacklist_${token}`, tokenExp, true);

    // return  the response
      return response.send({
        status: 'success',
        message: 'Logout successful',
      });
    });

Then, for every request that requires that the user is authenticated, you would need to check your in-memory database to see if the token has been invalidated or not and then send a response based on the result from the check. Take a look at my approach below.

module.exports = (request, response, next) => {

// 1. take out the token from the request
  const { token } = request;

// 2. Check Redis if the token exists. If so, redirect or send a response else move on with the request.
  redisClient.get(`blacklist_${token}`, (error, data) => {
    if (error) {
      return response.status(400).send({ error });
    }
    if (data !== null) {
      return response.send({
        message: 'You have to login!',
      });
    }
// 3. If not, move on with the request.
    return next();
  });
};

Conclusion

This is one of the various ways to invalidate a token. I personally use this approach and it works efficiently. I would like to know your thoughts in the comments.

Thank you for reading, cheers.