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(),
});
}
}
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)
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.
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
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?
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)
I am also thinking so it will also remove the chance of duplication with deleting the token on expiration. This comment deserves more votes
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....
Thanks Jonas. I addressed these concerns in the other articles in this series
Awesome write up man
Thanks Vincent
This is for token and not dealing with refresh token ?
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.
thanks