Building Secure JWT Authentication with Refresh Token Strategy
Introduction
JWT (JSON Web Tokens) have become the de facto standard for authentication in modern web applications. However, implementing JWT authentication securely requires more than just signing a token and sending it to the client. In this comprehensive guide, we'll explore how to build a robust authentication system using JWT access tokens paired with refresh tokens, addressing common security vulnerabilities and best practices.
Understanding the JWT Security Challenge
The primary security concern with JWTs is their stateless nature. Once issued, a JWT remains valid until it expires, regardless of whether the user logs out or their account is compromised. This creates several risks:
- Token hijacking: If an access token is stolen, attackers can use it until expiration
- Long-lived tokens: Tokens with extended expiration times increase security windows
- No server-side revocation: Pure JWTs cannot be invalidated server-side
The refresh token strategy solves these issues by using short-lived access tokens paired with longer-lived refresh tokens.
The Refresh Token Architecture
Our authentication system will use two types of tokens:
- Access Token: Short-lived (15-30 minutes), contains user claims, used for API requests
- Refresh Token: Longer-lived (7-30 days), stored securely, used only to generate new access tokens
Database Schema
First, let's set up a table to store refresh tokens:
CREATE TABLE refresh_tokens (
id VARCHAR(255) PRIMARY KEY,
user_id INT NOT NULL,
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_revoked BOOLEAN DEFAULT FALSE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);Implementation with Node.js and Express
Let's build a complete authentication system. First, install the required dependencies:
npm install jsonwebtoken bcryptjs crypto mysql2 expressJWT Utility Functions
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class TokenService {
static generateTokens(payload) {
const accessToken = jwt.sign(
payload,
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = crypto.randomBytes(64).toString('hex');
return { accessToken, refreshToken };
}
static verifyAccessToken(token) {
try {
return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
} catch (error) {
return null;
}
}
static async storeRefreshToken(userId, refreshToken) {
const tokenHash = crypto.createHash('sha256')
.update(refreshToken).digest('hex');
const tokenId = crypto.randomUUID();
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
// Store in database
await db.query(
'INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at) VALUES (?, ?, ?, ?)',
[tokenId, userId, tokenHash, expiresAt]
);
return tokenId;
}
}Authentication Middleware
const authenticateToken = async (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' });
}
const decoded = TokenService.verifyAccessToken(token);
if (!decoded) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = decoded;
next();
};Login Endpoint
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Validate user credentials
const user = await getUserByEmail(email);
if (!user || !await bcrypt.compare(password, user.password_hash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate tokens
const payload = { userId: user.id, email: user.email, role: user.role };
const { accessToken, refreshToken } = TokenService.generateTokens(payload);
// Store refresh token
await TokenService.storeRefreshToken(user.id, refreshToken);
// Set refresh token as httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
});
res.json({
success: true,
accessToken,
user: { id: user.id, email: user.email, role: user.role }
});
} catch (error) {
res.status(500).json({ error: 'Authentication failed' });
}
});Token Refresh Endpoint
app.post('/auth/refresh', async (req, res) => {
try {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
// Hash the token to compare with database
const tokenHash = crypto.createHash('sha256')
.update(refreshToken).digest('hex');
// Validate refresh token
const [tokenRecord] = await db.query(
'SELECT * FROM refresh_tokens WHERE token_hash = ? AND expires_at > NOW() AND is_revoked = FALSE',
[tokenHash]
);
if (!tokenRecord) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
// Get user data
const [user] = await db.query('SELECT * FROM users WHERE id = ?', [tokenRecord.user_id]);
// Generate new access token
const payload = { userId: user.id, email: user.email, role: user.role };
const { accessToken } = TokenService.generateTokens(payload);
res.json({ success: true, accessToken });
} catch (error) {
res.status(500).json({ error: 'Token refresh failed' });
}
});Security Best Practices
1. Secure Cookie Configuration
Always use httpOnly, secure, and sameSite flags for refresh token cookies:
res.cookie('refreshToken', refreshToken, {
httpOnly: true, // Prevents XSS attacks
secure: true, // HTTPS only in production
sameSite: 'strict', // CSRF protection
maxAge: 30 * 24 * 60 * 60 * 1000
});2. Token Rotation
For enhanced security, rotate refresh tokens on each use:
// In refresh endpoint, after validating old token
const { accessToken, refreshToken: newRefreshToken } = TokenService.generateTokens(payload);
// Revoke old token and store new one
await db.query('UPDATE refresh_tokens SET is_revoked = TRUE WHERE token_hash = ?', [tokenHash]);
await TokenService.storeRefreshToken(user.id, newRefreshToken);3. Logout Implementation
app.post('/auth/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (refreshToken) {
const tokenHash = crypto.createHash('sha256')
.update(refreshToken).digest('hex');
await db.query(
'UPDATE refresh_tokens SET is_revoked = TRUE WHERE token_hash = ?',
[tokenHash]
);
}
res.clearCookie('refreshToken');
res.json({ success: true, message: 'Logged out successfully' });
});Frontend Integration
On the client side, implement automatic token refresh:
// Axios interceptor for automatic token refresh
axios.interceptors.response.use(
(response) => response,
async (error) => {
const original = error.config;
if (error.response?.status === 403 && !original._retry) {
original._retry = true;
try {
const response = await axios.post('/auth/refresh', {}, {
withCredentials: true
});
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
return axios(original);
} catch (refreshError) {
// Redirect to login
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);Conclusion
Implementing JWT authentication with refresh tokens provides a robust security layer while maintaining good user experience. The key benefits include reduced attack windows through short-lived access tokens, server-side revocation capabilities, and protection against common web vulnerabilities.
Remember to regularly clean up expired tokens from your database and monitor authentication patterns for suspicious activity. This authentication strategy forms the foundation of secure modern web applications and should be implemented with careful attention to the security practices outlined above.
Related Posts
Building Secure Authentication with JWT and Refresh Tokens in 2024
Learn how to implement bulletproof JWT authentication with refresh token rotation and security best practices.
Building Secure REST APIs with JWT Authentication: A Complete Implementation Guide
Learn to implement robust JWT authentication in your APIs with proper security practices and real-world code examples.
Implementing Multi-Factor Authentication with JWT and Time-Based OTP in Node.js
Learn to build secure MFA using JWT tokens and time-based one-time passwords with practical Node.js implementation.