Back to blog
Authentication
Intermediate

Implementing JWT Token Refresh in Node.js with Express and Redis

This guide demonstrates how to implement JWT token refresh in Node.js applications using Express.js and Redis, providing step-by-step instructions, production-ready code examples, and security best practices for modern authentication systems.

October 5, 202320 min read

Introduction

JWT token refresh is a critical component of modern authentication systems, enabling seamless user sessions without requiring re-authentication. This guide explores implementing JWT token refresh in Node.js applications using Express.js and Redis, addressing common challenges like token expiration, security vulnerabilities, and scalability concerns. We'll cover the core concepts, architecture, step-by-step implementation, and production best practices.

Table of Contents

Core Concepts

JWT (JSON Web Token) consists of three parts: header, payload, and signature. The access token typically has a short lifespan (5-15 minutes) while the refresh token has a longer lifespan (days or weeks). The refresh token is stored securely and used to obtain new access tokens when the current one expires. This separation enhances security by minimizing the damage window if an access token is compromised.

Key considerations include:

  • Refresh tokens should never be stored in client-side storage like localStorage due to XSS vulnerabilities
  • Refresh tokens require their own security mechanisms including rotation and revocation
  • Token storage must balance security with usability for the client application
  • Token expiration policies should align with security requirements and user experience expectations

Architecture Overview

The architecture involves three key components: the Express.js server handling authentication requests, Redis for secure token storage and management, and the client application making authenticated requests. The flow is as follows:

  1. Client obtains initial access token and refresh token upon successful login
  2. Client includes access token in Authorization header for API requests
  3. When access token expires, client sends refresh token to server
  4. Server validates refresh token, generates new access token, and optionally rotates the refresh token
  5. Server stores refresh token in Redis with appropriate expiration and revocation capabilities

This architecture provides several advantages: centralized token management, improved security through token rotation, and scalability through Redis caching. The Redis layer enables efficient token validation without database hits for every request.

Step-by-Step Guide

Implementing JWT token refresh requires several coordinated steps:

1. Setup Dependencies

Install required packages:

npm install express jsonwebtoken redis dotenv

2. Configure Redis Connection

Create a Redis client with proper error handling:

const redis = require('redis');const client = redis.createClient({  url: process.env.REDIS_URL || 'redis://localhost:6379'});client.on('error', (err) => {  console.error('Redis error:', err);});async function connectRedis() {  try {    await client.connect();    console.log('Connected to Redis');  } catch (err) {    console.error('Redis connection failed:', err);  }}connectRedis();

3. Token Storage Strategy

Store refresh tokens in Redis with a key structure that allows efficient validation and revocation:

