re-uploading work
This commit is contained in:
356
docs/authentication/token-refresh-quickref.md
Normal file
356
docs/authentication/token-refresh-quickref.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# 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
|
||||
```typescript
|
||||
// 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
|
||||
```typescript
|
||||
// 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)
|
||||
```typescript
|
||||
// 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)
|
||||
```typescript
|
||||
// Manual handling in components:
|
||||
<ErrorDisplay error={error} />
|
||||
// Shows: "Access Denied - You do not have permission..."
|
||||
```
|
||||
|
||||
### Network/Server Errors
|
||||
```typescript
|
||||
// Generic error display:
|
||||
<ErrorDisplay
|
||||
error={error}
|
||||
showRetry={true}
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
```
|
||||
|
||||
## Usage in Components
|
||||
|
||||
### Detecting Error Types
|
||||
```typescript
|
||||
// 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
|
||||
```typescript
|
||||
// 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
|
||||
```bash
|
||||
# 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
|
||||
```bash
|
||||
# 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
|
||||
```bash
|
||||
# 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
|
||||
```bash
|
||||
# 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
|
||||
```bash
|
||||
# 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
|
||||
|
||||
## Related Files
|
||||
|
||||
- `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
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Full details: `work-summary/2025-01-token-refresh-improvements.md`
|
||||
- Authentication: `docs/authentication/authentication.md`
|
||||
- Token rotation: `docs/authentication/token-rotation.md`
|
||||
Reference in New Issue
Block a user