Building a High-Performance Authentication System with JWT and Redis
Introduction
Authentication is the cornerstone of web application security. While JWT (JSON Web Tokens) provide a stateless authentication mechanism, combining them with Redis creates a powerful hybrid approach that offers both performance and security. In this guide, we'll build a robust authentication system that leverages JWT for token-based auth while using Redis for session management, token blacklisting, and refresh token storage.
Why JWT + Redis?
Pure JWT implementations have limitations:
- Token Revocation: JWTs are stateless, making immediate revocation difficult
- Security Risks: Long-lived tokens pose security risks if compromised
- Session Management: No built-in way to track active sessions
Redis solves these issues by providing fast, in-memory storage for token metadata, blacklists, and refresh tokens.
Setting Up the Foundation
First, let's set up our Node.js project with the necessary dependencies:
npm init -y
npm install express jsonwebtoken redis bcryptjs helmet cors dotenv
npm install --save-dev nodemonCreate the basic Express server structure:
// server.js
const express = require('express');
const jwt = require('jsonwebtoken');
const Redis = require('redis');
const bcrypt = require('bcryptjs');
const helmet = require('helmet');
const cors = require('cors');
require('dotenv').config();
const app = express();
const redis = Redis.createClient({ url: process.env.REDIS_URL });
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// Connect to Redis
redis.connect().catch(console.error);JWT Token Management
Let's create a comprehensive token management system:
// utils/tokenManager.js
class TokenManager {
constructor(redisClient) {
this.redis = redisClient;
this.accessTokenSecret = process.env.ACCESS_TOKEN_SECRET;
this.refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET;
this.accessTokenExpiry = '15m';
this.refreshTokenExpiry = '7d';
}
generateTokens(payload) {
const accessToken = jwt.sign(
payload,
this.accessTokenSecret,
{ expiresIn: this.accessTokenExpiry }
);
const refreshToken = jwt.sign(
payload,
this.refreshTokenSecret,
{ expiresIn: this.refreshTokenExpiry }
);
return { accessToken, refreshToken };
}
async storeRefreshToken(userId, refreshToken) {
const key = `refresh_token:${userId}`;
await this.redis.setEx(key, 7 * 24 * 60 * 60, refreshToken); // 7 days
}
async blacklistToken(token) {
const decoded = jwt.decode(token);
if (decoded && decoded.exp) {
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await this.redis.setEx(`blacklist:${token}`, ttl, 'true');
}
}
}
async isTokenBlacklisted(token) {
const result = await this.redis.get(`blacklist:${token}`);
return result === 'true';
}
}Session Management
Implement session tracking to monitor active user sessions:
// utils/sessionManager.js
class SessionManager {
constructor(redisClient) {
this.redis = redisClient;
}
async createSession(userId, deviceInfo = {}) {
const sessionId = this.generateSessionId();
const sessionData = {
userId,
createdAt: new Date().toISOString(),
lastActivity: new Date().toISOString(),
deviceInfo
};
await this.redis.setEx(
`session:${sessionId}`,
24 * 60 * 60, // 24 hours
JSON.stringify(sessionData)
);
// Track user sessions
await this.redis.sAdd(`user_sessions:${userId}`, sessionId);
return sessionId;
}
async updateSessionActivity(sessionId) {
const sessionData = await this.getSession(sessionId);
if (sessionData) {
sessionData.lastActivity = new Date().toISOString();
await this.redis.setEx(
`session:${sessionId}`,
24 * 60 * 60,
JSON.stringify(sessionData)
);
}
}
async getSession(sessionId) {
const data = await this.redis.get(`session:${sessionId}`);
return data ? JSON.parse(data) : null;
}
async revokeAllUserSessions(userId) {
const sessions = await this.redis.sMembers(`user_sessions:${userId}`);
const pipeline = this.redis.multi();
sessions.forEach(sessionId => {
pipeline.del(`session:${sessionId}`);
});
pipeline.del(`user_sessions:${userId}`);
await pipeline.exec();
}
generateSessionId() {
return require('crypto').randomBytes(32).toString('hex');
}
}Authentication Middleware
Create middleware to protect routes and validate tokens:
// middleware/auth.js
const authMiddleware = (tokenManager, sessionManager) => {
return async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required' });
}
const token = authHeader.substring(7);
// Check if token is blacklisted
const isBlacklisted = await tokenManager.isTokenBlacklisted(token);
if (isBlacklisted) {
return res.status(401).json({ error: 'Token has been revoked' });
}
// Verify token
const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
// Validate session if sessionId is present
if (decoded.sessionId) {
const session = await sessionManager.getSession(decoded.sessionId);
if (!session) {
return res.status(401).json({ error: 'Session expired' });
}
// Update last activity
await sessionManager.updateSessionActivity(decoded.sessionId);
}
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
};
};Authentication Routes
Implement login, logout, and token refresh endpoints:
// Initialize managers
const tokenManager = new TokenManager(redis);
const sessionManager = new SessionManager(redis);
const auth = authMiddleware(tokenManager, sessionManager);
// Login endpoint
app.post('/auth/login', async (req, res) => {
try {
const { email, password, deviceInfo } = req.body;
// Validate user credentials (implement your user lookup logic)
const user = await getUserByEmail(email);
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create session
const sessionId = await sessionManager.createSession(user.id, deviceInfo);
// Generate tokens
const { accessToken, refreshToken } = tokenManager.generateTokens({
userId: user.id,
email: user.email,
sessionId
});
// Store refresh token
await tokenManager.storeRefreshToken(user.id, refreshToken);
res.json({ accessToken, refreshToken, user: { id: user.id, email: user.email } });
} catch (error) {
res.status(500).json({ error: 'Login failed' });
}
});
// Logout endpoint
app.post('/auth/logout', auth, async (req, res) => {
try {
const token = req.headers.authorization.substring(7);
// Blacklist current token
await tokenManager.blacklistToken(token);
// Remove session
if (req.user.sessionId) {
const session = await sessionManager.getSession(req.user.sessionId);
if (session) {
await redis.del(`session:${req.user.sessionId}`);
await redis.sRem(`user_sessions:${req.user.userId}`, req.user.sessionId);
}
}
res.json({ message: 'Logged out successfully' });
} catch (error) {
res.status(500).json({ error: 'Logout failed' });
}
});Best Practices and Security Considerations
When implementing JWT with Redis, follow these security practices:
- Short-lived Access Tokens: Keep access tokens short-lived (15-30 minutes)
- Secure Storage: Never store tokens in localStorage; use httpOnly cookies for web apps
- Rate Limiting: Implement rate limiting on auth endpoints
- Token Rotation: Rotate refresh tokens on each use
- Monitoring: Log authentication events and monitor for suspicious activity
Conclusion
Combining JWT with Redis creates a robust authentication system that balances statelessness with security. This approach provides immediate token revocation, session management, and improved security while maintaining the scalability benefits of JWT. The Redis layer adds minimal overhead while solving critical security challenges in token-based authentication.
Remember to implement proper error handling, logging, and monitoring in production environments. This foundation can be extended with features like multi-factor authentication, device tracking, and advanced session analytics.