async function storeRefreshToken(userId, refreshToken, expiresIn) {  const tokenKey = `refresh_token:${userId}:${refreshToken}`;  await client.set(tokenKey, 'valid', {    EX: expiresIn * 24 * 60 * 60, // Convert to seconds    NX: true // Only set if not exists  });}async function isRefreshTokenValid(refreshToken, userId) {  const tokenKey = `refresh_token:${userId}:${refreshToken}`;  const result = await client.get(tokenKey);  return result === 'valid';}async function revokeRefreshToken(refreshToken, userId) {  const tokenKey = `refresh_token:${userId}:${refreshToken}`;  await client.del(tokenKey);}

4. Implement Token Refresh Endpoint

Create an endpoint that validates the refresh token and issues new access tokens:

app.post('/api/token/refresh', async (req, res) => {  const { refreshToken } = req.body;    if (!refreshToken) {    return res.status(400).json({ error: 'Refresh token is required' });  }  try {    // Extract user ID from refresh token payload (assumes JWT structure)    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);    const userId = decoded.userId;    // Validate refresh token in Redis    if (!await isRefreshTokenValid(refreshToken, userId)) {      return res.status(401).json({ error: 'Invalid or revoked refresh token' });    }    // Generate new access token    const newAccessToken = jwt.sign({      userId,      role: decoded.role    }, process.env.JWT_ACCESS_SECRET, {      expiresIn: '15m'    });    // Rotate refresh token for enhanced security    const newRefreshToken = jwt.sign({      userId,      role: decoded.role    }, process.env.JWT_REFRESH_SECRET, {      expiresIn: '7d'    });    // Store new refresh token and revoke old one    await storeRefreshToken(userId, newRefreshToken, 7 * 24 * 60 * 60);    await revokeRefreshToken(refreshToken, userId);    res.json({      accessToken: newAccessToken,      refreshToken: newRefreshToken    });  } catch (err) {    console.error('Token refresh error:', err);    res.status(401).json({ error: 'Invalid token' });  }});

5. Client-Side Implementation

On the client side, implement logic to handle token expiration and automatic refresh:

// Example using Axiosimport axios from 'axios';const api = axios.create({  baseURL: 'https://api.example.com',  timeout: 10000});// Interceptor to handle token expirationapi.interceptors.response.use(  response => response,  async error => {    const originalRequest = error.config;        // Only handle token refresh once    if (originalRequest.url !== '/api/token/refresh' &&         error.response && error.response.status === 401) {            // Clear current request queue      if (originalRequest._retry) return Promise.reject(error);      originalRequest._retry = true;      try {        const refreshResponse = await axios.post(          'https://api.example.com/api/token/refresh',          { refreshToken: localStorage.getItem('refreshToken') }        );        // Update tokens        localStorage.setItem('accessToken', refreshResponse.data.accessToken);        localStorage.setItem('refreshToken', refreshResponse.data.refreshToken);        // Retry original request        originalRequest.headers['Authorization'] = `Bearer ${refreshResponse.data.accessToken}`;        return api(originalRequest);      } catch (refreshError) {        // Handle refresh token failure (e.g., logout user)        localStorage.removeItem('accessToken');        localStorage.removeItem('refreshToken');        window.location.href = '/login';        return Promise.reject(refreshError);      }    }    return Promise.reject(error);  });

Real-World Examples

Several industries implement JWT token refresh with varying approaches:

E-commerce Platforms

Online stores use short-lived access tokens (15 minutes) for shopping cart operations and checkout, with refresh tokens stored securely to maintain user sessions during extended browsing sessions. This prevents cart abandonment due to token expiration during checkout.

SaaS Applications

Software-as-a-Service platforms often implement tiered token lifespans: access tokens expire after 10 minutes for security, while refresh tokens last 30 days. This balances security needs with user experience for power users who may be away from their devices for extended periods.

Mobile Applications

Mobile apps typically store refresh tokens in secure keychain/keystore rather than browser storage, with token refresh occurring automatically in the background when the app detects token expiration.

Production Code Examples

Below are production-ready code snippets demonstrating key aspects of JWT token refresh implementation:

Token Validation Middleware

// Middleware to validate access tokenconst authenticateToken = (req, res, next) => {  const authHeader = req.headers['authorization'];  const token = authHeader && authHeader.split(' ')[1];  if (!token) {    return res.status(401).json({ error: 'Access token required' });  }  try {    const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);    req.user = decoded;    next();  } catch (err) {    return res.status(403).json({ error: 'Invalid or expired token' });  }};// Usage in routeapp.get('/api/protected', authenticateToken, (req, res) => {  res.json({ message: 'Protected resource', user: req.user });});

Token Blacklist Management

For immediate token revocation (e.g., after password change), maintain a blacklist in Redis:

async function isTokenBlacklisted(token) {  // Check access token blacklist  const accessTokenKey = `blacklist:${token}`;  return (await client.get(accessTokenKey)) !== null;}// In authentication middlewareif (await isTokenBlacklisted(token)) {  return res.status(401).json({ error: 'Token has been revoked' });}

Refresh Token Rotation

Implementing refresh token rotation enhances security by invalidating old tokens after use:

// In refresh endpointconst oldRefreshToken = refreshToken;const newRefreshToken = generateNewRefreshToken();// Store new token BEFORE revoking old oneawait storeRefreshToken(userId, newRefreshToken, 7 * 24 * 60 * 60);await revokeRefreshToken(oldRefreshToken, userId);

Comparison Table

Approach Security Level Scalability Complexity Use Case
In-Memory Token Storage Low Poor Low Small applications with single instance
Database-Backed Tokens Medium Moderate Medium Applications needing persistent storage
Redis Token Storage High Excellent Medium Scalable applications requiring fast validation
Token Blacklisting Very High Good High Security-sensitive applications needing immediate revocation

Redis storage provides the best balance of security, scalability, and performance for most production applications. While database-backed tokens offer persistence, they introduce latency that Redis avoids through in-memory operations.

Best Practices

Follow these established best practices for secure and maintainable JWT token refresh implementations:

  • Use separate secrets for access and refresh tokens to limit blast radius if one is compromised
  • Rotate refresh tokens on each use to prevent replay attacks
  • Implement token revocation capabilities for immediate session termination
  • Set appropriate token expiration times: access tokens 5-15 minutes, refresh tokens 7-30 days
  • Store refresh tokens in HTTP-only cookies for web applications to prevent XSS theft
  • Implement token binding to associate tokens with specific client devices or user agents
  • Log token refresh events for security monitoring and audit trails
  • Use HTTPS exclusively for all token-related communications

Common Mistakes

Avoid these frequent errors that compromise security or functionality:

  • Storing refresh tokens in client-side storage like localStorage or sessionStorage, making them vulnerable to XSS attacks
  • Using the same secret for access and refresh tokens, increasing security risk
  • Failing to rotate refresh tokens, allowing indefinite token reuse
  • Not implementing token revocation, preventing immediate session termination after security incidents
  • Setting excessively long access token lifespans, increasing exposure window
  • Neglecting to handle token refresh failures gracefully, causing application crashes
  • Not validating refresh token structure and claims before use

Performance Tips

Optimize performance while maintaining security:

  • Use Redis connection pooling for high-traffic applications to reduce connection overhead
  • Implement Redis key expiration matching token lifetimes to prevent memory bloat
  • Batch token validation operations when possible to reduce Redis round trips
  • Use Redis transactions for atomic token operations (store and revoke in single operation)
  • Monitor Redis memory usage and set appropriate maximum memory policies
  • Implement client-side token caching with proper expiration handling to reduce server requests

Security Considerations

Security is paramount when implementing token refresh systems:

  • Always use HTTPS to prevent token interception during transmission
  • Store refresh tokens in HTTP-only cookies to protect against XSS attacks
  • Implement refresh token rotation to prevent replay attacks
  • Use short-lived access tokens to minimize damage from token theft
  • Validate token signatures and claims (issuer, audience, expiration) before use
  • Implement rate limiting on token refresh endpoints to prevent brute force attacks
  • Regularly rotate signing secrets and invalidate old tokens
  • Use Redis security features like ACLs and network isolation

Never store refresh tokens in browser storage accessible to JavaScript. For web applications, HTTP-only cookies are the recommended approach as they prevent client-side script access while maintaining session persistence.

Deployment Notes

When deploying JWT token refresh systems:

  • Ensure Redis is deployed with proper security configurations including network restrictions and authentication
  • Use Redis replication for high availability in production environments
  • Implement Redis backup and recovery procedures for token store durability
  • Configure Redis to use TLS for encrypted in-transit communication
  • Set up Redis monitoring for key metrics like memory usage, connection counts, and error rates
  • Consider using managed Redis services (e.g., AWS ElastiCache) for reduced operational overhead

For containerized deployments, ensure Redis service discovery is properly configured and that token storage paths are consistent across service instances.

Debugging Tips

Common debugging scenarios and solutions:

  • Token verification failures: Check secret mismatches, token expiration, or invalid signature
  • Redis connection issues: Verify network connectivity, authentication, and Redis server status
  • Token refresh loops: Check for missing _retry flag handling in client interceptors
  • Blacklist not working: Ensure blacklist checks occur before token validation in middleware
  • Token rotation not occurring: Verify refresh token storage and revocation logic is correctly implemented

Use detailed logging with token IDs (not full tokens) for security and privacy reasons. Implement health checks that verify both Redis connectivity and token validation functionality.

FAQ

What's the difference between access tokens and refresh tokens?

Access tokens are short-lived credentials used for authenticating API requests, typically expiring within minutes. Refresh tokens are longer-lived credentials used to obtain new access tokens when the current one expires, often lasting days or weeks.

How long should refresh tokens be valid?

Refresh token validity depends on the application's security requirements. Common durations range from 7 days to 30 days. Longer durations increase convenience but also risk if tokens are compromised.

Can I store refresh tokens in Redis without a database?

Yes, Redis alone is sufficient for token storage in most applications. The key is to implement proper key expiration and revocation mechanisms, as Redis provides fast in-memory storage with TTL support.

What happens if a refresh token is compromised?

With token rotation implemented, the compromised token becomes invalid after the first use. The attacker would need the current valid refresh token to obtain new access tokens, which expires quickly if rotation is properly implemented.

Do I need to implement token blacklisting?

Token blacklisting is recommended for security-sensitive applications where immediate session termination is required (e.g., after password changes). It adds complexity but provides an immediate revocation mechanism.

How do I handle token refresh in mobile applications?

Mobile apps should store refresh tokens in secure storage (Keychain for iOS, Keystore for Android) rather than in application preferences. Implement background token refresh logic that detects expiration and silently obtains new tokens.

Is Redis necessary for JWT token refresh?

While not strictly necessary, Redis provides significant advantages in performance and scalability compared to database-backed token storage. For applications with moderate traffic, a simple database table might suffice, but Redis is preferred for high-traffic scenarios.

How often should I rotate my JWT signing secrets?

Rotate signing secrets periodically (e.g., every 3-6 months) and implement key versioning to support seamless rotation without disrupting active sessions. Always maintain backward compatibility during key rotation.

Conclusion

Implementing JWT token refresh with Express.js and Redis provides a robust, secure, and scalable solution for modern authentication systems. By following best practices including token rotation, proper storage mechanisms, and comprehensive security measures, developers can build authentication systems that balance security with user experience. The combination of short-lived access tokens and securely managed refresh tokens significantly reduces security risks while maintaining seamless user sessions.

To get started, implement the step-by-step guide provided here, focusing on security from the initial design phase. Remember that token refresh is not just a technical implementation but a critical security component that requires ongoing maintenance and monitoring.

Call to Action: Implement JWT token refresh in your next project using the patterns described here, and prioritize security through token rotation and proper storage mechanisms. For more in-depth guidance, explore our related articles on JWT fundamentals and Redis session management.