9.6 KiB
9.6 KiB
Token Refresh System - Quick Reference
Last Updated: 2025-01-27
Component: Web UI Authentication
Overview
The web UI implements automatic and proactive JWT token refresh to provide seamless authentication for active users.
Architecture
┌─────────────────────────────────────────────────────┐
│ User Activity → API Request │
│ ↓ │
│ Axios Interceptor (adds JWT) │
│ ↓ │
│ Server Response │
│ ├─ 200 OK → Continue │
│ ├─ 401 Unauthorized → Auto-refresh & retry │
│ └─ 403 Forbidden → Show permission error │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ Background: Token Monitor (every 60s) │
│ ↓ │
│ Token expires in < 5 min? │
│ ├─ Yes → Proactive refresh │
│ └─ No → Continue monitoring │
└─────────────────────────────────────────────────────┘
Key Components
1. API Wrapper (web/src/lib/api-wrapper.ts)
- Purpose: Configure axios with token refresh interceptors
- Features:
- Global axios defaults configuration
- Request interceptor (adds token)
- Response interceptor (handles 401/403)
- Proactive refresh monitor
2. ErrorDisplay Component (web/src/components/common/ErrorDisplay.tsx)
- Purpose: User-friendly error messages
- Distinguishes:
- 401: "Session expired" (handled automatically)
- 403: "Access denied - insufficient permissions"
- Other: Generic error with details
3. Auth Context (web/src/contexts/AuthContext.tsx)
- Purpose: Manage authentication state
- Lifecycle:
userset → Start token refresh monitorusercleared → Stop token refresh monitor
Token Lifecycle
Access Token
- Duration: 1 hour (configured on backend)
- Storage:
localStorage.getItem('access_token') - Refresh Trigger: Automatic on 401 response
- Proactive Refresh: 5 minutes before expiration
Refresh Token
- Duration: 7 days (configured on backend)
- Storage:
localStorage.getItem('refresh_token') - Used: To obtain new access token
- Rotation: Optional (backend can return new refresh token)
Configuration
Proactive Refresh Settings
// File: web/src/lib/api-wrapper.ts
// Check every 60 seconds
const MONITOR_INTERVAL = 60000; // ms
// Refresh if expiring within 5 minutes
const REFRESH_THRESHOLD = 300; // seconds
API Endpoint
// Refresh endpoint
POST /auth/refresh
Content-Type: application/json
{
"refresh_token": "..."
}
// Response
{
"data": {
"access_token": "...",
"refresh_token": "..." // Optional - for rotation
}
}
Error Handling
401 Unauthorized (Token Expired/Invalid)
// Automatic handling:
1. Interceptor detects 401
2. Attempts token refresh with refresh_token
3. On success: Retry original request
4. On failure: Clear tokens, redirect to /login
403 Forbidden (Insufficient Permissions)
// Manual handling in components:
<ErrorDisplay error={error} />
// Shows: "Access Denied - You do not have permission..."
Network/Server Errors
// Generic error display:
<ErrorDisplay
error={error}
showRetry={true}
onRetry={() => refetch()}
/>
Usage in Components
Detecting Error Types
// In React components using TanStack Query
const { data, error, isLoading } = useActions();
if (error) {
// ErrorDisplay component handles type detection
return <ErrorDisplay error={error} />;
}
Custom Error Handling
// Check for 403 errors
const is403 = error?.response?.status === 403 ||
error?.isAuthorizationError;
if (is403) {
// Show permission-specific UI
}
// Check for 401 errors (rare - usually handled by interceptor)
const is401 = error?.response?.status === 401;
Debugging
Console Logs
# Initialization
🔧 Initializing API wrapper
✓ Axios defaults configured with interceptors
✓ API wrapper initialized
# Token Refresh
🔄 Access token expired, attempting refresh...
✓ Token refreshed successfully
# Monitor
🔄 Starting token refresh monitor
✓ Token proactively refreshed
⏹️ Stopping token refresh monitor
# Errors
⚠️ No refresh token available, redirecting to login
Token refresh failed, clearing session and redirecting to login
Access forbidden - insufficient permissions for this resource
Browser DevTools
# Check tokens
Application → Local Storage → localhost
- access_token: "eyJ..."
- refresh_token: "eyJ..."
# Watch refresh requests
Network → Filter: refresh
- POST /auth/refresh
- Status: 200 OK
- Response: { data: { access_token, refresh_token } }
# Monitor console
Console → Filter: Token|refresh|Unauthorized
Common Scenarios
Scenario 1: Active User
User logged in → Using app normally
↓
Every 60s: Monitor checks token expiration
↓
Token expires in 4 minutes
↓
Proactive refresh triggered
↓
User continues seamlessly (no interruption)
Scenario 2: Idle User Returns
User logged in → Leaves tab idle for 70 minutes
↓
Access token expired (after 60 min)
↓
User returns, clicks action
↓
API returns 401
↓
Interceptor attempts refresh
↓
If refresh token valid: Success, retry request
If refresh token expired: Redirect to login
Scenario 3: Permission Denied
User logged in → Tries restricted action
↓
API returns 403 Forbidden
↓
ErrorDisplay shows: "Access Denied"
↓
User sees clear message (not "Unauthorized")
Scenario 4: Network Failure During Refresh
User action → 401 response → Refresh attempt
↓
Network error / API down
↓
Refresh fails → Tokens cleared
↓
Redirect to login
↓
SessionStorage saves current path
↓
After login → Redirect back to original page
Testing
Manual Test: Token Expiration
# 1. Log in to web UI
# 2. Open DevTools → Application → Local Storage
# 3. Copy access_token value
# 4. Decode at jwt.io - note expiration time
# 5. Wait until near expiration
# 6. Perform action (view page, click button)
# 7. Watch Network tab for /auth/refresh call
# 8. Verify action completes successfully
Manual Test: Permission Denied
# 1. Log in as limited user
# 2. Try to access admin-only resource
# 3. Verify: See "Access Denied" (not "Unauthorized")
# 4. Verify: Amber/yellow UI (not red)
# 5. Verify: Helpful message about permissions
Manual Test: Proactive Refresh
# 1. Log in
# 2. Open Console
# 3. Look for "🔄 Starting token refresh monitor"
# 4. Wait 60 seconds
# 5. If token expires within 5 min, see:
# "✓ Token proactively refreshed"
# 6. Logout
# 7. See: "⏹️ Stopping token refresh monitor"
Troubleshooting
Issue: Redirect loop to /login
Cause: Both access_token and refresh_token expired
Solution: Expected behavior - user must log in again
Issue: Token not refreshing automatically
Check:
- Axios interceptors configured? → See console for init logs
- Token exists in localStorage?
- Refresh token valid?
- Network connectivity?
- Backend /auth/refresh endpoint working?
Issue: Monitor not running
Check:
- User authenticated? → Monitor only runs when
useris set - Check console for "Starting token refresh monitor"
- Verify AuthContext lifecycle in React DevTools
Issue: Wrong error message (401 vs 403)
Check:
- Using ErrorDisplay component?
- Error object has
response.statusproperty? - Interceptor properly marking 403 errors?
Security Notes
-
Token Storage: Currently uses localStorage
- ✅ Works across tabs
- ⚠️ Vulnerable to XSS
- 🔒 Consider httpOnly cookies for production
-
Token Exposure: Tokens only in Authorization header
- ✅ Never in URL parameters
- ✅ Not logged to console
-
Automatic Cleanup: Failed refresh clears all tokens
- ✅ No stale authentication state
-
Single Sign-Out: Clearing tokens stops all access
- ✅ Immediate effect
API Requirements
The backend must provide:
- Login Endpoint: Returns access_token + refresh_token
- Refresh Endpoint: Accepts refresh_token, returns new access_token
- Token Format: Standard JWT with
expclaim - Error Codes:
- 401 for expired/invalid tokens
- 403 for permission denied
Related Files
web/src/lib/api-wrapper.ts- Core token refresh logicweb/src/lib/api-client.ts- Axios instance configurationweb/src/components/common/ErrorDisplay.tsx- Error UIweb/src/contexts/AuthContext.tsx- Auth state managementweb/src/pages/auth/LoginPage.tsx- Login with redirect
Related Documentation
- Full details:
work-summary/2025-01-token-refresh-improvements.md - Authentication:
docs/authentication/authentication.md - Token rotation:
docs/authentication/token-rotation.md