This document presents a production-ready low-level design for managing JSON Web Tokens in frontend applications, covering token storage strategies, refresh mechanisms, and security considerations for enterprise-scale web platforms.
1. Problem Statement & Requirements
Clarifying questions
- What authentication method is being used (OAuth, SAML, custom JWT, etc.)?
- Are we building a full authentication system or just token management utilities for an existing auth flow?
- What's the target browser support matrix (ES6+ browsers, IE11, mobile-only, etc.)?
- Do we need to support refresh token rotation or just access token expiration handling?
- Should the system handle multi-tab/window token synchronization (e.g., when a token expires in one tab, others should invalidate)?
- Are there specific security requirements for token storage (e.g., HTTPOnly cookies vs localStorage)?
- Do we need to support internationalization of error messages or token-related UI?
Functional requirements
- Token storage and retrieval (access token, refresh token)
- Automatic token refresh when access token expires
- Token validation and expiration handling
- Secure storage of tokens (localStorage, sessionStorage, HTTPOnly cookies)
- Token synchronization across browser tabs/windows
- Logout functionality with token invalidation
- Error handling for expired/invalid tokens
- Token refresh retry logic (rate limiting, exponential backoff)
- Integration with existing authentication flows
Non-functional requirements
- Performance: Token operations must complete in <5ms for 95% of requests
- Security: Tokens must not be exposed in logs, console, or memory dumps
- Scalability: Support for concurrent token operations across multiple browser tabs
- Reliability: Graceful handling of network failures during token refresh
- Accessibility: Token-related error messages must be screen reader accessible
Constraints and assumptions
- Assumption: Authentication is handled by a separate identity provider (e.g., Auth0, AWS Cognito)
- Constraint: Must support modern browser environments (ES2018+)
- Assumption: Token refresh will be handled by the same backend that issued tokens
- Constraint: No third-party token libraries (must implement core logic in-house)
- Assumption: Tokens will be stored in browser storage mechanisms
Out of scope
- Complete authentication system implementation (login, signup, password reset)
- Token encryption/decryption at rest
- Multi-factor authentication integration
- Token revocation via backend APIs (token blacklisting)
- Mobile app token management (only web browser contexts)
- Token sharing between different domains or subdomains
- Token encryption in storage (only secure storage mechanisms)
2. Component Architecture
graph TD
A[AuthManager] --> B[TokenProvider]
A --> C[AuthContext]
A --> D[AuthInterceptor]
B --> E[TokenStorage]
C --> F[UserSession]
D --> G[APIInterceptor]
C --> H[AuthRoutes]
H --> I[ProtectedRoute]
H --> J[PublicRoute]
I --> K[AuthGuard]
K --> L[UserDashboard]
K --> M[UserProfile]
Component Tree and Breakdown
The JWT token management system will be built around a core AuthManager that orchestrates all authentication flows. The component architecture follows a clear separation of concerns:
Smart Components (Container):
AuthManager- Main orchestrator that manages authentication lifecycle and token refresh logicTokenProvider- Context provider that exposes auth state to the appAuthInterceptor- HTTP interceptor that handles automatic token injection and refreshAuthGuard- Route guard that protects authenticated routes
Dumb Components (Presentational):
LoginView- Form for user credentialsLogoutButton- UI trigger for logout flowUserSession- Displays current user session infoTokenStorage- Abstracts local storage operations
TypeScript Props Contracts
// AuthManager Props
interface AuthManagerProps {
children: React.ReactNode;
onAuthChange?: (authState: AuthState) => void;
tokenRefreshThreshold?: number; // seconds before expiration to refresh
autoLogoutOnTokenExpiry?: boolean;
}
// AuthContext Props
interface AuthContextProps {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
refreshToken: () => Promise<boolean>;
}
// TokenProvider Props
interface TokenProviderProps {
children: React.ReactNode;
storageKey?: string;
tokenRefreshThreshold?: number;
}
// AuthInterceptor Props
interface AuthInterceptorProps {
onTokenRefresh?: () => Promise<string>;
onTokenExpiry?: () => void;
apiBaseURL: string;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Authenticating : login()
Authenticating --> Authenticated : success
Authenticating --> Failed : error
Authenticated --> Refreshing : token_expired
Refreshing --> Authenticated : success
Refreshing --> Failed : error
Authenticated --> LoggingOut : logout()
LoggingOut --> Idle : success
Failed --> Idle : retry_login
Component Responsibilities
AuthManager: Main orchestrator that handles auth flow, token refresh, and session managementTokenProvider: Provides authentication context to the application using React Context APIAuthInterceptor: Intercepts HTTP requests to inject tokens and handle refresh logicAuthGuard: Protects route access based on authentication stateUserSession: Displays current user information and session status
Reusability Decisions
The system is designed to be highly reusable across different authentication flows:
- Token storage abstraction allows switching between localStorage, sessionStorage, or secure storage
- AuthInterceptor can be reused with any HTTP client (axios, fetch)
- AuthContext provides a consistent auth state interface for all components
Component Lifecycle Considerations
- Mounting: AuthManager initializes token state from storage and sets up interceptors
- Updating: TokenProvider updates context when auth state changes
- Unmounting: AuthManager cleans up event listeners and timers
- Error Handling: Components handle token refresh failures gracefully with fallback mechanisms
// AuthManager component lifecycle example
useEffect(() => {
// Initialize auth state from storage on mount
const storedAuth = getStoredAuth();
if (storedAuth) {
setAuthState(storedAuth);
}
// Setup token refresh interval
const refreshInterval = setInterval(() => {
if (isTokenExpiringSoon()) {
refreshToken();
}
}, tokenRefreshThreshold * 1000);
return () => {
clearInterval(refreshInterval);
// Cleanup any ongoing requests or timers
};
}, []);
3. State Management Design
Local state vs. global state decisions
JWT token management requires global state persistence across the application lifecycle. The token should be accessible from any component that makes authenticated API calls, and its lifecycle needs to be managed centrally. Local state is only appropriate for UI-specific transient states like "isLoggingIn" or "showTokenExpiredModal".
State management approach
Zustand is selected for JWT token management due to:
- Minimal boilerplate and simple API
- Built-in middleware support (e.g., for persistence)
- Excellent TypeScript support with strict typing
- No provider wrapping needed
- Easy integration with React Query for server state
State shape and normalization strategy
interface JwtState {
token: string | null;
refreshToken: string | null;
expiresAt: number | null;
isRefreshing: boolean;
isAuthenticated: boolean;
lastError: string | null;
}
Server state vs. client state separation
- Client state: Token values, refresh status, authentication flags
- Server state: User profile, permissions, API responses (managed via React Query)
- Token refresh operations are server-state dependent but client-managed
Cache invalidation strategy
- Token expiration triggers cache invalidation
- Manual logout clears all caches
- Failed refresh attempts invalidate token and trigger login flow
- Token refresh success updates all cached authenticated requests
Optimistic updates design
When refreshing tokens, optimistic updates are applied to:
- Update token in state immediately
- Re-authenticate pending requests with new token
- Show loading indicators during refresh process
Critical-path hook implementation
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { jwtDecode } from 'jwt-decode';
interface JwtStore {
token: string | null;
refreshToken: string | null;
expiresAt: number | null;
isRefreshing: boolean;
isAuthenticated: boolean;
lastError: string | null;
// Actions
setTokens: (accessToken: string, refreshToken?: string) => void;
clearTokens: () => void;
refreshAccessToken: () => Promise<boolean>;
handleTokenRefreshError: (error: string) => void;
isTokenExpired: () => boolean;
}
const useJwtStore = create<JwtStore>()(
persist(
(set, get) => ({
token: null,
refreshToken: null,
expiresAt: null,
isRefreshing: false,
isAuthenticated: false,
lastError: null,
setTokens: (accessToken, refreshToken) => {
const decoded = jwtDecode<{ exp: number }>(accessToken);
set({
token: accessToken,
refreshToken,
expiresAt: decoded.exp * 1000,
isAuthenticated: true,
lastError: null
});
},
clearTokens: () => {
set({
token: null,
refreshToken: null,
expiresAt: null,
isAuthenticated: false,
lastError: null
});
},
handleTokenRefreshError: (error) => {
set({
lastError: error,
token: null,
refreshToken: null,
isAuthenticated: false
});
},
isTokenExpired: () => {
const { expiresAt } = get();
return !expiresAt || Date.now() >= expiresAt;
},
refreshAccessToken: async () => {
const { refreshToken, isRefreshing } = get();
if (!refreshToken || isRefreshing) return false;
set({ isRefreshing: true });
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const { accessToken } = await response.json();
set({
token: accessToken,
isRefreshing: false
});
return true;
} catch (error) {
set({ isRefreshing: false });
get().handleTokenRefreshError((error as Error).message);
return false;
}
}
}),
{
name: 'jwt-storage',
partialize: (state) => ({
token: state.token,
refreshToken: state.refreshToken
})
}
)
);
// Custom hook for token management
export const useJwt = () => {
const {
token,
refreshToken,
expiresAt,
isRefreshing,
isAuthenticated,
lastError,
setTokens,
clearTokens,
refreshAccessToken,
handleTokenRefreshError,
isTokenExpired
} = useJwtStore();
return {
token,
refreshToken,
expiresAt,
isRefreshing,
isAuthenticated,
lastError,
setTokens,
clearTokens,
refreshAccessToken,
handleTokenRefreshError,
isTokenExpired
};
};
The hook returns a comprehensive token management interface with:
- Token lifecycle management (set, clear)
- Refresh capability with optimistic updates
- Error handling and state tracking
- Authentication status indicators
- Token expiration checks
This implementation provides a clean, centralized way to manage JWT tokens with proper error handling and persistence while maintaining type safety through TypeScript interfaces.
4. Routing & Navigation
Route Structure and Hierarchy
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
B --> D[/login]
B --> E[/register]
B --> F[/forgot-password]
C --> G[/dashboard]
C --> H[/profile]
C --> I[/settings]
G --> J[/dashboard/reports]
G --> K[/dashboard/analytics]
The routing structure follows a clear separation between public and protected routes:
- Public Routes:
/login,/register,/forgot-password- accessible without authentication - Protected Routes:
/dashboard,/profile,/settings- require valid JWT token for access - Nested Protected Routes:
/dashboard/reports,/dashboard/analytics- sub-routes under protected parent
Code Splitting Strategy
Route-based code splitting is implemented using React.lazy and Suspense for optimal bundle loading:
// routes/index.tsx
import { lazy, Suspense } from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';
const Login = lazy(() => import('features/auth/Login'));
const Dashboard = lazy(() => import('features/dashboard/Dashboard'));
const Profile = lazy(() => import('features/profile/Profile'));
export const AppRoutes = () => (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Suspense>
);
Justification: Route-based splitting provides better user experience by loading only the necessary code for each route, reducing initial bundle size and improving load times.
Navigation Guards and Authentication Flows
Authentication guards are implemented using React Router's useNavigate hook and custom authentication context:
// components/ProtectedRoute.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from 'hooks/useAuth';
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
const { isAuthenticated, token, refreshToken } = useAuth();
useEffect(() => {
if (!isAuthenticated) {
navigate('/login', { replace: true });
}
}, [isAuthenticated, navigate]);
return children;
};
Authentication Flow:
- User navigates to protected route without valid token
- ProtectedRoute component redirects to
/login - After successful login, user is redirected back to original route
- Token refresh logic handles automatic renewal when token expires
Deep Linking Support and URL Parameter Design
Deep linking is supported through:
- Query parameters for state persistence:
/dashboard?tab=reports&filter=date - Route parameters for dynamic content:
/user/:userId/profile - URL fragment support for internal navigation:
/dashboard#analytics
// utils/urlUtils.ts
export const buildDeepLink = (path: string, params?: Record<string, string>) => {
const searchParams = new URLSearchParams(params);
return `${path}?${searchParams.toString()}`;
};
export const parseDeepLink = (url: string) => {
const urlObj = new URL(url);
return {
path: urlObj.pathname,
params: Object.fromEntries(urlObj.searchParams.entries()),
};
};
Browser History Management and Scroll Restoration
Scroll restoration is handled using React Router's built-in ScrollRestoration component:
// App.tsx
import { ScrollRestoration } from 'react-router-dom';
function App() {
return (
<>
<ScrollRestoration />
<AppRoutes />
</>
);
}
Additional considerations:
- Custom scroll restoration logic for modals and popups
- Manual scroll position saving when navigating between routes with large content
- History state management for back/forward button behavior consistency
// hooks/useScrollPosition.ts
import { useEffect, useRef } from 'react';
export const useScrollPosition = () => {
const scrollPositionRef = useRef<number>(0);
useEffect(() => {
const handleScroll = () => {
scrollPositionRef.current = window.scrollY;
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return scrollPositionRef.current;
};
5. API Integration Layer
API Client Design Pattern
We'll implement a centralized API client using Axios with request/response interceptors for consistent token management and error handling. The client will be configured as a singleton with proper base URL configuration and timeout settings.
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api';
const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
withCredentials: true, // Important for cookie-based auth
});
// Request interceptor to add auth token
apiClient.interceptors.request.use(
(config) => {
const token = getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor to handle token refresh
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newToken = await refreshAuthToken();
localStorage.setItem('authToken', newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
// Redirect to login if refresh fails
localStorage.removeItem('authToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default apiClient;
Request/Response Interceptors
The interceptor strategy includes:
- Authentication Token Injection: Automatically adds Bearer token to all requests
- Token Refresh Logic: Handles 401 responses by attempting refresh and retry
- Error Normalization: Standardizes error responses for consistent handling
- Response Transformation: Converts raw API responses to domain models
Error Handling and Retry Strategy
We implement a robust error handling system with:
- Retry Logic: 2 retries for network errors with exponential backoff
- Error Classification: Distinguishes between client, server, and auth errors
- Global Error Handler: Centralized error reporting with user feedback
// api/errorHandler.ts
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
export const handleApiError = (error: any): ApiError => {
if (error.response) {
// Server responded with error status
const { status, data } = error.response;
return new ApiError(
status,
data.message || `Server Error: ${status}`,
data.code
);
} else if (error.request) {
// Request was made but no response received
return new ApiError(503, 'Network Error: Unable to reach server');
} else {
// Something else happened
return new ApiError(500, error.message || 'Unknown Error');
}
};
// Retry utility with exponential backoff
export const retryWithBackoff = async (
fn: () => Promise<any>,
retries = 2,
delay = 1000
): Promise<any> => {
try {
return await fn();
} catch (error) {
if (retries > 0 && [502, 503, 504].includes(error.response?.status)) {
await new Promise(resolve => setTimeout(resolve, delay));
return retryWithBackoff(fn, retries - 1, delay * 2);
}
throw error;
}
};
Loading, Error, and Empty States Design
We implement a consistent state management pattern across all API operations:
// api/types.ts
export type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: ApiError };
export type ApiResponse<T> = {
data: T | null;
loading: boolean;
error: ApiError | null;
refetch: () => void;
};
Data Fetching Strategy
For JWT token management, we use a hybrid approach:
- CSR (Client-Side Rendering) for interactive auth flows
- SSR for protected routes that need server-side rendering
- Caching Strategy: Token refresh tokens are cached with proper expiration handling
API Response Transformation Layer
We transform raw API responses into domain models for better type safety and consistency:
// api/transformers.ts
export interface AuthResponse {
token: string;
user: {
id: string;
email: string;
name: string;
};
}
export interface AuthToken {
accessToken: string;
refreshToken: string;
expiresAt: Date;
}
export const transformAuthResponse = (response: AuthResponse): AuthToken => {
return {
accessToken: response.token,
refreshToken: response.refreshToken,
expiresAt: new Date(Date.now() + 3600000), // 1 hour
};
};
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant Client
participant AuthAPI
participant TokenService
Client->>AuthAPI: Login Request (email/password)
AuthAPI-->>Client: JWT Token Response
Client->>TokenService: Store Token (localStorage)
Client->>AuthAPI: Protected Request
TokenService->>AuthAPI: Inject Bearer Token
AuthAPI-->>Client: Resource Response
Client->>AuthAPI: Token Refresh Request
TokenService->>AuthAPI: Refresh Token (cookie)
AuthAPI-->>Client: New JWT Token
Client->>TokenService: Update Stored Token
Token Refresh Flow
When a 401 error occurs, the system automatically attempts to refresh the token:
// api/auth.ts
export const refreshAuthToken = async (): Promise<string> => {
try {
const response = await apiClient.post('/auth/refresh', {
refreshToken: getRefreshToken(),
});
const { token } = response.data;
return token;
} catch (error) {
throw new Error('Failed to refresh authentication token');
}
};
export const getAuthToken = (): string | null => {
return localStorage.getItem('authToken');
};
export const getRefreshToken = (): string | null => {
return localStorage.getItem('refreshToken');
};
This API integration layer provides a robust, scalable foundation for JWT token management with proper error handling, automatic refresh capabilities, and seamless integration with the frontend state management system.
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for all routes
- Interaction to Next Paint (INP): <200ms for user interactions
- Cumulative Layout Shift (CLS): <0.1 for all pages
Bundle Size Budget per Route
- Authentication routes (Login, Signup): <150KB total
- Protected dashboard: <300KB total
- Profile/Settings: <100KB total
- All other routes: <50KB total
Critical Rendering Path Optimization
-
Critical CSS Inlining:
- Inline critical CSS for login and auth flows
- Defer non-critical CSS to prevent render-blocking
-
Resource Hints:
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin> <link rel="prefetch" href="/api/user/profile"> -
Code Splitting Strategy:
- Route-based:
/auth→AuthLazyComponent - Component-based: Token refresh logic isolated in
useTokenRefresh
- Route-based:
Image Optimization Strategy
- Format Selection: WebP for modern browsers, JPEG fallback
- Lazy Loading:
loading="lazy"on all auth-related images - Responsive Images:
<picture>
<source srcset="/img/login@2x.webp" media="(min-width: 768px)">
<img src="/img/login.webp" alt="Login background">
</picture>
Font Loading Strategy
- Font Display Optimization:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-display: swap;
}
- Preload Critical Fonts:
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>
Memoization Strategy
- useCallback for Event Handlers:
const handleTokenRefresh = useCallback(() => {
// refresh logic
}, [token, refreshToken]);
const handleLogout = useCallback(() => {
// logout logic
}, [authToken]);
- React.memo for Presentational Components:
const AuthStatus = React.memo(({ status }: { status: string }) => (
<div className="auth-status">{status}</div>
));
- useMemo for Computed Values:
const tokenExpiry = useMemo(() => {
return parseJwt(token).exp * 1000;
}, [token]);
Virtualization for Large Lists
Not applicable to JWT token management system, but included as reference:
// For potential future use in token history or session lists
import { FixedSizeList as List } from 'react-window';
const TokenHistoryItem = ({ index, style }: { index: number; style: any }) => (
<div style={style}>
{/* Token item content */}
</div>
);
const VirtualizedTokenList = () => (
<List
height={400}
itemCount={tokenHistory.length}
itemSize={50}
width="100%"
>
{TokenHistoryItem}
</List>
);
Prefetching and Preloading Strategy
- Token Refresh Preloading:
// Preload refresh token logic when user is active
const preloadTokenRefresh = () => {
// Load token refresh utility in background
import('~/utils/token-refresh').then(() => {
// Token refresh logic ready for use
});
};
- Route Preloading:
// Preload auth routes when user navigates to protected areas
const preloadAuthRoutes = () => {
if (isUserAuthenticated()) {
preload('/dashboard');
}
};
- Service Worker Integration:
// Register service worker for token caching
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(() => console.log('Token cache SW registered'));
}
Performance Monitoring
- Core Web Vitals Tracking:
// Track LCP, INP, CLS with PerformanceObserver
const observePerformance = () => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.startTime);
}
}
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
};
- Bundle Size Monitoring:
// Monitor bundle size in development
const checkBundleSize = () => {
if (process.env.NODE_ENV === 'development') {
console.log('Current bundle size:', window.performance.memory?.usedJSHeapSize);
}
};
- Token Refresh Performance:
// Measure token refresh duration
const measureTokenRefresh = async () => {
const start = performance.now();
await refreshAuthToken();
const end = performance.now();
console.log(`Token refresh took ${end - start}ms`);
};
7. Error Handling & Resilience
Error Boundary Placement Strategy
graph TD
A[App Root] --> B[AuthErrorBoundary]
A --> C[UIErrorBoundary]
B --> D[AuthenticationErrorFallback]
C --> E[GenericErrorFallback]
F[LoginFlow] --> G[AuthErrorBoundary]
H[Dashboard] --> I[UIErrorBoundary]
Global Error Handler Design
// src/core/error/GlobalErrorHandler.tsx
import React from 'react';
interface GlobalErrorContextType {
handleError: (error: Error, errorInfo: React.ErrorInfo) => void;
clearError: () => void;
}
const GlobalErrorContext = React.createContext<GlobalErrorContextType | undefined>(undefined);
export const useGlobalErrorHandler = () => {
const context = React.useContext(GlobalErrorContext);
if (!context) {
throw new Error('useGlobalErrorHandler must be used within GlobalErrorProvider');
}
return context;
};
export const GlobalErrorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [error, setError] = React.useState<Error | null>(null);
const [errorInfo, setErrorInfo] = React.useState<React.ErrorInfo | null>(null);
const handleError = (error: Error, errorInfo: React.ErrorInfo) => {
setError(error);
setErrorInfo(errorInfo);
// Log to monitoring service
console.error('Global error caught:', error, errorInfo);
};
const clearError = () => {
setError(null);
setErrorInfo(null);
};
return (
<GlobalErrorContext.Provider value={{ handleError, clearError }}>
{error ? (
<GenericErrorFallback error={error} onRetry={clearError} />
) : (
children
)}
</GlobalErrorContext.Provider>
);
};
Offline / Network Failure Handling
// src/core/network/NetworkStatus.ts
import { useEffect, useState } from 'react';
export const useNetworkStatus = () => {
const [isOnline, setIsOnline] = useState<boolean>(true);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
};
// src/core/error/NetworkError.ts
export class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = 'NetworkError';
}
}
Graceful Degradation Patterns
// src/core/error/OfflineFallback.tsx
import React from 'react';
interface OfflineFallbackProps {
onRetry: () => void;
}
export const OfflineFallback: React.FC<OfflineFallbackProps> = ({ onRetry }) => {
return (
<div className="offline-fallback">
<h2>Offline Mode</h2>
<p>You are currently offline. Some features may be limited.</p>
<button onClick={onRetry}>Retry Connection</button>
</div>
);
};
// src/core/error/TokenRefreshFallback.tsx
import React from 'react';
interface TokenRefreshFallbackProps {
onRetry: () => void;
}
export const TokenRefreshFallback: React.FC<TokenRefreshFallbackProps> = ({ onRetry }) => {
return (
<div className="token-refresh-fallback">
<h3>Token Refresh Required</h3>
<p>Your session has expired or token is invalid.</p>
<button onClick={onRetry}>Refresh Token</button>
</div>
);
};
User Feedback on Errors
// src/core/toast/ToastManager.tsx
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
export const ToastManager: React.FC = () => {
return (
<ToastContainer
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
);
};
// src/core/toast/ToastService.ts
export class ToastService {
static showSuccess(message: string) {
toast.success(message);
}
static showError(message: string, options?: { autoClose?: number }) {
toast.error(message, {
autoClose: options?.autoClose || 5000,
});
}
static showWarning(message: string) {
toast.warning(message);
}
}
Error Boundary Implementation
// src/core/error/AuthErrorBoundary.tsx
import React from 'react';
interface AuthErrorBoundaryState {
hasError: boolean;
error?: Error;
}
export class AuthErrorBoundary extends React.Component<
{ children: React.ReactNode },
AuthErrorBoundaryState
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('AuthErrorBoundary caught error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <AuthenticationErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// src/core/error/AuthenticationErrorFallback.tsx
import React from 'react';
interface AuthenticationErrorFallbackProps {
error?: Error;
onRetry?: () => void;
}
export const AuthenticationErrorFallback: React.FC<AuthenticationErrorFallbackProps> = ({
error,
onRetry,
}) => {
return (
<div className="auth-error-fallback">
<h2>Authentication Error</h2>
<p>We're having trouble with your session.</p>
{error && (
<pre className="error-details">{error.message}</pre>
)}
<button onClick={onRetry}>Re-authenticate</button>
</div>
);
};
Retry Strategy Implementation
// src/core/network/RetryStrategy.ts
export class RetryStrategy {
static exponentialBackoff(
maxRetries: number,
baseDelay: number = 1000
): (attempt: number) => number {
return (attempt: number) => {
if (attempt >= maxRetries) return 0;
return baseDelay * Math.pow(2, attempt);
};
}
static retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
backoffStrategy = this.exponentialBackoff(maxRetries)
): Promise<T> {
let lastError: Error;
const attempt = async (attemptNumber: number): Promise<T> => {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attemptNumber >= maxRetries) {
throw lastError;
}
const delay = backoffStrategy(attemptNumber);
await new Promise(resolve => setTimeout(resolve, delay));
return attempt(attemptNumber + 1);
}
};
return attempt(0);
}
}
Token Refresh Error Handling
// src/core/auth/TokenManager.ts
import { RetryStrategy } from '../network/RetryStrategy';
export class TokenManager {
private static refreshTokenPromise: Promise<string> | null = null;
static async refreshAccessToken(): Promise<string> {
if (this.refreshTokenPromise) {
return this.refreshTokenPromise;
}
this.refreshTokenPromise = RetryStrategy.retryWithBackoff(
async () => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: getStoredRefreshToken() }),
});
if (!response.ok) {
throw new Error(`Failed to refresh token: ${response.status}`);
}
const data = await response.json();
setStoredAccessToken(data.accessToken);
return data.accessToken;
},
3,
RetryStrategy.exponentialBackoff(3, 1000)
);
try {
const token = await this.refreshTokenPromise;
return token;
} finally {
this.refreshTokenPromise = null;
}
}
static handleTokenError(error: any): void {
if (error.status === 401) {
// Token expired or invalid
clearStoredTokens();
window.location.href = '/login';
} else if (error.status === 403) {
// Insufficient permissions
ToastService.showError('Access denied');
}
}
}
8. Testing Strategy
Unit Testing Approach
Component Tests (70% coverage target):
- Test all interactive components with various state transitions
- Verify proper prop handling and error boundary behavior
- Mock external dependencies (API calls, localStorage)
- Test edge cases like invalid token formats, expired tokens
Utility Tests (90% coverage target):
- Token parsing and validation functions
- JWT payload extraction and verification logic
- Token expiration calculation methods
- Storage read/write operations with proper error handling
Custom Hook Tests (100% coverage):
- Token refresh logic with mock API responses
- Error state transitions and recovery flows
- Session expiration handling and cleanup
- Token persistence across browser sessions
Integration Testing Strategy
API Integration Tests (50% coverage):
- End-to-end token refresh flow with mock server
- Authentication state synchronization across tabs
- Concurrent request handling during token refresh
- Error propagation from API to UI layer
Cross-Component Tests (30% coverage):
- Token validity propagation through component tree
- Auth context updates and re-renders
- Navigation guard behavior with invalid tokens
- Session timeout cascading effects
E2E Testing Scope
Cypress Test Coverage (80% of key user flows):
- Login → token storage → protected route access
- Token expiration → automatic refresh → continued session
- Manual logout → token cleanup → redirect to login
- Concurrent tab sessions → token invalidation in other tabs
Critical User Journeys:
- Successful authentication flow
- Token refresh during active session
- Session timeout and automatic logout
- Invalid token handling and recovery
- Multi-tab session management
Visual Regression Testing
Screenshot Strategy:
- Test token-related UI states (loading, error, success)
- Verify consistent styling across browsers
- Capture authentication modals and error messages
- Validate responsive behavior on mobile/desktop
Accessibility Testing Automation
Axe-core Integration:
- Run accessibility checks on all auth-related components
- Verify proper ARIA attributes for token status indicators
- Test keyboard navigation through authentication flows
- Validate screen reader compatibility with error messages
Test Data Strategy
Fixtures and Mocks:
- Valid JWT tokens with different expiration times (1hr, 30min, expired)
- Invalid token formats (malformed, wrong signature)
- API response mocks with success/failure scenarios
- Edge case token payloads (missing claims, invalid claims)
Test Data Isolation:
- Use separate test storage keys to prevent cross-test contamination
- Implement cleanup between test runs
- Mock localStorage/sessionStorage with isolated contexts
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The JWT token management system must meet WCAG 2.1 AA standards to ensure accessibility for users with disabilities:
- Text Contrast: All text must have a contrast ratio of at least 4.5:1 against its background (minimum 3:1 for large text)
- Keyboard Navigation: Full functionality must be available via keyboard alone, including token input fields and action buttons
- Focus Management: Clear visual focus indicators for all interactive elements, with proper focus order that follows logical reading flow
- Screen Reader Support: All interactive elements must be properly labeled for screen reader users with ARIA attributes where needed
- Error Handling: Error messages must be programmatically associated with their input fields and announced to screen readers
- Input Validation: Real-time validation feedback must be available through both visual and auditory means
Keyboard navigation support
All JWT token management functionality must be fully operable using keyboard navigation only:
- Tab order follows logical reading flow: token input → validation button → refresh button → logout button
- Focus is properly managed when modals open/close (focus trapped within modal, restored to previous element on close)
- Keyboard-accessible dropdown menus for token expiration options
- Enter and Space keys activate all interactive elements (buttons, checkboxes, radio buttons)
- Escape key closes modals and dropdowns when open
- Focus indicators are visible and clearly distinguishable from surrounding elements
Focus management strategy
stateDiagram-v2
[*] --> Idle
Idle --> TokenInputFocused: Tab to token input
TokenInputFocused --> ValidateButtonFocused: Tab to validate button
ValidateButtonFocused --> RefreshButtonFocused: Tab to refresh button
RefreshButtonFocused --> LogoutButtonFocused: Tab to logout button
LogoutButtonFocused --> Idle: Tab to first element or Escape
TokenInputFocused --> ModalOpen: Open token modal
ModalOpen --> ModalContentFocused: Focus inside modal
ModalContentFocused --> ModalCloseButtonFocused: Tab to close button
ModalCloseButtonFocused --> TokenInputFocused: Escape closes modal
ValidateButtonFocused --> ValidationInProgress: Click validate
ValidationInProgress --> ValidationSuccess: Success response
ValidationSuccess --> Idle: Show success message
RefreshButtonFocused --> RefreshInProgress: Click refresh
RefreshInProgress --> RefreshSuccess: Success response
RefreshSuccess --> Idle: Show success message
ARIA roles and attributes plan
- Token Input Field:
aria-describedby="token-help-text"for assistive text,aria-invalidwhen validation fails - Validation Button:
aria-label="Validate JWT Token"for screen readers,aria-describedby="validation-status" - Modal Dialog:
role="dialog"witharia-modal="true",aria-labelledby="modal-title" - Loading States:
aria-busy="true"on loading elements,aria-live="polite"for status updates - Error Messages:
role="alert"witharia-live="assertive"for immediate feedback - Progress Indicators:
aria-valuenow,aria-valuemin,aria-valuemaxfor progress bars
Screen reader compatibility
All interactive elements must be properly labeled:
- Buttons with icons must have
aria-labeloraria-labelledbyattributes - Token input fields must announce their purpose and current value when focused
- Validation status messages must be announced immediately to screen reader users
- Success/failure states must provide clear auditory feedback with appropriate ARIA live regions
- Dropdown menus must announce selection changes and available options to screen readers
// Example accessible token input component with proper ARIA attributes
interface TokenInputProps {
value: string;
onChange: (value: string) => void;
error?: string;
isValidating?: boolean;
}
const TokenInput: React.FC<TokenInputProps> = ({
value,
onChange,
error,
isValidating
}) => {
const inputId = useId();
return (
<div className="token-input-container">
<label htmlFor={inputId} className="sr-only">
JWT Token
</label>
<input
id={inputId}
type="password"
value={value}
onChange={(e) => onChange(e.target.value)}
aria-describedby={error ? `${inputId}-error` : undefined}
aria-invalid={!!error}
aria-busy={isValidating}
className={`token-input ${error ? 'error' : ''}`}
placeholder="Enter JWT token"
/>
{error && (
<div
id={`${inputId}-error`}
role="alert"
aria-live="assertive"
className="error-message"
>
{error}
</div>
)}
</div>
);
};
The token management system must support screen reader users through proper landmark regions, semantic HTML structure, and ARIA attributes that provide context and status information for all interactive elements. All error states must be announced to screen readers with appropriate urgency levels, and focus must be programmatically managed when modals open or validation occurs.
10. Security Considerations
XSS Prevention Strategy
Content Security Policy (CSP) Implementation:
// CSP header configuration (server-side)
const cspPolicy = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Only for development
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'"
].join('; ');
// In server middleware
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', cspPolicy);
});
DOM-based XSS Protection:
// Safe DOM manipulation helper
const safeInsertHTML = (element: HTMLElement, html: string) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const body = doc.body;
// Remove any script tags and event handlers
const scripts = body.querySelectorAll('script');
scripts.forEach(script => script.remove());
// Sanitize attributes
const elements = body.querySelectorAll('*');
elements.forEach(el => {
const attrs = el.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name.startsWith('on')) {
el.removeAttribute(attr.name);
}
});
});
element.innerHTML = body.innerHTML;
};
CSRF Protection
CSRF Token Implementation:
// CSRF token manager with automatic injection
class CSRFTokenManager {
private token: string | null = null;
constructor() {
this.fetchToken();
}
async fetchToken(): Promise<string> {
const response = await fetch('/api/csrf-token', {
method: 'GET',
credentials: 'same-origin'
});
const { token } = await response.json();
this.token = token;
return token;
}
getHeaders(): Record<string, string> {
return {
'X-CSRF-Token': this.token || '',
'X-Requested-With': 'XMLHttpRequest'
};
}
// Validate token in server response
validateToken(response: Response): boolean {
const token = response.headers.get('X-CSRF-Token');
return token === this.token;
}
}
Content Security Policy (CSP) Headers
CSP Implementation:
// CSP configuration with security headers
const securityHeaders = {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin'
};
// Apply to all responses
app.use((req, res, next) => {
Object.entries(securityHeaders).forEach(([key, value]) => {
res.setHeader(key, value);
});
next();
});
Sensitive Data Handling in Frontend State
Secure Token Storage:
// Secure token storage with encryption (limited scope)
class SecureTokenStore {
private static readonly STORAGE_KEY = 'auth_tokens';
// Store tokens in memory only (not localStorage/sessionStorage)
private static tokens: {
accessToken: string | null;
refreshToken: string | null;
} = {
accessToken: null,
refreshToken: null
};
static setTokens(accessToken: string, refreshToken: string) {
// Clear any existing tokens
this.tokens.accessToken = null;
this.tokens.refreshToken = null;
// Store in memory only (no persistence)
this.tokens.accessToken = accessToken;
this.tokens.refreshToken = refreshToken;
// Clear any stale tokens from memory
this.clearStaleTokens();
}
static getAccessToken(): string | null {
return this.tokens.accessToken;
}
static getRefreshToken(): string | null {
return this.tokens.refreshToken;
}
static clearTokens() {
this.tokens.accessToken = null;
this.tokens.refreshToken = null;
}
static clearStaleTokens() {
// Clear tokens that are expired or invalid
if (this.tokens.accessToken && this.isTokenExpired(this.tokens.accessToken)) {
this.tokens.accessToken = null;
}
if (this.tokens.refreshToken && this.isTokenExpired(this.tokens.refreshToken)) {
this.tokens.refreshToken = null;
}
}
private static isTokenExpired(token: string): boolean {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return Date.now() >= payload.exp * 1000;
} catch (e) {
return true;
}
}
}
Input Sanitization Approach
Input Validation and Sanitization:
// Input sanitization utilities
class InputSanitizer {
static sanitizeString(input: string): string {
// Remove potentially dangerous characters
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
static sanitizeURL(input: string): string {
try {
const url = new URL(input);
// Only allow https and http protocols
if (!['https:', 'http:'].includes(url.protocol)) {
throw new Error('Invalid protocol');
}
return url.toString();
} catch (e) {
throw new Error('Invalid URL format');
}
}
static validateTokenStructure(token: string): boolean {
// Basic JWT structure validation
const parts = token.split('.');
if (parts.length !== 3) return false;
try {
// Validate base64 structure
const [header, payload, signature] = parts;
JSON.parse(atob(header));
JSON.parse(atob(payload));
return true;
} catch (e) {
return false;
}
}
}
Token Security Best Practices
Secure Token Handling:
// JWT token validation and security checks
class JWTSecurity {
static validateToken(token: string): boolean {
// Basic structure validation
if (!this.isValidJWTFormat(token)) return false;
// Validate signature (server-side verification)
if (!this.verifySignature(token)) return false;
// Check expiration
if (this.isTokenExpired(token)) return false;
// Validate audience and issuer
const payload = this.decodePayload(token);
if (!this.validateAudience(payload)) return false;
return true;
}
private static isValidJWTFormat(token: string): boolean {
const parts = token.split('.');
return parts.length === 3 &&
parts.every(part => /^[A-Za-z0-9+/]*={0,2}$/.test(part));
}
private static isTokenExpired(token: string): boolean {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return Date.now() >= payload.exp * 1000;
} catch (e) {
return true;
}
}
private static decodePayload(token: string): any {
const payload = token.split('.')[1];
return JSON.parse(atob(payload));
}
private static verifySignature(token: string): boolean {
// In production, this would involve server-side signature verification
// This is a simplified client-side check for demonstration
return true;
}
private static validateAudience(payload: any): boolean {
// Validate that token is intended for this application
return payload.aud === process.env.REACT_APP_CLIENT_ID;
}
}
Token Rotation and Refresh Strategy
Secure Token Refresh:
// Secure token refresh with validation
class SecureTokenRefresh {
private static readonly REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 minutes
static async refreshAccessToken(
refreshToken: string,
onTokenRefreshed: (newToken: string) => void
): Promise<string | null> {
try {
// Validate refresh token before use
if (!this.validateRefreshToken(refreshToken)) {
throw new Error('Invalid refresh token');
}
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${refreshToken}`
},
body: JSON.stringify({
refreshToken: refreshToken
})
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const { accessToken } = await response.json();
// Validate new access token
if (!JWTSecurity.validateToken(accessToken)) {
throw new Error('Invalid refreshed token');
}
onTokenRefreshed(accessToken);
return accessToken;
} catch (error) {
console.error('Token refresh failed:', error);
return null;
}
}
private static validateRefreshToken(token: string): boolean {
// Validate refresh token format and expiration
return JWTSecurity.validateToken(token) &&
this.isRefreshToken(token);
}
private static isRefreshToken(token: string): boolean {
try {
const payload = this.decodePayload(token);
return payload.tokenType === 'refresh';
} catch (e) {
return false;
}
}
private static decodePayload(token: string): any {
const payload = token.split('.')[1];
return JSON.parse(atob(payload));
}
}
Security Headers Implementation
Security Headers Configuration:
// Security headers for all API requests and responses
const securityConfig = {
// Prevent XSS attacks
'X-XSS-Protection': '1; mode=block',
// Prevent clickjacking
'X-Frame-Options': 'DENY',
// Prevent MIME type sniffing
'X-Content-Type-Options': 'nosniff',
// CSRF protection headers
'X-CSRF-Token': 'required',
// Referrer policy for privacy
'Referrer-Policy': 'strict-origin-when-cross-origin',
// Content Security Policy (CSP)
'Content-Security-Policy': [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'"
].join('; ')
};
// Apply security headers to all responses
const applySecurityHeaders = (res: Response) => {
Object.entries(securityConfig).forEach(([header, value]) => {
res.headers.set(header, value);
});
};
Token Storage Security
Secure Token Storage Strategy:
// Comprehensive token storage with security measures
class SecureTokenStorage {
private static readonly TOKENS = new Map<string, string>();
static setTokens(accessToken: string, refreshToken: string) {
// Validate tokens before storage
if (!JWTSecurity.validateToken(accessToken)) {
throw new Error('Invalid access token');
}
if (!JWTSecurity.validateToken(refreshToken)) {
throw new Error('Invalid refresh token');
}
// Store tokens in memory only (no persistence)
this.TOKENS.set('access', accessToken);
this.TOKENS.set('refresh', refreshToken);
// Set up automatic token cleanup
this.setupTokenCleanup();
}
static getAccessToken(): string | null {
return this.TOKENS.get('access') || null;
}
static getRefreshToken(): string | null {
return this.TOKENS.get('refresh') || null;
}
static clearTokens() {
this.TOKENS.clear();
}
private static setupTokenCleanup() {
// Clear tokens when app is inactive for too long
const timeout = setTimeout(() => {
this.clearTokens();
}, 30 * 60 * 1000); // 30 minutes of inactivity
// Clear on window blur (user switches tabs)
window.addEventListener('blur', () => {
clearTimeout(timeout);
this.clearTokens();
});
}
}
Security Implementation Summary
Key Security Measures:
- No sensitive data in localStorage/sessionStorage - Tokens stored in memory only
- CSP implementation - Prevents XSS and content injection attacks
- CSRF protection - Token validation for all API requests
- Token format validation - JWT structure and signature verification
- Secure token rotation - Refresh tokens with proper validation
- Input sanitization - Prevents malicious input injection
- Security headers - Comprehensive HTTP security headers
- Token expiration handling - Automatic token cleanup and refresh
Security Considerations:
- Tokens are never persisted to storage (localStorage, sessionStorage)
- All tokens are validated before use
- CSP headers prevent XSS attacks
- CSRF tokens protect against cross-site request forgery
- Token refresh is handled securely with validation
- Input sanitization prevents injection attacks
- Security headers provide additional protection layers
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Translation keys will follow a hierarchical, descriptive naming convention to ensure maintainability and clarity:
auth.login.email.placeholder
auth.login.password.placeholder
auth.login.submit.button
auth.login.forgot-password.link
auth.login.signup.link
auth.login.error.invalid-credentials
auth.login.error.network-unavailable
Keys will use dot notation for nesting, with lowercase letters and hyphens for word separation. Keys will be organized by feature/module first (e.g., auth, dashboard) followed by component-specific paths.
RTL Support Considerations
RTL (Right-to-Left) language support will be implemented with:
- CSS classes that flip layout direction (
direction: rtl) - Text alignment adjustments (
text-align: right) - Margin/padding overrides for directional consistency
- Component-level RTL-aware styling using CSS modules or styled-components with conditional logic
RTL support will be toggled via a global isRTL context flag that affects the entire application layout.
Date, Number, Currency Formatting Strategy
All date/time formatting will use Intl.DateTimeFormat with locale-specific options:
const formatDate = (date: Date, locale: string = 'en-US') => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
const formatCurrency = (amount: number, locale: string = 'en-US') => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(amount);
};
const formatNumber = (num: number, locale: string = 'en-US') => {
return new Intl.NumberFormat(locale).format(num);
};
Locale Detection and Switching Strategy
Automatic locale detection will be implemented using:
- Browser
navigator.languageas primary fallback - User preference stored in localStorage or cookie
- Manual override via language selector UI component
The locale switching strategy will:
- Update the
i18ncontext provider with new locale - Re-fetch translated content if needed
- Update all date/time formatting contexts
- Maintain session persistence across page refreshes
// Example locale switching logic
const switchLocale = (newLocale: string) => {
setLocale(newLocale);
localStorage.setItem('preferred-locale', newLocale);
// Reinitialize all i18n-aware components
};
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
The JWT token management system should track key performance indicators that directly impact user experience:
- Token Refresh Success Rate: Percentage of successful token refresh operations vs. total attempts
- Token Expiry Latency: Time between token expiration and successful refresh (target < 2s)
- Authentication Flow Duration: End-to-end time from login to token availability (target < 3s)
- Network Round-trip Time: Average time for token refresh requests (target < 500ms)
- Failed Authentication Rate: Percentage of authentication failures vs. total auth attempts
Error Tracking Integration
Sentry Integration Strategy:
- Capture all JWT-related errors with structured context (token type, error code, timestamp)
- Log authentication failures with user session metadata for debugging
- Track token refresh timeouts and network errors with retry count information
- Implement error grouping to identify patterns in authentication failures
Custom Events and User Journey Tracking
Authentication Flow Events:
// Event tracking for authentication journeys
const trackAuthEvent = (event: string, properties?: Record<string, any>) => {
// Track token refresh success/failure
// Track login/logout events
// Track session expiration events
// Track token validation results
};
User Journey Triggers:
- Token refresh initiated (user action or automatic)
- Session expiration detected
- Authentication flow start/end
- Token validation success/failure
Performance Monitoring and Regression Detection
Performance Baselines:
- Token refresh operations should complete under 500ms (95th percentile)
- Authentication flow should complete under 2 seconds (95th percentile)
- Memory usage for token storage should remain stable (< 10KB per session)
Regression Detection:
- Implement automated performance tests in CI pipeline
- Set up alerts for token refresh latency > 1000ms
- Monitor authentication failure rate increases beyond 2% threshold
- Track token refresh success rate drops below 98%
Monitoring Dashboard Components:
- Real-time Authentication Metrics: Live dashboard showing token refresh rates, success/failure ratios
- Error Rate Trends: Historical view of authentication errors and token-related failures
- User Session Health: Overview of active sessions, expiration patterns, refresh frequency
- Performance Baselines: Comparison of current performance vs. historical baselines
Alerting Strategy:
- Critical: Token refresh failures > 5% of requests
- Warning: Authentication flow duration > 3s
- Info: Token refresh latency > 500ms (95th percentile)
- Recovery: Automatic alert reset when metrics return to baseline
Implementation Considerations
Telemetry Data Collection:
- All telemetry data should be sampled to avoid performance impact (10% sampling rate for detailed metrics)
- Implement data anonymization for user session identifiers
- Ensure compliance with privacy regulations (GDPR, CCPA) in telemetry collection
Data Pipeline:
- Use structured logging for all JWT operations
- Implement batching for telemetry data to reduce network overhead
- Design for both real-time monitoring and batch analytics processing
Integration Points:
- Connect to existing observability platforms (Datadog, New Relic)
- Support for custom logging integrations
- Export capability for compliance and audit purposes
Metrics Granularity:
- Per-user session metrics for enterprise customers
- Aggregated system-wide metrics for general monitoring
- Regional/tenant-level breakdowns where applicable
This observability design ensures comprehensive monitoring of JWT token management while maintaining performance and privacy standards expected in enterprise frontend applications.
13. File & Folder Structure
Proposed Directory Structure
src/
├── features/
│ └── auth/
│ ├── components/
│ │ ├── LoginModal.tsx
│ │ ├── LogoutButton.tsx
│ │ └── AuthGuard.tsx
│ ├── hooks/
│ │ ├── useAuth.ts
│ │ └── useTokenRefresh.ts
│ ├── services/
│ │ ├── authService.ts
│ │ └── tokenService.ts
│ ├── types/
│ │ └── auth.types.ts
│ └── utils/
│ └── tokenUtils.ts
├── shared/
│ ├── hooks/
│ │ └── useLocalStorage.ts
│ ├── utils/
│ │ └── cryptoUtils.ts
│ └── types/
│ └── common.types.ts
├── store/
│ ├── authSlice.ts
│ └── index.ts
└── services/
└── apiClient.ts
Module Boundaries and Import Rules
Feature-based structure chosen over layer-based to:
- Encapsulate auth functionality in a single module
- Enable easy feature extraction if needed
- Maintain clear ownership boundaries between auth and other features
Import Rules:
- Auth feature can import from shared/ but not from other features
- No cross-feature imports within features directory
- Barrel exports for components and services to reduce import paths
Shared vs Feature-Specific Code Separation
Shared code (in src/shared/):
- Generic utilities used across multiple features
- Common types that don't belong to a specific feature
- Reusable hooks like
useLocalStorage
Feature-specific code (in src/features/auth/):
- Auth-specific components and logic
- Token handling utilities that only apply to auth flow
- Service layer for auth-specific API calls
This structure enables clean separation of concerns while maintaining the flexibility to extract features later if needed. The auth feature is self-contained and can be tested in isolation, while shared utilities are available for cross-feature use without tight coupling.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. jsonwebtoken (jwt-simple)
- Justification: Core JWT parsing and validation functionality. Provides reliable token decoding and signature verification with minimal bundle size.
- Bundle Size: ~2KB
- Security: Well-maintained library with regular security updates
2. axios
- Justification: Industry standard HTTP client with built-in interceptors, request/response transformation, and robust error handling capabilities.
- Bundle Size: ~15KB (with interceptors)
- Features: Request/response interceptors, automatic JSON parsing, timeout handling
3. react-query (TanStack Query)
- Justification: Superior data fetching and state management for API interactions. Provides automatic caching, background refetching, and optimistic updates.
- Bundle Size: ~12KB
- Advantages: Built-in cache invalidation, automatic retry logic, server state synchronization
4. zod
- Justification: Type-safe validation for API responses and token payloads. Provides runtime type checking with zero runtime overhead.
- Bundle Size: ~5KB
- Benefits: Compile-time type safety with runtime validation, excellent error messages
5. react-router-dom
- Justification: Standard routing solution with excellent ecosystem integration and performance optimizations.
- Bundle Size: ~10KB (core)
- Features: Route-based code splitting, nested routing, lazy loading support
Alternatives Considered and Rejected
1. jose vs jsonwebtoken
- Rejected: jose is more feature-complete but significantly larger (20KB+) and overkill for basic JWT validation use cases.
- Decision: jwt-simple provides sufficient functionality with better performance characteristics.
2. redux vs zustand
- Rejected: Redux adds unnecessary complexity for token management. Zustand provides simpler state management with better performance.
- Decision: Zustand's minimal API surface and zero boilerplate make it ideal for token state management.
3. react-native vs react
- Rejected: React is the appropriate choice for web applications. React Native would be overkill and introduce unnecessary complexity.
- Decision: React's ecosystem and component model are well-suited for token management UI components.
Licensing and Bundle Size Impact Assessment
Bundle Size Budget Allocation:
- Core JWT handling: ~5KB (jsonwebtoken + zod)
- HTTP client and API layer: ~15KB (axios + react-query)
- Routing and navigation: ~10KB (react-router-dom)
- Total estimated: ~30KB (excluding React ecosystem)
License Compliance: All selected libraries are MIT or BSD-3-Clause licensed, compatible with enterprise use cases. Regular security scanning will be implemented to monitor for license changes or vulnerabilities.
Tree-shaking Strategy:
- axios: Only import required modules (interceptors, request methods)
- react-query: Import only specific hooks and utilities
- zod: Use only necessary validation schemas
Performance Impact: All libraries are chosen for minimal runtime impact and optimal tree-shaking support to maintain bundle size efficiency while providing necessary functionality.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Token storage location: Should JWTs be stored in HTTP-only cookies (more secure but harder to access from frontend) or localStorage/sessionStorage (easier to work with but vulnerable to XSS). This decision directly impacts the entire security model and requires alignment between frontend and backend teams.
- Refresh token strategy: Whether to use refresh tokens with sliding expiration or fixed expiration times, and how often to rotate them. This affects both security posture and user experience.
- Token scope and permissions: How granular should token scopes be? Should we implement RBAC or simpler role-based access control?
Known trade-offs made in this design
- Security vs. convenience: Prioritizing developer experience over maximum security by using localStorage for token storage in the prototype, with a note that production should use HTTP-only cookies
- State management complexity: Using a global state manager (Zustand) for token management despite the simplicity of context API, to support future scalability and complex state transitions
- Performance vs. security: Implementing automatic token refresh before expiration to prevent 401s but potentially increasing server load and latency
Risks and mitigations
- Token theft risk: If tokens are stored in localStorage, they're vulnerable to XSS attacks. Mitigation: Implement Content Security Policy (CSP) headers and regular security audits
- Session hijacking: Risk of session theft through man-in-the-middle attacks. Mitigation: Enforce HTTPS and implement secure cookie flags (HttpOnly, SameSite, Secure)
- Token expiration handling: Risk of unhandled 401 responses causing UI inconsistencies. Mitigation: Implement comprehensive error boundary and global error handler with automatic redirect to login
- Offline state handling: Risk of stale tokens being used when network is restored. Mitigation: Implement proper offline detection and token refresh logic with exponential backoff retry strategy
Technical debt acknowledged
- Hardcoded token expiration times: Currently using hardcoded 15-minute access tokens and 7-day refresh tokens, which should be configurable via environment variables or backend configuration
- Simplified error handling: Basic toast notifications instead of comprehensive error categorization and user guidance for different error types (network, auth, server errors)
- Limited token scope support: Current implementation assumes all tokens have the same scope and permissions, but future versions should support granular token scopes for different API endpoints
- No token revocation mechanism: The design doesn't include support for immediate token invalidation (e.g., logout from all devices), which would require backend integration and additional state management complexity