Files
motovaultpro/docs/changes/mobile-optimization-v1/05-TOKEN-OPTIMIZATION.md
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

709 lines
20 KiB
Markdown

# Token Optimization & Authentication Enhancement Guide
## Overview
This document provides detailed guidance for optimizing Auth0 token management, enhancing error recovery, and implementing robust authentication patterns for improved mobile and desktop experience.
## Current Implementation Analysis
### Existing Token Management Strengths
**File**: `/home/egullickson/motovaultpro/frontend/src/core/auth/Auth0Provider.tsx`
**Current Features**:
- Progressive fallback strategy with 3 retry attempts
- Mobile-optimized token acquisition with enhanced timeouts
- Exponential backoff for mobile network conditions
- Pre-warming token cache for mobile devices
- Sophisticated error handling and logging
**Current Token Acquisition Logic** (lines 44-95):
```typescript
const getTokenWithRetry = async (): Promise<string | null> => {
const maxRetries = 3;
const baseDelay = 500;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
let token: string;
if (attempt === 1) {
// Cache-first approach
token = await getAccessTokenSilently({
cacheMode: 'on',
timeoutInSeconds: 15,
});
} else if (attempt === 2) {
// Force refresh
token = await getAccessTokenSilently({
cacheMode: 'off',
timeoutInSeconds: 20,
});
} else {
// Final attempt with extended timeout
token = await getAccessTokenSilently({
timeoutInSeconds: 30,
});
}
return token;
} catch (error) {
const delay = baseDelay * Math.pow(2, attempt - 1);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
return null;
};
```
## Enhancement Areas
### 1. Token Refresh Retry Logic for 401 Responses
**Problem**: API calls fail with 401 responses without attempting token refresh
**Solution**: Implement automatic token refresh and retry for 401 errors
#### Enhanced API Client
**File**: `frontend/src/core/api/client.ts` (modifications)
```typescript
import { Auth0Context } from '@auth0/auth0-react';
import { useContext } from 'react';
// Enhanced token management service
class TokenManager {
private static instance: TokenManager;
private isRefreshing = false;
private failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: Error) => void;
}> = [];
static getInstance(): TokenManager {
if (!TokenManager.instance) {
TokenManager.instance = new TokenManager();
}
return TokenManager.instance;
}
async refreshToken(getAccessTokenSilently: any): Promise<string> {
if (this.isRefreshing) {
// Return a promise that will resolve when the current refresh completes
return new Promise((resolve, reject) => {
this.failedQueue.push({ resolve, reject });
});
}
this.isRefreshing = true;
try {
// Force token refresh
const token = await getAccessTokenSilently({
cacheMode: 'off',
timeoutInSeconds: 20,
});
// Process queued requests
this.failedQueue.forEach(({ resolve }) => resolve(token));
this.failedQueue = [];
return token;
} catch (error) {
// Reject queued requests
this.failedQueue.forEach(({ reject }) => reject(error as Error));
this.failedQueue = [];
throw error;
} finally {
this.isRefreshing = false;
}
}
}
// Enhanced API client with 401 retry logic
export const createApiClient = (getAccessTokenSilently: any) => {
const tokenManager = TokenManager.getInstance();
const client = axios.create({
baseURL: process.env.REACT_APP_API_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor - inject tokens
client.interceptors.request.use(
async (config) => {
try {
const token = await getAccessTokenSilently({
cacheMode: 'on',
timeoutInSeconds: 15,
});
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
} catch (error) {
console.warn('Token acquisition failed, proceeding without token:', error);
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle 401s with token refresh retry
client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Handle 401 responses with token refresh
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
console.log('401 detected, attempting token refresh...');
const newToken = await tokenManager.refreshToken(getAccessTokenSilently);
// Update the failed request with new token
originalRequest.headers.Authorization = `Bearer ${newToken}`;
// Retry the original request
return client(originalRequest);
} catch (refreshError) {
console.error('Token refresh failed:', refreshError);
// If token refresh fails, the user needs to re-authenticate
// This should trigger the Auth0 login flow
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// Enhanced mobile error handling
if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
if (isMobile) {
error.message = 'Connection timeout. Please check your network and try again.';
}
}
return Promise.reject(error);
}
);
return client;
};
```
### 2. Background Token Refresh
**Problem**: Tokens can expire during extended mobile use
**Solution**: Implement proactive background token refresh
#### Background Token Service
**File**: `frontend/src/core/auth/backgroundTokenService.ts` (new)
```typescript
class BackgroundTokenService {
private static instance: BackgroundTokenService;
private refreshInterval: NodeJS.Timeout | null = null;
private getAccessTokenSilently: any = null;
private isActive = false;
static getInstance(): BackgroundTokenService {
if (!BackgroundTokenService.instance) {
BackgroundTokenService.instance = new BackgroundTokenService();
}
return BackgroundTokenService.instance;
}
start(getAccessTokenSilently: any) {
if (this.isActive) return;
this.getAccessTokenSilently = getAccessTokenSilently;
this.isActive = true;
// Refresh token every 45 minutes (tokens typically expire after 1 hour)
this.refreshInterval = setInterval(() => {
this.refreshTokenInBackground();
}, 45 * 60 * 1000);
// Also refresh on app visibility change (mobile app switching)
document.addEventListener('visibilitychange', this.handleVisibilityChange);
}
stop() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
this.isActive = false;
}
private handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// App became visible, refresh token to ensure it's valid
this.refreshTokenInBackground();
}
};
private async refreshTokenInBackground() {
if (!this.getAccessTokenSilently) return;
try {
await this.getAccessTokenSilently({
cacheMode: 'off',
timeoutInSeconds: 10,
});
console.debug('Background token refresh successful');
} catch (error) {
console.warn('Background token refresh failed:', error);
// Don't throw - this is a background operation
}
}
}
export default BackgroundTokenService;
```
#### Integration with Auth0Provider
**File**: `/home/egullickson/motovaultpro/frontend/src/core/auth/Auth0Provider.tsx` (modifications)
```typescript
import BackgroundTokenService from './backgroundTokenService';
// Inside the Auth0Provider component
const CustomAuth0Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
const initializeAuth = async () => {
// Existing initialization logic...
// Start background token service after authentication
if (isAuthenticated) {
const backgroundService = BackgroundTokenService.getInstance();
backgroundService.start(getAccessTokenSilently);
}
};
initializeAuth();
// Cleanup on unmount
return () => {
const backgroundService = BackgroundTokenService.getInstance();
backgroundService.stop();
};
}, [isAuthenticated, getAccessTokenSilently]);
// Rest of component...
};
```
### 3. Enhanced Error Boundaries for Token Failures
**Problem**: Token acquisition failures can break the app
**Solution**: Implement error boundaries with graceful degradation
#### Auth Error Boundary
**File**: `frontend/src/core/auth/AuthErrorBoundary.tsx` (new)
```typescript
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
isAuthError: boolean;
}
export class AuthErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
isAuthError: false,
};
public static getDerivedStateFromError(error: Error): State {
const isAuthError = error.message.includes('auth') ||
error.message.includes('token') ||
error.message.includes('login');
return {
hasError: true,
error,
isAuthError
};
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Auth Error Boundary caught an error:', error, errorInfo);
}
private handleRetry = () => {
this.setState({ hasError: false, error: null, isAuthError: false });
};
private handleReauth = () => {
// Redirect to login
window.location.href = '/login';
};
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
<div className="mb-4">
<svg
className="mx-auto h-12 w-12 text-red-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<h2 className="text-lg font-semibold text-gray-900 mb-2">
{this.state.isAuthError ? 'Authentication Error' : 'Something went wrong'}
</h2>
<p className="text-gray-600 mb-6">
{this.state.isAuthError
? 'There was a problem with authentication. Please sign in again.'
: 'An unexpected error occurred. Please try again.'}
</p>
<div className="flex space-x-3">
<button
onClick={this.handleRetry}
className="flex-1 bg-gray-200 text-gray-700 py-2 px-4 rounded-lg font-medium hover:bg-gray-300 transition-colors"
>
Try Again
</button>
{this.state.isAuthError && (
<button
onClick={this.handleReauth}
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Sign In
</button>
)}
</div>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="mt-4 text-left">
<summary className="text-sm text-gray-500 cursor-pointer">
Error Details (dev only)
</summary>
<pre className="mt-2 text-xs text-red-600 bg-red-50 p-2 rounded overflow-auto">
{this.state.error.message}
</pre>
</details>
)}
</div>
</div>
);
}
return this.props.children;
}
}
```
### 4. Optimized Mobile Token Warm-up
**Problem**: Current 100ms delay may not be sufficient for all mobile devices
**Solution**: Adaptive warm-up timing based on device performance
#### Adaptive Token Warm-up
**File**: `frontend/src/core/auth/tokenWarmup.ts` (new)
```typescript
class TokenWarmupService {
private static instance: TokenWarmupService;
private warmupDelay: number = 100; // Default
static getInstance(): TokenWarmupService {
if (!TokenWarmupService.instance) {
TokenWarmupService.instance = new TokenWarmupService();
}
return TokenWarmupService.instance;
}
async calculateOptimalDelay(): Promise<number> {
// Detect device performance characteristics
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
if (!isMobile) {
return 50; // Faster for desktop
}
// Mobile performance detection
const startTime = performance.now();
// Simple CPU-bound task to gauge performance
let sum = 0;
for (let i = 0; i < 100000; i++) {
sum += Math.random();
}
const endTime = performance.now();
const executionTime = endTime - startTime;
// Adaptive delay based on device performance
if (executionTime < 10) {
return 100; // Fast mobile device
} else if (executionTime < 50) {
return 200; // Medium mobile device
} else {
return 500; // Slower mobile device
}
}
async warmupWithAdaptiveDelay(callback: () => Promise<void>): Promise<void> {
const delay = await this.calculateOptimalDelay();
this.warmupDelay = delay;
return new Promise((resolve) => {
setTimeout(async () => {
await callback();
resolve();
}, delay);
});
}
getLastWarmupDelay(): number {
return this.warmupDelay;
}
}
export default TokenWarmupService;
```
#### Integration with Auth0Provider
```typescript
// Inside Auth0Provider initialization
const warmupService = TokenWarmupService.getInstance();
await warmupService.warmupWithAdaptiveDelay(async () => {
try {
await getAccessTokenSilently({
cacheMode: 'on',
timeoutInSeconds: 5,
});
} catch (error) {
// Warm-up failed, but continue initialization
console.warn('Token warm-up failed:', error);
}
});
```
### 5. Offline Token Management
**Problem**: Mobile users may have intermittent connectivity
**Solution**: Implement offline token caching and validation
#### Offline Token Cache
**File**: `frontend/src/core/auth/offlineTokenCache.ts` (new)
```typescript
interface CachedTokenInfo {
token: string;
expiresAt: number;
cachedAt: number;
}
class OfflineTokenCache {
private static instance: OfflineTokenCache;
private readonly CACHE_KEY = 'motovaultpro-offline-token';
private readonly MAX_OFFLINE_DURATION = 30 * 60 * 1000; // 30 minutes
static getInstance(): OfflineTokenCache {
if (!OfflineTokenCache.instance) {
OfflineTokenCache.instance = new OfflineTokenCache();
}
return OfflineTokenCache.instance;
}
cacheToken(token: string): void {
try {
// Decode JWT to get expiration (simplified - in production, use a JWT library)
const payload = JSON.parse(atob(token.split('.')[1]));
const expiresAt = payload.exp * 1000; // Convert to milliseconds
const tokenInfo: CachedTokenInfo = {
token,
expiresAt,
cachedAt: Date.now(),
};
localStorage.setItem(this.CACHE_KEY, JSON.stringify(tokenInfo));
} catch (error) {
console.warn('Failed to cache token:', error);
}
}
getCachedToken(): string | null {
try {
const cached = localStorage.getItem(this.CACHE_KEY);
if (!cached) return null;
const tokenInfo: CachedTokenInfo = JSON.parse(cached);
const now = Date.now();
// Check if token is expired
if (now >= tokenInfo.expiresAt) {
this.clearCache();
return null;
}
// Check if we've been offline too long
if (now - tokenInfo.cachedAt > this.MAX_OFFLINE_DURATION) {
this.clearCache();
return null;
}
return tokenInfo.token;
} catch (error) {
console.warn('Failed to retrieve cached token:', error);
this.clearCache();
return null;
}
}
clearCache(): void {
localStorage.removeItem(this.CACHE_KEY);
}
isOnline(): boolean {
return navigator.onLine;
}
}
export default OfflineTokenCache;
```
## Implementation Integration
### Updated API Client Factory
**File**: `frontend/src/core/api/index.ts` (new)
```typescript
import { createApiClient } from './client';
import OfflineTokenCache from '../auth/offlineTokenCache';
export const createEnhancedApiClient = (getAccessTokenSilently: any) => {
const offlineCache = OfflineTokenCache.getInstance();
const client = createApiClient(getAccessTokenSilently);
// Enhance request interceptor for offline support
client.interceptors.request.use(
async (config) => {
try {
// Try to get fresh token
const token = await getAccessTokenSilently({
cacheMode: 'on',
timeoutInSeconds: 15,
});
if (token) {
// Cache token for offline use
offlineCache.cacheToken(token);
config.headers.Authorization = `Bearer ${token}`;
}
} catch (error) {
// If online token acquisition fails, try cached token
if (!offlineCache.isOnline()) {
const cachedToken = offlineCache.getCachedToken();
if (cachedToken) {
config.headers.Authorization = `Bearer ${cachedToken}`;
console.log('Using cached token for offline request');
}
}
}
return config;
},
(error) => Promise.reject(error)
);
return client;
};
```
## Testing Requirements
### Token Management Tests
- ✅ 401 responses trigger automatic token refresh and retry
- ✅ Background token refresh prevents expiration during extended use
- ✅ Token warm-up adapts to device performance
- ✅ Error boundaries handle token failures gracefully
- ✅ Offline token caching works during network interruptions
### Mobile-Specific Tests
- ✅ Enhanced retry logic handles poor mobile connectivity
- ✅ App visibility changes trigger token refresh
- ✅ Mobile error messages are user-friendly
- ✅ Token acquisition timing adapts to device performance
### Integration Tests
- ✅ Enhanced API client works with existing components
- ✅ Background service doesn't interfere with normal token acquisition
- ✅ Error boundaries don't break existing error handling
- ✅ Offline caching doesn't conflict with Auth0's built-in caching
## Implementation Phases
### Phase 1: Core Enhancements
1. Implement 401 retry logic in API client
2. Add background token refresh service
3. Create auth error boundary
### Phase 2: Mobile Optimizations
1. Implement adaptive token warm-up
2. Add offline token caching
3. Enhance mobile error handling
### Phase 3: Integration & Testing
1. Integrate all enhancements with existing Auth0Provider
2. Test across various network conditions
3. Validate mobile and desktop compatibility
### Phase 4: Monitoring & Analytics
1. Add token performance monitoring
2. Implement retry success/failure analytics
3. Add offline usage tracking
## Success Criteria
Upon completion:
1. **Robust Token Management**: No 401 failures without retry attempts
2. **Background Refresh**: No token expiration issues during extended use
3. **Mobile Optimization**: Adaptive timing and offline support for mobile users
4. **Error Recovery**: Graceful handling of all token acquisition failures
5. **Performance**: Minimal impact on app performance and user experience
These enhancements will provide a robust, mobile-optimized authentication system that gracefully handles network issues and provides an excellent user experience across all platforms.