# 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 ```json { "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` ### Response Structure ```json { "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. ```javascript import RefreshTokenService from './services/API/RefreshTokenService'; const response = await RefreshTokenService.refreshToken("eyJhbGci..."); ``` **Returns:** ```javascript { 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. ```javascript 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. ```javascript 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 `refreshPromise` tracking - Automatic token refresh before API requests - Handles refresh failures gracefully **New Methods:** #### `async refreshAccessToken()` Refreshes the access token using the stored refresh token. ```javascript 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. ```javascript if (authManager.shouldRefreshToken()) { await authManager.refreshAccessToken(); } ``` #### `async ensureValidToken()` Automatically refreshes token if needed. Called before API requests. ```javascript // 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/refresh` endpoint itself - Can be disabled per-request with `skipTokenRefresh` option **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:** ```javascript 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: ```javascript // 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 1. **Token Rotation**: Each refresh generates new tokens, invalidating old ones 2. **Session Validation**: Refresh validates against active backend session 3. **Prevents Token Reuse**: Old tokens become invalid immediately 4. **CWE-613 Prevention**: Session validation prevents token use after logout 5. **Proactive Refresh**: Reduces window of expired token exposure 6. **Duplicate Prevention**: `refreshPromise` tracking prevents race conditions 7. **Automatic Cleanup**: Failed refresh automatically clears session ## Usage Examples ### Manual Token Refresh ```javascript 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 ; } ``` ### Checking Token Status ```javascript 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) ```javascript // 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 ```javascript // 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 ```bash # 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): ```javascript // 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 ```javascript // 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 ```bash # 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:** 1. Clock skew between client and server 2. Token expiry dates incorrect 3. 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:** 1. Token not being stored correctly 2. ApiClient not using updated token 3. 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:** 1. Refresh token expired (after 7 days) 2. Backend session invalidated (after 14 days inactivity) 3. User logged out on another device 4. Backend restarted and cleared sessions **Solution:** - Check `maplepress_refresh_expiry` in localStorage - Verify backend session still exists - Login again to create new session ### Infinite refresh loop **Possible causes:** 1. Token expiry dates set incorrectly 2. Backend always returning expired tokens 3. `skipTokenRefresh` not working **Solution:** - Check for `/api/v1/refresh` endpoint 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: ```javascript // 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_API.md) - Login implementation and token storage - [REGISTRATION_API.md](./REGISTRATION_API.md) - Registration implementation - [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Complete architecture guide - [README.md](./README.md) - Documentation index - [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md) - Complete API reference ## Summary The token refresh implementation provides: 1. **Seamless Sessions**: Users stay logged in without interruption 2. **Automatic Refresh**: No manual intervention required 3. **Proactive Strategy**: Tokens refreshed before expiry 4. **Reactive Fallback**: Handles unexpected 401 errors 5. **Security**: Token rotation prevents reuse 6. **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