← Back to Learn
JWT Token Lifecycle
Understanding the complete lifecycle of JWT tokens: generation, validation, refresh, and revocation.
1. Token Generation
Token Generation
// Step 1: User authenticates
async function login(username, password) {
// Verify credentials
const user = await verifyCredentials(username, password);
if (!user) {
throw new Error('Invalid credentials');
}
// Step 2: Generate access token
const accessToken = jwt.sign(
{
sub: user.id,
role: user.role,
permissions: user.permissions,
iat: Math.floor(Date.now() / 1000)
},
ACCESS_SECRET,
{
algorithm: 'HS256',
expiresIn: '15m', // Short-lived
issuer: 'my-app',
audience: 'my-api'
}
);
// Step 3: Generate refresh token
const refreshToken = jwt.sign(
{
sub: user.id,
type: 'refresh',
iat: Math.floor(Date.now() / 1000)
},
REFRESH_SECRET,
{
algorithm: 'HS256',
expiresIn: '7d', // Longer-lived
issuer: 'my-app'
}
);
// Step 4: Return tokens
return {
accessToken,
refreshToken,
expiresIn: 900 // 15 minutes in seconds
};
}2. Token Validation
Token Validation
// Middleware for token validation
async function validateToken(req, res, next) {
try {
// Step 1: Extract token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
// Step 2: Verify signature and expiration
const decoded = jwt.verify(token, ACCESS_SECRET, {
algorithms: ['HS256'],
issuer: 'my-app',
audience: 'my-api',
clockTolerance: 60 // Allow 60s clock skew
});
// Step 3: Check if token is blacklisted
if (await isTokenBlacklisted(token)) {
return res.status(401).json({ error: 'Token revoked' });
}
// Step 4: Verify token version (if using versioning)
const user = await db.users.findById(decoded.sub);
if (user.tokenVersion !== decoded.tokenVersion) {
return res.status(401).json({ error: 'Token invalidated' });
}
// Step 5: Attach user info to request
req.user = decoded;
req.userId = decoded.sub;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}
return res.status(500).json({ error: 'Token validation failed' });
}
}3. Token Refresh
Token Refresh Flow
// Client-side: Automatic token refresh
let accessToken = null;
let refreshToken = null;
async function refreshAccessToken() {
if (!refreshToken) {
redirectToLogin();
return;
}
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
credentials: 'include'
});
if (!response.ok) {
throw new Error('Refresh failed');
}
const data = await response.json();
accessToken = data.accessToken;
// Optionally rotate refresh token
if (data.refreshToken) {
refreshToken = data.refreshToken;
}
return accessToken;
} catch (error) {
// Refresh token expired - redirect to login
redirectToLogin();
}
}
// Server-side: Refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
// Verify refresh token
const decoded = jwt.verify(refreshToken, REFRESH_SECRET, {
algorithms: ['HS256'],
issuer: 'my-app'
});
// Check if refresh token is blacklisted
if (await isTokenBlacklisted(refreshToken)) {
return res.status(401).json({ error: 'Refresh token revoked' });
}
// Get current user data
const user = await db.users.findById(decoded.sub);
// Generate new access token
const newAccessToken = jwt.sign(
{
sub: user.id,
role: user.role,
permissions: user.permissions,
tokenVersion: user.tokenVersion,
iat: Math.floor(Date.now() / 1000)
},
ACCESS_SECRET,
{
algorithm: 'HS256',
expiresIn: '15m',
issuer: 'my-app',
audience: 'my-api'
}
);
// Optionally rotate refresh token
const newRefreshToken = jwt.sign(
{
sub: user.id,
type: 'refresh',
iat: Math.floor(Date.now() / 1000)
},
REFRESH_SECRET,
{
algorithm: 'HS256',
expiresIn: '7d',
issuer: 'my-app'
}
);
// Blacklist old refresh token (optional)
await blacklistToken(refreshToken);
res.json({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiresIn: 900
});
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});4. Token Revocation
Token Revocation
// Logout - Revoke tokens
async function logout(req, res) {
try {
const token = extractToken(req);
const refreshToken = req.body.refreshToken;
// Decode token to get jti
const decoded = jwt.decode(token);
const refreshDecoded = jwt.decode(refreshToken);
// Add to blacklist
if (decoded?.jti) {
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`blacklist:${decoded.jti}`, ttl, '1');
}
}
if (refreshDecoded?.jti) {
const ttl = refreshDecoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`blacklist:${refreshDecoded.jti}`, ttl, '1');
}
}
// Alternative: Increment token version
await db.users.updateOne(
{ _id: req.userId },
{ $inc: { tokenVersion: 1 } }
);
res.json({ message: 'Logged out successfully' });
} catch (error) {
res.status(500).json({ error: 'Logout failed' });
}
}
// Check blacklist during validation
async function isTokenBlacklisted(token) {
const decoded = jwt.decode(token);
if (!decoded?.jti) {
return false;
}
const blacklisted = await redis.exists(`blacklist:${decoded.jti}`);
return blacklisted === 1;
}5. Complete Lifecycle Flow
- Authentication: User provides credentials, server validates and generates access + refresh tokens
- Storage: Client stores tokens securely (HttpOnly cookies or memory)
- Usage: Client includes access token in Authorization header for each request
- Validation: Server validates token signature, expiration, and claims
- Expiration: When access token expires, client uses refresh token to get new access token
- Refresh: Server validates refresh token and issues new access token (optionally new refresh token)
- Revocation: On logout or security event, tokens are blacklisted or invalidated
- Cleanup: Expired tokens are automatically removed from blacklist
6. Best Practices for Lifecycle Management
- Use short-lived access tokens (15-60 minutes)
- Use longer-lived refresh tokens (7-30 days)
- Implement automatic token refresh before expiration
- Rotate refresh tokens on use (optional but recommended)
- Use jti (JWT ID) for token identification and blacklisting
- Implement token versioning for password changes
- Monitor token usage patterns for anomalies
- Set up alerts for suspicious token activity
- Clean up expired tokens from blacklist regularly
- Log authentication events (without tokens) for auditing