Implementing JWT Token Blacklisting in NestJS with Redis
In modern web applications, securing authentication flows is critical, especially when using stateless technologies like JSON Web Tokens (JWT). JWT tokens offer a simple, scalable way to authenticate users without storing session data on the server. However, their stateless nature presents a challenge: how do you invalidate or revoke tokens when a user logs out or if a token is compromised?
One popular solution is token blacklisting. In this blog, we’ll explore how to implement JWT token blacklisting in a NestJS application using Redis to ensure that invalid tokens cannot be reused.
Why Blacklist JWT Tokens?
JWTs are stateless by design, meaning they do not require server-side storage. Once issued, they remain valid until their expiration time, unless they are compromised or revoked. This presents a potential security issue in cases such as:
- User Logout: If a user logs out, the token remains valid until it expires.
- Token Compromise: If a token is stolen or compromised, it could be used maliciously unless it’s invalidated.
By implementing a blacklist, we can ensure that tokens that are no longer valid (e.g., after logout) cannot be used.
Using Redis for Token Blacklisting
Redis is an excellent choice for managing token blacklists because it is fast, in-memory, and supports automatic expiration (TTL) of keys. By storing blacklisted tokens in Redis with a TTL that matches the token’s expiration time, you can manage blacklisted tokens efficiently.
Let’s break down the steps for implementing JWT token blacklisting with Redis in NestJS.
Step 1: Install Dependencies
First, install Redis and the necessary Redis client libraries in your NestJS project.
npm install redis @nestjs-modules/ioredis
This will allow us to interact with Redis within our NestJS app.
Step 2: Configure Redis in Your App
Next, set up Redis in your application by configuring it in the AppModule
. We’ll use the @nestjs-modules/ioredis
package to easily integrate Redis
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-modules/ioredis';
@Module({
imports: [
RedisModule.forRoot({
config: {
url: 'redis://localhost:6379', // Replace with your Redis URL
},
}),
],
})
export class AppModule {}
This connects your NestJS app to your Redis server, which will handle storing blacklisted tokens.
Step 3: Blacklist Tokens on Logout
When a user logs out, we want to add their token to Redis with an expiration time matching the token’s remaining validity. This way, even if the token is still valid, it will no longer be accepted by our server.
Here’s an example of how to implement this in the AuthService
:
import { Injectable, Inject } from '@nestjs/common';
import { RedisService } from '@nestjs-modules/ioredis';
import * as jwt from 'jsonwebtoken';
@Injectable()
export class AuthService {
constructor(@Inject(RedisService) private readonly redisService: RedisService) {}
async logout(token: string) {
const decoded: any = jwt.decode(token);
const tokenExpiry = decoded.exp * 1000; // JWT expiry is in seconds, so we convert to milliseconds.
// Calculate the remaining time to live (TTL) for the token.
const currentTime = Date.now();
const ttl = tokenExpiry - currentTime;
if (ttl > 0) {
// Store the token in Redis with the remaining TTL.
await this.redisService.getClient().set(token, 'blacklisted', 'PX', ttl);
}
}
}
In this code:
- We decode the JWT token to find its expiration time.
- We calculate the remaining TTL (time to live) for the token.
- If the token hasn’t expired, we add it to Redis with a TTL equal to the remaining lifespan of the token.
Step 4: Check the Blacklist During Authorization
Before processing any requests that require authentication, we need to check if the token is in the Redis blacklist. If the token is blacklisted, the request should be denied.
Here’s how we can implement this check in a custom JWT guard:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { RedisService } from '@nestjs-modules/ioredis';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private readonly redisService: RedisService,
private readonly jwtService: JwtService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];
if (!token) {
return false; // No token present
}
// Check if the token is blacklisted.
const isBlacklisted = await this.redisService.getClient().get(token);
if (isBlacklisted) {
return false; // Token is blacklisted
}
try {
// Validate the token.
await this.jwtService.verifyAsync(token);
return true;
} catch (err) {
return false; // Invalid token
}
}
}
In this guard:
- We extract the token from the
Authorization
header. - We check if the token is blacklisted by querying Redis.
- If the token is not blacklisted and valid, the request is allowed; otherwise, it is rejected.
Step 5: Setting Token Expiration in Redis
When blacklisting tokens, it’s essential to ensure that the Redis entries expire after the token is no longer valid. By setting the TTL when storing the token, Redis will automatically remove the token after it has expired.
For example, in the logout
method, we set the token’s TTL to its remaining lifetime:
await this.redisService.getClient().set(token, 'blacklisted', 'PX', ttl);
Final Thoughts on Blacklisting JWT Tokens
Implementing JWT token blacklisting with Redis in NestJS adds a layer of security to your application, ensuring that logged-out users or compromised tokens cannot continue to access protected resources.
While JWT provides a stateless authentication mechanism, blacklisting allows you to control token validity and mitigate potential security risks. Redis is an ideal solution for token blacklisting due to its speed and support for key expiration, making it easy to manage invalid tokens without significant overhead.
Securely Send Token to Client
To securely send a token and store it to client side use HTTP-only-cookie. This cookie is not accessible via JavaScript and is automatically included in every request.
@Post('login')
async login(@Body() loginDto: LoginDto, @Res() response: Response) {
const jwt = await this.authService.login(loginDto);
response.cookie('jwt', jwt, { httpOnly: true, secure: true });
return response.send({ message: 'Login successful' });
}
By following the steps in this post, you can enhance your NestJS app’s security and better manage user sessions. Happy coding!
Key Takeaways
- JWT Blacklisting: Invalidate tokens to ensure that they cannot be reused after logout or compromise.
- Redis: Use Redis to efficiently manage token blacklists and automatically remove expired tokens.
- NestJS Guard: Implement a guard to check the blacklist before allowing access to protected resources.
Feel free to reach out if you have any questions or feedback on implementing JWT blacklisting in your NestJS applications!
Note: This blog is not about how to configure JWT
Reference
https://docs.nestjs.com/microservices/redis
https://docs.nestjs.com/security/authentication#jwt-token