17 KiB
Token Refresh API Implementation
This document describes the complete implementation of the token refresh feature for the MaplePress frontend, integrated with the MaplePress backend API.
Overview
The token refresh feature allows users to obtain new access tokens without requiring them to log in again. This maintains seamless user sessions by automatically refreshing expired tokens in the background.
Backend API Endpoint
Endpoint: POST /api/v1/refresh
Authentication: None required (public endpoint, but requires valid refresh token)
Documentation: /cloud/maplepress-backend/docs/API.md (lines 230-324)
Request Structure
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response Structure
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"user_email": "john@example.com",
"user_name": "John Doe",
"user_role": "user",
"tenant_id": "650e8400-e29b-41d4-a716-446655440000",
"session_id": "750e8400-e29b-41d4-a716-446655440000",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"access_expiry": "2024-10-24T12:30:00Z",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_expiry": "2024-10-31T00:15:00Z",
"refreshed_at": "2024-10-24T12:15:00Z"
}
Key Features
- Token Rotation: Both access and refresh tokens are regenerated on each refresh
- Old Token Invalidation: Previous tokens become invalid immediately
- Session Validation: Validates that the session still exists and is active
- Automatic Expiry: Access tokens expire after 15 minutes, refresh tokens after 7 days
Frontend Implementation
1. RefreshTokenService (src/services/API/RefreshTokenService.js)
Handles direct communication with the backend refresh API.
Key Features:
- Request validation (required refresh token)
- Request body formatting (snake_case for backend)
- Response transformation (camelCase for frontend)
- User-friendly error message mapping
- Helper functions for token expiry checking
Methods:
refreshToken(refreshToken)
Main refresh method that calls the backend API.
import RefreshTokenService from './services/API/RefreshTokenService';
const response = await RefreshTokenService.refreshToken("eyJhbGci...");
Returns:
{
userId: string,
userEmail: string,
userName: string,
userRole: string,
tenantId: string,
sessionId: string,
accessToken: string,
accessExpiry: Date,
refreshToken: string,
refreshExpiry: Date,
refreshedAt: Date
}
shouldRefreshToken(accessExpiry, bufferMinutes = 1)
Checks if token should be refreshed based on expiry time.
const needsRefresh = RefreshTokenService.shouldRefreshToken(
new Date('2024-10-24T12:14:30Z'), // Access expiry
1 // Refresh if expiring within 1 minute
);
isRefreshTokenValid(refreshExpiry)
Checks if the refresh token itself is still valid.
const isValid = RefreshTokenService.isRefreshTokenValid(
new Date('2024-10-31T00:15:00Z')
);
2. AuthManager Enhancement (src/services/Manager/AuthManager.js)
Updated with complete token refresh functionality and automatic refresh logic.
New Features:
- Token refresh on initialization if access token expired
- Prevents duplicate refresh requests with
refreshPromisetracking - Automatic token refresh before API requests
- Handles refresh failures gracefully
New Methods:
async refreshAccessToken()
Refreshes the access token using the stored refresh token.
const authManager = useAuth().authManager;
try {
await authManager.refreshAccessToken();
console.log("Token refreshed successfully");
} catch (error) {
console.error("Refresh failed:", error);
// User will be logged out automatically
}
Features:
- Prevents duplicate refresh requests (returns same promise if refresh in progress)
- Validates refresh token exists and is not expired
- Clears session on refresh failure
- Implements token rotation (stores new tokens)
- Preserves tenant name/slug from previous session
shouldRefreshToken(bufferMinutes = 1)
Checks if the current access token needs refresh.
if (authManager.shouldRefreshToken()) {
await authManager.refreshAccessToken();
}
async ensureValidToken()
Automatically refreshes token if needed. Called before API requests.
// Automatically called by ApiClient before each request
await authManager.ensureValidToken();
3. ApiClient Enhancement (src/services/API/ApiClient.js)
Updated to automatically refresh tokens before making requests and handle 401 errors.
Automatic Token Refresh:
- Calls
ensureValidToken()before every API request - Skips refresh for the
/api/v1/refreshendpoint itself - Can be disabled per-request with
skipTokenRefreshoption
401 Error Handling:
- Automatically attempts token refresh on 401 Unauthorized
- Retries the original request once with new token
- Logs out user if refresh fails
Enhanced Request Flow:
1. Check if token needs refresh (within 1 minute of expiry)
2. If yes, refresh token proactively
3. Make API request with current token
4. If 401 received, force refresh and retry once
5. If retry fails, throw error and clear session
Data Flow
Initial Authentication
User logs in
↓
Backend returns tokens
↓
AuthManager stores tokens
↓
Access Token: valid for 15 minutes
Refresh Token: valid for 7 days
Proactive Token Refresh (Before Expiry)
User makes API request
↓
ApiClient checks token expiry
↓
Token expires in < 1 minute?
↓
Yes: Refresh token proactively
↓
AuthManager.ensureValidToken()
↓
AuthManager.refreshAccessToken()
↓
RefreshTokenService.refreshToken()
↓
POST /api/v1/refresh
↓
Backend validates session
↓
Backend returns new tokens
↓
AuthManager stores new tokens
↓
Continue with original API request
Reactive Token Refresh (After 401 Error)
API request made
↓
Backend returns 401 Unauthorized
↓
ApiClient detects 401
↓
Attempt token refresh
↓
Retry original request
↓
Success: Return response
Failure: Clear session and throw error
Token Refresh on App Initialization
App starts
↓
AuthManager.initialize()
↓
Load tokens from localStorage
↓
Access token expired?
↓
Yes: Check refresh token
↓
Refresh token valid?
↓
Yes: Refresh access token
No: Clear session
Token Lifecycle
Token Expiry Timeline
Login (00:00)
│
├─ Access Token: expires at 00:15 (15 minutes)
│ └─ Proactive refresh at 00:14 (1 minute before)
│
└─ Refresh Token: expires at 7 days later
└─ Requires new login when expired
Session Validation
- Session Duration: 14 days of inactivity (backend setting)
- Access Token: 15 minutes (can be refreshed)
- Refresh Token: 7 days (single-use due to rotation)
If the backend session expires (14 days), token refresh will fail even if the refresh token hasn't expired yet.
Error Handling
Error Types
| Error Condition | Response | Frontend Behavior |
|---|---|---|
| Missing refresh token | 400 Bad Request | Clear session, throw error |
| Invalid refresh token | 401 Unauthorized | Clear session, show "Session expired" |
| Expired refresh token | 401 Unauthorized | Clear session, show "Session expired" |
| Session invalidated | 401 Unauthorized | Clear session, show "Session expired or invalidated" |
| Server error | 500 Internal Server Error | Clear session, show generic error |
Error Messages
The frontend maps backend errors to user-friendly messages:
// Backend: "invalid or expired refresh token"
// Frontend: "Your session has expired. Please log in again to continue."
// Backend: "session not found or expired"
// Frontend: "Session has expired or been invalidated. Please log in again."
// Backend: Generic error
// Frontend: "Unable to refresh your session. Please log in again to continue."
Security Features
- Token Rotation: Each refresh generates new tokens, invalidating old ones
- Session Validation: Refresh validates against active backend session
- Prevents Token Reuse: Old tokens become invalid immediately
- CWE-613 Prevention: Session validation prevents token use after logout
- Proactive Refresh: Reduces window of expired token exposure
- Duplicate Prevention:
refreshPromisetracking prevents race conditions - Automatic Cleanup: Failed refresh automatically clears session
Usage Examples
Manual Token Refresh
import { useAuth } from './services/Services';
function MyComponent() {
const { authManager } = useAuth();
const handleManualRefresh = async () => {
try {
await authManager.refreshAccessToken();
console.log("Token refreshed manually");
} catch (error) {
console.error("Manual refresh failed:", error);
// User will be redirected to login
}
};
return <button onClick={handleManualRefresh}>Refresh Session</button>;
}
Checking Token Status
const { authManager } = useAuth();
// Check if token needs refresh
if (authManager.shouldRefreshToken()) {
console.log("Token expires soon");
}
// Check if refresh token is valid
const refreshExpiry = authManager.refreshExpiry;
if (RefreshTokenService.isRefreshTokenValid(refreshExpiry)) {
console.log("Refresh token is still valid");
}
Automatic Refresh (Built-in)
// Automatic refresh is built into ApiClient
// No manual intervention needed
const { apiClient } = useApi();
// Token will be automatically refreshed if needed
const response = await apiClient.get('/api/v1/some-endpoint');
Testing the Token Refresh Flow
1. Prerequisites
- Backend running at
http://localhost:8000 - Frontend running at
http://localhost:5173 - User already logged in
2. Test Proactive Refresh
// In browser console
// 1. Login first
// (Use the login form)
// 2. Check token expiry
const expiry = localStorage.getItem('maplepress_access_expiry');
console.log('Token expires at:', expiry);
// 3. Manually set expiry to 30 seconds from now (for testing)
const newExpiry = new Date(Date.now() + 30000).toISOString();
localStorage.setItem('maplepress_access_expiry', newExpiry);
// 4. Make an API request (will trigger proactive refresh after 30 seconds)
// The ApiClient will automatically refresh before the request
// 5. Check that new token was stored
setTimeout(() => {
const newToken = localStorage.getItem('maplepress_access_token');
console.log('New token:', newToken);
}, 31000);
3. Test Refresh on Initialization
# 1. Login to the app
# 2. Open browser DevTools → Application → Local Storage
# 3. Find 'maplepress_access_expiry'
# 4. Change the date to yesterday
# 5. Refresh the page
# 6. Check console logs - should see token refresh attempt
4. Test 401 Handling
This requires backend cooperation (backend must return 401):
// In browser console after login
// 1. Make access token invalid
localStorage.setItem('maplepress_access_token', 'invalid_token');
// 2. Make an authenticated API request
// The ApiClient will receive 401, attempt refresh, and retry
// 3. Check console for refresh logs
5. Test Refresh Failure
// In browser console
// 1. Make refresh token invalid
localStorage.setItem('maplepress_refresh_token', 'invalid_refresh');
// 2. Try to refresh
window.maplePressServices.authManager.refreshAccessToken()
.catch(error => console.log('Expected error:', error));
// 3. Check that session was cleared
window.maplePressServices.authManager.isAuthenticated() // Should be false
6. Backend Testing with curl
# 1. Login and get refresh token
curl -X POST http://localhost:8000/api/v1/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "SecurePass123!"
}' | jq -r '.refresh_token'
# 2. Use refresh token to get new tokens
curl -X POST http://localhost:8000/api/v1/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "eyJhbGci..."
}' | jq
# Expected: New access_token and refresh_token
# 3. Try to use old refresh token (should fail)
curl -X POST http://localhost:8000/api/v1/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "eyJhbGci..."
}'
# Expected: 401 Unauthorized
Best Practices
Implemented Best Practices
✅ Proactive Refresh: Tokens are refreshed 1 minute before expiry ✅ Automatic Refresh: Built into ApiClient, no manual intervention needed ✅ Token Rotation: Both tokens regenerated on refresh ✅ Session Validation: Backend validates session on each refresh ✅ Duplicate Prevention: Only one refresh request at a time ✅ Error Handling: Failed refresh clears session and redirects to login ✅ Secure Storage: Tokens stored in localStorage (consider upgrading to httpOnly cookies for production)
Recommended Enhancements
- Move to HTTP-only cookies for token storage (more secure than localStorage)
- Implement refresh token allowlist (backend: store valid refresh tokens)
- Add refresh token fingerprinting (tie token to device)
- Implement sliding sessions (extend session on activity)
- Add token refresh monitoring/metrics
- Implement token refresh failure notifications
Troubleshooting
Token refresh fails immediately after login
Possible causes:
- Clock skew between client and server
- Token expiry dates incorrect
- localStorage corruption
Solution:
- Clear localStorage and login again
- Check browser console for expiry dates
- Verify system clock is correct
Token refresh succeeds but returns 401 on next request
Possible causes:
- Token not being stored correctly
- ApiClient not using updated token
- Race condition between refresh and request
Solution:
- Check localStorage after refresh
- Verify
authManager.getAccessToken()returns new token - Check for duplicate refresh requests in network tab
Session cleared unexpectedly
Possible causes:
- Refresh token expired (after 7 days)
- Backend session invalidated (after 14 days inactivity)
- User logged out on another device
- Backend restarted and cleared sessions
Solution:
- Check
maplepress_refresh_expiryin localStorage - Verify backend session still exists
- Login again to create new session
Infinite refresh loop
Possible causes:
- Token expiry dates set incorrectly
- Backend always returning expired tokens
skipTokenRefreshnot working
Solution:
- Check for
/api/v1/refreshendpoint exclusion in ApiClient - Verify token expiry dates in localStorage
- Check backend logs for refresh endpoint errors
Integration with Dashboard
The dashboard automatically benefits from token refresh:
// Dashboard.jsx
useEffect(() => {
if (!authManager.isAuthenticated()) {
navigate("/login");
return;
}
// Token will be automatically refreshed if needed
const userData = authManager.getUser();
setUser(userData);
}, [authManager, navigate]);
Any API calls made from the dashboard will automatically trigger token refresh if needed.
Future Enhancements
Planned Features
- Background Refresh Timer: Periodic refresh check every 5 minutes
- Session Extension Prompt: Ask user to extend session before refresh token expires
- Multi-tab Synchronization: Coordinate token refresh across browser tabs
- Refresh Analytics: Track refresh success/failure rates
- Grace Period Handling: Handle partial network failures gracefully
Security Improvements
- HTTP-only Cookies: Move from localStorage to secure cookies
- Token Binding: Bind tokens to specific devices/browsers
- Refresh Token Allowlist: Backend maintains list of valid refresh tokens
- Token Rotation Detection: Detect and prevent token replay attacks
- Session Fingerprinting: Additional validation beyond token
Related Files
Created Files
src/services/API/RefreshTokenService.js
Modified Files
src/services/Manager/AuthManager.js
src/services/API/ApiClient.js
Backend Reference Files
cloud/maplepress-backend/docs/API.md (lines 230-324)
cloud/maplepress-backend/internal/interface/http/dto/gateway/refresh_dto.go
cloud/maplepress-backend/internal/interface/http/handler/gateway/refresh_handler.go
Related Documentation
- LOGIN_API.md - Login implementation and token storage
- REGISTRATION_API.md - Registration implementation
- FRONTEND_ARCHITECTURE.md - Complete architecture guide
- README.md - Documentation index
- Backend API Documentation - Complete API reference
Summary
The token refresh implementation provides:
- Seamless Sessions: Users stay logged in without interruption
- Automatic Refresh: No manual intervention required
- Proactive Strategy: Tokens refreshed before expiry
- Reactive Fallback: Handles unexpected 401 errors
- Security: Token rotation prevents reuse
- Reliability: Duplicate prevention and error handling
The implementation follows backend best practices and integrates seamlessly with the existing three-layer service architecture.
Last Updated: October 30, 2024 Frontend Version: 0.0.0 Documentation Version: 1.0.0