Files
attune/docs/authentication/token-refresh-quickref.md
2026-02-04 17:46:30 -06:00

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:
    • user set → Start token refresh monitor
    • user cleared → 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:

  1. Axios interceptors configured? → See console for init logs
  2. Token exists in localStorage?
  3. Refresh token valid?
  4. Network connectivity?
  5. Backend /auth/refresh endpoint working?

Issue: Monitor not running

Check:

  1. User authenticated? → Monitor only runs when user is set
  2. Check console for "Starting token refresh monitor"
  3. Verify AuthContext lifecycle in React DevTools

Issue: Wrong error message (401 vs 403)

Check:

  1. Using ErrorDisplay component?
  2. Error object has response.status property?
  3. Interceptor properly marking 403 errors?

Security Notes

  1. Token Storage: Currently uses localStorage

    • Works across tabs
    • ⚠️ Vulnerable to XSS
    • 🔒 Consider httpOnly cookies for production
  2. Token Exposure: Tokens only in Authorization header

    • Never in URL parameters
    • Not logged to console
  3. Automatic Cleanup: Failed refresh clears all tokens

    • No stale authentication state
  4. Single Sign-Out: Clearing tokens stops all access

    • Immediate effect

API Requirements

The backend must provide:

  1. Login Endpoint: Returns access_token + refresh_token
  2. Refresh Endpoint: Accepts refresh_token, returns new access_token
  3. Token Format: Standard JWT with exp claim
  4. Error Codes:
    • 401 for expired/invalid tokens
    • 403 for permission denied
  • web/src/lib/api-wrapper.ts - Core token refresh logic
  • web/src/lib/api-client.ts - Axios instance configuration
  • web/src/components/common/ErrorDisplay.tsx - Error UI
  • web/src/contexts/AuthContext.tsx - Auth state management
  • web/src/pages/auth/LoginPage.tsx - Login with redirect
  • Full details: work-summary/2025-01-token-refresh-improvements.md
  • Authentication: docs/authentication/authentication.md
  • Token rotation: docs/authentication/token-rotation.md