9.7 KiB
JWT Refresh Token Implementation Guide
Overview
The JWT authentication system supports both cookie-based and header-based (Bearer token) authentication with comprehensive refresh token functionality and proper logout logic. All authentication methods now use refresh tokens - there is no legacy single-token mode.
Features
- Dual Authentication Methods: Support for both cookie-based and Bearer token authentication
- Universal Refresh Tokens: All logins receive both access and refresh tokens
- Automatic Token Refresh: Tokens are refreshed when 75% of their lifetime has passed
- Logout Functionality: Proper token blacklisting and cleanup
- Security: Short-lived access tokens (30 minutes) and longer-lived refresh tokens (7 days)
Authentication Methods
1. Cookie-Based Authentication
- Access token stored in
auth_tokencookie - Refresh token stored in
refresh_tokencookie - Suitable for web applications with same-origin requests
- Tokens also returned in response body
2. Bearer Token Authentication
- Access token sent in
Authorization: Bearer <token>header - Refresh token sent in
X-Refresh-Tokenheader - Suitable for mobile apps, SPAs, and API integrations
- Tokens returned in response body
API Endpoints
Login
POST /api/user/login
Content-Type: application/json
{
"username": "user@example.com",
"password": "password123"
}
Response (all logins):
{
"user": { ... },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
For cookie-based auth, tokens are also set as httpOnly cookies.
Refresh Token
POST /api/user/refresh-token
For Cookie-based auth:
- Refresh token is read from
refresh_tokencookie - New tokens are set as cookies AND returned in response body
For Bearer token auth:
POST /api/user/refresh-token
X-Refresh-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Response:
{
"success": true,
"message": "Tokens refreshed successfully",
"accessToken": "new_access_token",
"refreshToken": "new_refresh_token"
}
Logout
POST /api/user/logout
Authorization: Bearer <access_token>
Response:
{
"success": true
}
Environment Variables
# JWT Configuration
JWT_SECRET=your-secret-key-for-access-tokens
JWT_REFRESH_SECRET=your-secret-key-for-refresh-tokens
# Access Token Expiry (use one of these)
JWT_ACCESS_TOKEN_EXPIRY=1800 # Access token expiry in seconds (30 minutes)
JWT_ACCESS_TOKEN_EXPIRATION=30m # Access token expiry (supports s, m, h, d)
JWT_EXPIRY=1800 # Legacy: Access token expiry in seconds
JWT_EXPIRATION=30m # Legacy: Access token expiry with duration
# Refresh Token Expiry (use one of these)
JWT_REFRESH_TOKEN_EXPIRY=604800 # Refresh token expiry in seconds (7 days)
JWT_REFRESH_TOKEN_EXPIRATION=7d # Refresh token expiry (supports s, m, h, d)
JWT_REFRESH_EXPIRATION=7d # Legacy: Refresh token expiry with duration
# Cookie Names (optional)
JWT_COOKIE_NAME=auth_token # Access token cookie name (default: auth_token)
JWT_REFRESH_COOKIE_NAME=refresh_token # Refresh token cookie name (default: refresh_token)
Environment Variable Priority
Access Token Expiry (checked in order):
JWT_ACCESS_TOKEN_EXPIRY(seconds)JWT_ACCESS_TOKEN_EXPIRATION(duration string)JWT_EXPIRY(seconds) - legacyJWT_EXPIRATION(duration string) - legacy- Default: 1800 seconds (30 minutes)
Refresh Token Expiry (checked in order):
JWT_REFRESH_TOKEN_EXPIRY(seconds)JWT_REFRESH_TOKEN_EXPIRATION(duration string)JWT_REFRESH_EXPIRATION(duration string) - legacy- Default: 604800 seconds (7 days)
Duration String Format
Supports: s (seconds), m (minutes), h (hours), d (days)
Examples: 30s, 15m, 2h, 7d
Token Structure
Access Token Payload
{
"userId": "user-uuid",
"authLevel": 0,
"userStatus": 1,
"orgId": "org-uuid",
"type": "access",
"iat": 1640995200,
"exp": 1640997000
}
Refresh Token Payload
{
"userId": "user-uuid",
"orgId": "org-uuid",
"type": "refresh",
"iat": 1640995200,
"exp": 1641600000
}
Automatic Token Refresh
The system automatically refreshes tokens when:
- Token is within 25% of its expiration time (75% of lifetime has passed)
- Valid refresh token is available
- User makes an authenticated request
✅ Automatic refresh happens on every authenticated API call - no manual intervention needed!
Response Headers
For Bearer token authentication, refresh responses include:
X-New-Access-Token: New access tokenX-New-Refresh-Token: New refresh tokenX-Token-Refreshed: "true" indicator
Manual Refresh (Optional)
While automatic refresh handles most scenarios, manual refresh is available for:
- Proactive refresh: Before critical operations
- Background apps: Long-running applications that need fresh tokens
- Offline recovery: When app reconnects after being offline
POST /api/user/refresh-token
X-Refresh-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Client Implementation Examples
JavaScript/TypeScript (Fetch API)
class ApiClient {
private accessToken: string = '';
private refreshToken: string = '';
async login(username: string, password: string) {
const response = await fetch('/api/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
this.accessToken = data.token;
this.refreshToken = data.refreshToken; // Always present now
return data;
}
async makeAuthenticatedRequest(url: string, options: RequestInit = {}) {
const headers = {
'Authorization': `Bearer ${this.accessToken}`,
...options.headers
};
let response = await fetch(url, { ...options, headers });
// Automatically handle token refresh (tokens updated in response headers)
if (response.headers.get('X-Token-Refreshed') === 'true') {
const newAccessToken = response.headers.get('X-New-Access-Token');
const newRefreshToken = response.headers.get('X-New-Refresh-Token');
if (newAccessToken) this.accessToken = newAccessToken;
if (newRefreshToken) this.refreshToken = newRefreshToken;
}
return response;
}
// Optional: Manual refresh (usually not needed due to automatic refresh)
async refreshTokens() {
const response = await fetch('/api/user/refresh-token', {
method: 'POST',
headers: {
'X-Refresh-Token': this.refreshToken
}
});
if (response.ok) {
const data = await response.json();
this.accessToken = data.accessToken;
this.refreshToken = data.refreshToken;
return true;
}
return false;
}
async logout() {
await fetch('/api/user/logout', {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.accessToken}` }
});
this.accessToken = '';
this.refreshToken = '';
}
}
React Hook Example
import { useState, useCallback } from 'react';
export const useAuth = () => {
const [accessToken, setAccessToken] = useState<string>('');
const [refreshToken, setRefreshToken] = useState<string>('');
const login = useCallback(async (username: string, password: string) => {
const response = await fetch('/api/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
setAccessToken(data.token);
setRefreshToken(data.refreshToken); // Always present
return data;
}, []);
const logout = useCallback(async () => {
if (accessToken) {
await fetch('/api/user/logout', {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}` }
});
}
setAccessToken('');
setRefreshToken('');
}, [accessToken]);
return { accessToken, refreshToken, login, logout };
};
Security Considerations
- Token Blacklisting: Logout tokens are blacklisted in Redis with TTL matching token expiration
- Short-lived Access Tokens: 30-minute expiry reduces exposure window
- Secure Cookies: httpOnly, secure, sameSite attributes for cookie-based auth
- Token Rotation: Refresh tokens are rotated on each refresh
- Environment-specific Secrets: Different secrets for access and refresh tokens
Migration Guide
From Single Token to Refresh Token System
Since this is a new implementation, all clients should expect:
- Login Response: Always includes both
token(access) andrefreshToken - Token Storage: Store both tokens securely
- API Requests: Use access token in Authorization header
- Automatic Refresh: Tokens refresh automatically - just watch for response headers
- Logout: Call logout endpoint to invalidate tokens
Key Point: Manual refresh is optional since automatic refresh handles token renewal seamlessly.
No backward compatibility needed - this is the only authentication method.
Testing
# Login and get tokens
curl -X POST http://localhost:3000/api/user/login \
-H "Content-Type: application/json" \
-d '{"username": "test@example.com", "password": "password"}'
# Use access token
curl -X GET http://localhost:3000/api/user/profile \
-H "Authorization: Bearer <access_token>"
# Refresh tokens
curl -X POST http://localhost:3000/api/user/refresh-token \
-H "X-Refresh-Token: <refresh_token>"
# Logout
curl -X POST http://localhost:3000/api/user/logout \
-H "Authorization: Bearer <access_token>"