monorepo/web/maplepress-frontend/docs/API/REFRESH_TOKEN_API.md

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 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.

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/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:

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

  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

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)

  • 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:

// 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

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

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