20 KiB
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):
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)
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)
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)
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)
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)
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
// 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)
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)
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
- Implement 401 retry logic in API client
- Add background token refresh service
- Create auth error boundary
Phase 2: Mobile Optimizations
- Implement adaptive token warm-up
- Add offline token caching
- Enhance mobile error handling
Phase 3: Integration & Testing
- Integrate all enhancements with existing Auth0Provider
- Test across various network conditions
- Validate mobile and desktop compatibility
Phase 4: Monitoring & Analytics
- Add token performance monitoring
- Implement retry success/failure analytics
- Add offline usage tracking
Success Criteria
Upon completion:
- Robust Token Management: No 401 failures without retry attempts
- Background Refresh: No token expiration issues during extended use
- Mobile Optimization: Adaptive timing and offline support for mobile users
- Error Recovery: Graceful handling of all token acquisition failures
- 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.