Secure token storage is critical for modern web applications handling authentication and authorization flows. This document explores best practices, implementation strategies, and security considerations for protecting sensitive authentication tokens across different client-side environments.
1. Problem Statement & Requirements
Clarifying questions
- What authentication method is being used (JWT, OAuth, SAML, etc.) and what are the token expiration policies?
- What target environments does this need to support (web browsers, mobile web, PWA, native apps)?
- Are there specific security compliance requirements (GDPR, HIPAA, SOC2) that must be addressed?
- What level of offline support is required for token persistence (e.g., should tokens persist across browser sessions)?
- Are there any specific accessibility or internationalization requirements for token handling UI?
- What is the expected token size and refresh frequency in typical usage patterns?
- Does the system need to support cross-origin token sharing or is it strictly same-origin?
Functional requirements
- Securely store authentication tokens (JWT, OAuth tokens) in the browser
- Automatically refresh expired tokens when possible
- Provide token access and removal APIs to authenticated components
- Support secure storage in memory vs. persistent storage (localStorage/sessionStorage)
- Implement token rotation and refresh mechanisms
- Handle token invalidation on logout or session expiration
- Support secure token transmission to backend services
Non-functional requirements
- Security: Tokens must never be accessible via XSS attacks or browser dev tools
- Performance: Token access should have minimal latency (sub-10ms response time)
- Reliability: Token storage must survive page refreshes and browser sessions
- Compliance: Adhere to security best practices (OWASP, NIST guidelines)
- Scalability: Storage mechanism must handle concurrent token access across multiple tabs
- Maintainability: Storage layer should be easily testable and maintainable
Constraints and assumptions
- Tokens are assumed to be sensitive data that must never appear in browser console or logs
- Browser environment is assumed to support modern security features (Secure Contexts, HTTPOnly cookies)
- No server-side token storage is available for this design (client-side only solution)
- Token size is assumed to be under 10KB for performance reasons
- No third-party token storage services are available or preferred
Out of scope
- Token encryption at rest (assuming secure storage mechanisms are sufficient)
- Cross-origin token sharing or SSO implementation
- Token encryption in transit (assumes HTTPS is already enforced)
- Token backup or recovery mechanisms
- Token versioning or migration strategies
- Integration with external authentication providers (e.g., Auth0, Okta)
- Mobile app-specific token storage (iOS/Android native storage)
2. Component Architecture
graph TD
A[AuthManager] --> B[TokenStorageProvider]
A --> C[SecureTokenService]
B --> D[LocalStorageManager]
B --> E[MemoryStorageManager]
C --> F[TokenValidator]
C --> G[TokenRefreshHandler]
C --> H[SecureStorageManager]
D --> I[TokenPersistenceLayer]
E --> I
H --> J[EncryptedStorage]
H --> K[SecureKeyStore]
classDef container fill:#4CAF50,stroke:#333,color:white;
classDef component fill:#2196F3,stroke:#333,color:white;
classDef service fill:#FF9800,stroke:#333,color:white;
class A,B,C,H container
class D,E,F,G,I,J,K component
class C service
Component Tree Breakdown
Smart (Container) Components:
AuthManager- orchestrates authentication flow and token lifecycleTokenStorageProvider- provides secure storage context to child componentsSecureTokenService- core logic for token operations and validation
Dumb (Presentational) Components:
LocalStorageManager- handles local storage operationsMemoryStorageManager- manages in-memory token storageTokenValidator- validates token integrity and expirationTokenRefreshHandler- manages token refresh logicSecureStorageManager- abstracts secure storage operationsEncryptedStorage- handles encryption/decryption of tokensSecureKeyStore- manages secure key storage
TypeScript Props Contracts
// AuthManager props
interface AuthManagerProps {
children: React.ReactNode;
onTokenRefresh?: (newToken: string) => void;
onTokenExpiry?: () => void;
onError?: (error: Error) => void;
autoRefresh?: boolean;
refreshThreshold?: number; // seconds before expiry to trigger refresh
}
// TokenStorageProvider props
interface TokenStorageProviderProps {
storageType: 'localStorage' | 'memory';
encryptionKey?: string;
children: React.ReactNode;
}
// SecureTokenService props
interface SecureTokenServiceProps {
token?: string;
onTokenChange?: (token: string) => void;
onTokenExpiry?: () => void;
refreshThreshold?: number;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> CheckingToken : init
CheckingToken --> ValidToken : token_valid
CheckingToken --> InvalidToken : token_invalid
ValidToken --> Refreshing : refresh_required
ValidToken --> Expired : token_expired
Refreshing --> ValidToken : refresh_success
Refreshing --> InvalidToken : refresh_failed
InvalidToken --> Idle : token_reset
Expired --> Idle : token_reset
state Refreshing {
[*] --> RequestingRefresh
RequestingRefresh --> RefreshSuccess : success
RequestingRefresh --> RefreshFailed : error
}
Component Responsibilities
AuthManager
- Coordinates authentication lifecycle across the app
- Manages token refresh and expiration handling
- Provides error boundaries for auth-related errors
- Handles fallback logic when secure storage fails
TokenStorageProvider
- Determines and configures appropriate token storage strategy
- Wraps child components with secure context
- Handles fallback mechanisms between localStorage and memory
SecureTokenService
- Implements core token validation logic
- Manages token refresh cycles with configurable thresholds
- Handles encryption/decryption of tokens in secure storage
- Provides token state transitions to parent components
Reusability Decisions
The component architecture is designed for maximum reusability:
- Storage Abstraction:
TokenStorageProviderallows switching between localStorage and memory storage without affecting business logic - Encryption Layer:
SecureStorageManagercan be reused across different token types or authentication systems - Validation Logic:
TokenValidatorcan be extended for different token formats (JWT, OAuth, etc.) - Error Handling:
AuthManagerprovides consistent error handling patterns across the application
Component Lifecycle Considerations
- Mounting:
AuthManagerinitializes token validation and sets up event listeners - Token Refresh:
SecureTokenServicemonitors token expiration and triggers refresh cycles - Unmounting: Cleanup of event listeners and memory cleanup for in-memory storage
- Error Recovery: Fallback to memory storage when localStorage fails, with graceful degradation
The architecture supports both client-side and server-side rendering scenarios while maintaining secure token handling across all environments.
3. State Management Design
Local state vs. global state decisions
For secure token storage, we need to distinguish between:
- Local state: Tokens that are only relevant within a single component (e.g., login form fields)
- Global state: Authentication tokens that need to be accessible across the entire application (access token, refresh token)
- Session state: User session metadata that should persist across navigation but not be stored in insecure locations
State management approach
Zustand is selected as the primary state management solution for the following reasons:
- Minimal API surface: Simpler than Redux Toolkit with less boilerplate
- No provider wrapping required: Easier to integrate into existing component trees
- Middleware support: Built-in middleware for logging, persistence, and error handling
- TypeScript native: Excellent type inference and support for complex state shapes
- Lightweight: Bundle size impact is minimal compared to Redux Toolkit
State shape and normalization strategy
// TokenStoreState
interface TokenStoreState {
// Secure token storage (in-memory only)
accessToken: string | null;
refreshToken: string | null;
// Session metadata
user: User | null;
isAuthenticated: boolean;
tokenExpiry: Date | null;
// Token refresh state
isRefreshing: boolean;
refreshError: string | null;
// Session lifecycle state
lastLogin: Date | null;
sessionTimeout: number | null;
}
Server state vs. client state separation
Client state (secure storage):
- Access tokens (in-memory only, never persisted to localStorage/sessionStorage)
- Refresh tokens (in-memory only, never persisted to localStorage/sessionStorage)
- User session metadata
Server state (API-managed):
- Token validation status
- Session expiration times from server
- User permissions and roles
Cache invalidation strategy
- Token refresh: Invalidate access token when it expires, trigger refresh
- Session invalidation: Clear all tokens on logout or server-side session expiration
- Manual invalidation: Clear tokens when user explicitly logs out or token is revoked
Optimistic updates design
For token refresh operations:
- Optimistic update: Immediately use new access token if available
- Fallback handling: If refresh fails, revert to previous state and show login prompt
- Concurrent request handling: Prevent multiple simultaneous refresh attempts
Critical-path hook implementation
// src/hooks/useTokenStore.ts
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import type { User } from '../types/user';
interface TokenStoreState {
accessToken: string | null;
refreshToken: string | null;
user: User | null;
isAuthenticated: boolean;
tokenExpiry: Date | null;
isRefreshing: boolean;
refreshError: string | null;
lastLogin: Date | null;
sessionTimeout: number | null;
}
interface TokenStoreActions {
setTokens: (accessToken: string, refreshToken: string) => void;
clearTokens: () => void;
setUser: (user: User) => void;
setTokenExpiry: (expiry: Date) => void;
startRefresh: () => void;
completeRefresh: (newAccessToken: string) => void;
failRefresh: (error: string) => void;
setSessionTimeout: (timeout: number) => void;
}
export const useTokenStore = create<TokenStoreState & TokenStoreActions>(
subscribeWithSelector(
persist(
(set, get) => ({
// Initial state
accessToken: null,
refreshToken: null,
user: null,
isAuthenticated: false,
tokenExpiry: null,
isRefreshing: false,
refreshError: null,
lastLogin: null,
sessionTimeout: null,
// Actions
setTokens: (accessToken, refreshToken) => {
set({
accessToken,
refreshToken,
isAuthenticated: true,
lastLogin: new Date(),
});
},
clearTokens: () => {
set({
accessToken: null,
refreshToken: null,
user: null,
isAuthenticated: false,
tokenExpiry: null,
refreshError: null,
});
},
setUser: (user) => {
set({ user });
},
setTokenExpiry: (expiry) => {
set({ tokenExpiry: expiry });
},
startRefresh: () => {
set({ isRefreshing: true, refreshError: null });
},
completeRefresh: (newAccessToken) => {
set({
accessToken: newAccessToken,
isRefreshing: false,
refreshError: null,
});
},
failRefresh: (error) => {
set({
isRefreshing: false,
refreshError: error,
});
},
setSessionTimeout: (timeout) => {
set({ sessionTimeout: timeout });
},
}),
{
name: 'token-storage',
// Never persist tokens to localStorage/sessionStorage
partialize: (state) => ({
user: state.user,
lastLogin: state.lastLogin,
sessionTimeout: state.sessionTimeout,
}),
}
)
)
);
// Custom hook for token operations
export const useAuthTokens = () => {
const {
accessToken,
refreshToken,
isAuthenticated,
isRefreshing,
refreshError,
setTokens,
clearTokens,
startRefresh,
completeRefresh,
failRefresh,
} = useTokenStore();
const getAuthHeaders = () => {
if (!accessToken) return {};
return { Authorization: `Bearer ${accessToken}` };
};
const handleTokenRefresh = async (refreshToken: string) => {
try {
startRefresh();
// Simulate API call to refresh token
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');
}
const { accessToken: newAccessToken } = await response.json();
completeRefresh(newAccessToken);
return newAccessToken;
} catch (error) {
failRefresh(error instanceof Error ? error.message : 'Unknown refresh error');
clearTokens();
throw error;
}
};
return {
accessToken,
refreshToken,
isAuthenticated,
isRefreshing,
refreshError,
getAuthHeaders,
setTokens,
clearTokens,
handleTokenRefresh,
};
};
The critical-path hook provides:
- Secure token handling: Tokens never persisted to insecure storage
- Optimistic refresh: Immediate UI feedback during token refresh operations
- Error boundary: Clear error states and fallback behavior
- Type safety: Full TypeScript support for all token operations
- Session management: Comprehensive session lifecycle handling
The store uses Zustand's middleware to handle persistence only for non-sensitive session metadata, ensuring that actual tokens remain in memory-only storage for maximum security.
4. Routing & Navigation
Route Structure and Hierarchy
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
B --> B1[Landing Page]
B --> B2[Login]
B --> B3[Signup]
C --> C1[Dashboard]
C --> C2[Profile]
C --> C3[Settings]
C --> C4[Admin]
C1 --> C11[Analytics]
C1 --> C12[Reports]
The application follows a clear route hierarchy with protected and public routes. Public routes are accessible without authentication, while protected routes require valid session tokens to access.
Code Splitting Strategy
Route-based code splitting with dynamic imports:
// routes.tsx
import { lazy } from 'react';
const Dashboard = lazy(() => import('pages/Dashboard'));
const Login = lazy(() => import('pages/Login'));
const Profile = lazy(() => import('pages/Profile'));
export const routes = [
{
path: '/login',
element: <Login />,
public: true,
},
{
path: '/dashboard',
element: <Dashboard />,
protected: true,
},
{
path: '/profile',
element: <Profile />,
protected: true,
},
];
Justification:
- Critical routes like
/loginare loaded immediately for fast authentication flow - Protected dashboard pages are split to reduce initial bundle size
- Component-based splitting is used for feature-specific components within routes
Navigation Guards and Authentication Flows
// auth-guard.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from 'stores/auth';
export const AuthGuard = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
const { isAuthenticated, token } = useAuthStore();
useEffect(() => {
if (!isAuthenticated && !token) {
navigate('/login', { replace: true });
}
}, [isAuthenticated, token, navigate]);
return children;
};
Deep Linking Support and URL Parameter Design
// URL structure examples:
// /dashboard?tab=analytics&filter=date-range
// /profile/settings?section=security&mode=edit
// /admin/users?status=active&page=2
const useUrlParams = () => {
const searchParams = new URLSearchParams(window.location.search);
return {
tab: searchParams.get('tab') || 'overview',
filter: searchParams.get('filter') || 'all',
status: searchParams.get('status') || 'active',
page: parseInt(searchParams.get('page') || '1', 10),
};
};
Browser History Management and Scroll Restoration
// scroll-restoration.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export const ScrollRestoration = () => {
const location = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [location.pathname]);
return null;
};
// In App.tsx:
// <ScrollRestoration />
Authentication Redirect Strategy
// auth-redirect.tsx
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
export const AuthRedirect = () => {
const navigate = useNavigate();
const location = useLocation();
const { isAuthenticated } = useAuthStore();
useEffect(() => {
if (isAuthenticated && location.pathname === '/login') {
navigate('/dashboard', { replace: true });
}
}, [isAuthenticated, location.pathname, navigate]);
return null;
};
Route Protection Implementation
// ProtectedRoute.tsx
import { ReactNode } from 'react';
import { Navigate } from 'react-router-dom';
import { useAuthStore } from 'stores/auth';
interface ProtectedRouteProps {
children: ReactNode;
}
export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
const { isAuthenticated, token } = useAuthStore();
if (!isAuthenticated || !token) {
return <Navigate to="/login" replace />;
}
return children;
};
Route-Level Security Implementation
// security-guard.tsx
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from 'stores/auth';
export const SecurityGuard = ({
children,
requiredPermissions
}: {
children: React.ReactNode;
requiredPermissions?: string[];
}) => {
const navigate = useNavigate();
const { user, permissions } = useAuthStore();
const [isAuthorized, setIsAuthorized] = useState<boolean>(false);
useEffect(() => {
if (requiredPermissions && user) {
const hasPermission = requiredPermissions.every(
perm => permissions.includes(perm)
);
if (!hasPermission) {
navigate('/unauthorized', { replace: true });
return;
}
}
setIsAuthorized(true);
}, [requiredPermissions, user, navigate]);
if (!isAuthorized) {
return <div>Loading...</div>;
}
return children;
};
Route Configuration with Security
// app-routes.tsx
import { lazy } from 'react';
import { ProtectedRoute } from './components/ProtectedRoute';
import { SecurityGuard } from './components/SecurityGuard';
const Dashboard = lazy(() => import('pages/Dashboard'));
const Login = lazy(() => import('pages/Login'));
const Profile = lazy(() => import('pages/Profile'));
const Admin = lazy(() => import('pages/Admin'));
export const appRoutes = [
{
path: '/login',
element: <Login />,
public: true,
},
{
path: '/dashboard',
element: (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
),
protected: true,
},
{
path: '/admin',
element: (
<ProtectedRoute>
<SecurityGuard requiredPermissions={['admin']}>
<Admin />
</SecurityGuard>
</ProtectedRoute>
),
protected: true,
},
];
Token Refresh Strategy in Navigation
// token-refresh-guard.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from 'stores/auth';
export const TokenRefreshGuard = () => {
const navigate = useNavigate();
const { token, refreshToken, refresh } = useAuthStore();
useEffect(() => {
const checkTokenExpiry = async () => {
if (token && refreshToken) {
try {
const isTokenExpired = await checkIfTokenExpired(token);
if (isTokenExpired) {
const newToken = await refresh();
if (!newToken) {
navigate('/login', { replace: true });
}
}
} catch (error) {
navigate('/login', { replace: true });
}
}
};
checkTokenExpiry();
}, [token, refreshToken, navigate, refresh]);
return null;
};
Performance Considerations for Navigation
// navigation-performance.ts
const useNavigationPerformance = () => {
const [navigationMetrics, setNavigationMetrics] = useState({
routeChangeTime: 0,
loadTime: 0,
});
const handleRouteChange = (from: string, to: string) => {
const startTime = performance.now();
// Track route change timing
const metrics = {
from,
to,
duration: performance.now() - startTime,
};
// Log to monitoring service
reportNavigationMetrics(metrics);
};
return { navigationMetrics, handleRouteChange };
};
Error Handling in Navigation
// navigation-error-handler.tsx
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { captureException } from '@sentry/react';
export const NavigationErrorHandler = () => {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const handleError = (error: Error) => {
captureException(error);
navigate('/error', {
state: {
error,
path: location.pathname
}
});
};
// Add global error listener for navigation
window.addEventListener('error', handleError);
return () => {
window.removeEventListener('error', handleError);
};
}, [navigate, location.pathname]);
return null;
};
5. API Integration Layer
API Client Design Pattern
For secure token storage, we'll implement a robust API client with proper security considerations. The approach will use a custom fetch wrapper that handles authentication tokens, request/response interception, and secure storage patterns.
// api/client.ts
import { AuthToken } from '../types/auth';
class ApiClient {
private baseUrl: string;
private tokenStorageKey: string = 'auth_token';
private tokenType: string = 'bearer';
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private async getAuthToken(): Promise<AuthToken | null> {
try {
const tokenString = sessionStorage.getItem(this.tokenStorageKey);
if (!tokenString) return null;
const token: AuthToken = JSON.parse(tokenString);
return this.isTokenValid(token) ? token : null;
} catch (error) {
console.error('Failed to retrieve auth token:', error);
return null;
}
}
private isTokenValid(token: AuthToken): boolean {
if (!token.expiresAt) return false;
return new Date(token.expiresAt) > new Date();
}
private async setAuthToken(token: AuthToken): Promise<void> {
try {
const tokenString = JSON.stringify(token);
sessionStorage.setItem(this.tokenStorageKey, tokenString);
} catch (error) {
console.error('Failed to store auth token:', error);
throw new Error('Authentication storage failed');
}
}
private async clearAuthToken(): Promise<void> {
try {
sessionStorage.removeItem(this.tokenStorageKey);
} catch (error) {
console.error('Failed to clear auth token:', error);
}
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const token = await this.getAuthToken();
const headers = new Headers(options.headers || {});
if (token) {
headers.set('Authorization', `${this.tokenType} ${token.value}`);
}
const config: RequestInit = {
...options,
headers: headers,
};
const response = await fetch(url, config);
if (response.status === 401) {
// Token expired or invalid, clear it
await this.clearAuthToken();
throw new Error('Authentication required');
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP ${response.status}`);
}
return response.json();
}
async login(credentials: { username: string; password: string }): Promise<AuthToken> {
const response = await this.request<{ token: AuthToken }>(
'/auth/login',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
}
);
await this.setAuthToken(response.token);
return response.token;
}
async logout(): Promise<void> {
await this.clearAuthToken();
}
async refresh(): Promise<AuthToken> {
const token = await this.getAuthToken();
if (!token) throw new Error('No token available for refresh');
const response = await this.request<{ token: AuthToken }>(
'/auth/refresh',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `${this.tokenType} ${token.value}`
}
}
);
await this.setAuthToken(response.token);
return response.token;
}
async get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint);
}
async post<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
async put<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, {
method: 'DELETE',
});
}
}
export const apiClient = new ApiClient(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000');
Request/Response Interceptors
The API client implements request/response interceptors to handle authentication tokens, response normalization, and error transformation with security considerations:
- Request Interception: Automatically injects authentication tokens
- Response Interception: Handles token expiration and refresh logic
- Error Transformation: Normalizes API errors for consistent handling
Error Handling and Retry Strategy
// 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 = async (error: any): Promise<ApiError> => {
if (error instanceof ApiError) {
return error;
}
if (error.response) {
const { status, data } = error.response;
return new ApiError(
status,
data.message || `API Error: ${status}`,
data.code
);
}
return new ApiError(500, 'Network error', 'NETWORK_ERROR');
};
// Retry logic for transient failures
export const retryWithBackoff = async <T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> => {
let lastError: any;
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i === maxRetries) break;
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, i) + Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
};
Loading, Error, and Empty States Design
The API integration layer handles all state management through React Query patterns:
// hooks/useApi.ts
import { useQuery, useMutation, UseQueryOptions } from '@tanstack/react-query';
import { apiClient } from '../api/client';
// Query keys for cache invalidation
export const QUERY_KEYS = {
USER_PROFILE: ['user', 'profile'],
USER_TOKENS: ['user', 'tokens'],
AUTH_STATUS: ['auth', 'status']
};
// API query hooks with proper error handling
export const useUserProfile = (options?: UseQueryOptions<UserProfile>) => {
return useQuery<UserProfile>({
queryKey: QUERY_KEYS.USER_PROFILE,
queryFn: () => apiClient.get<UserProfile>('/user/profile'),
...options
});
};
export const useUserTokens = () => {
return useQuery<Token[]>({
queryKey: QUERY_KEYS.USER_TOKENS,
queryFn: () => apiClient.get<Token[]>('/user/tokens'),
staleTime: 5 * 60 * 1000 // 5 minutes
});
};
export const useAuthMutation = () => {
return useMutation({
mutationFn: async (credentials: { username: string; password: string }) => {
return apiClient.login(credentials);
},
onSuccess: (token) => {
// Token is automatically stored in session storage
console.log('Authentication successful');
},
onError: (error) => {
// Handle authentication errors
console.error('Authentication failed:', error);
}
});
};
Data Fetching Strategy
For secure token storage, we use a hybrid CSR/SSR approach:
- CSR (Client-Side Rendering) - Primary for authenticated routes
- SSR (Server-Side Rendering) - For public pages that don't require auth
- SSG (Static Site Generation) - For content that never changes
The strategy prioritizes:
- Public pages: SSG for performance
- Authenticated routes: CSR with proper token handling
- User-specific data: Client-side fetching with proper caching
API Response Transformation Layer
// api/transformers.ts
interface RawAuthResponse {
token: string;
expiresAt: string;
refreshToken?: string;
}
interface AuthToken {
value: string;
expiresAt: Date;
refreshToken?: string;
}
export const transformAuthResponse = (raw: RawAuthResponse): AuthToken => {
return {
value: raw.token,
expiresAt: new Date(raw.expiresAt),
refreshToken: raw.refreshToken
};
};
// Domain model mapping for token management
export interface Token {
id: string;
name: string;
type: 'access' | 'refresh';
createdAt: Date;
expiresAt: Date;
lastUsed?: Date;
}
Sequence Diagram for Authentication Flow
sequenceDiagram
participant U as User
participant C as Client (Browser)
participant A as Auth API
participant T as Token Manager
U->>C: Login with credentials
C->>A: POST /auth/login
A->>C: 200 OK with token
C->>T: Store token in secure session storage
T->>C: Token stored successfully
C->>A: GET /user/profile
A->>C: 200 OK with user data
C->>A: POST /auth/refresh
A->>C: 200 OK with new token
C->>T: Update token in session storage
C->>A: DELETE /auth/logout
A->>C: 200 OK
C->>T: Remove token from session storage
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for all routes
- First Input Delay (FID): <100ms for interactive elements
- Cumulative Layout Shift (CLS): <0.1 for all user interactions
Bundle Size Budget per Route
- Home/Overview: <200KB (including all dependencies)
- Auth Flow: <150KB (minimal auth-specific bundles)
- Token Management: <100KB (core token handling logic)
- Settings/Profile: <120KB (configuration and user data)
Critical Rendering Path Optimization
- Critical CSS: Inline essential styles for immediate rendering
- Resource Hints: Preload key fonts and critical assets
- Code Splitting: Route-based chunking with dynamic imports
- Lazy Loading: Non-critical components loaded on demand
Image Optimization Strategy
- Formats: WebP for modern browsers, JPEG fallback for legacy
- Lazy Loading:
loading="lazy"attribute on all images - Responsive Images:
srcsetwith multiple sizes for different viewports - Compression: Server-side optimization with progressive loading
Font Loading Strategy
- Font Display:
font-display: swapto prevent FOIT - Preload: Critical font files preloaded via
<link rel="preload"> - Fallback Fonts: System fonts as fallback to prevent flash of invisible text
- Subset Loading: Only load characters needed for current view
Memoization Strategy
// Example memoization patterns for token management components
const TokenCard = React.memo(({ token, onRevoke }: TokenCardProps) => {
const handleRevoke = useCallback(() => {
onRevoke(token.id);
}, [token.id, onRevoke]);
return (
<div className="token-card">
<span>{token.name}</span>
<button onClick={handleRevoke}>Revoke</button>
</div>
);
});
const TokenList = React.memo(({ tokens, onRevoke }: TokenListProps) => {
const sortedTokens = useMemo(() =>
[...tokens].sort((a, b) => a.createdAt - b.createdAt),
[tokens]
);
return (
<div className="token-list">
{sortedTokens.map(token => (
<TokenCard
key={token.id}
token={token}
onRevoke={onRevoke}
/>
))}
</div>
);
});
Virtualization for Large Lists
For token lists with hundreds of entries:
import { FixedSizeList as List } from 'react-window';
const TokenRow = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const token = tokens[index];
return (
<div style={style} className="token-row">
{token.name}
</div>
);
};
const VirtualizedTokenList = () => {
return (
<List
height={600}
itemCount={tokens.length}
itemSize={50}
width="100%"
>
{TokenRow}
</List>
);
};
Prefetching and Preloading Strategy
- Token Refresh: Auto-refresh tokens 5 minutes before expiration (prefetch)
- Critical Route Preload: When user is on dashboard, preload token management route
- Service Worker: Cache frequently accessed token metadata for offline access
- Background Sync: Re-fetch tokens when network becomes available after offline period
Performance Monitoring Implementation
// Performance tracking for token operations
const useTokenPerformance = () => {
const [metrics, setMetrics] = useState<TokenMetrics>({
loadTime: 0,
refreshCount: 0,
errorCount: 0
});
const trackTokenLoad = (duration: number) => {
setMetrics(prev => ({
...prev,
loadTime: prev.loadTime + duration,
refreshCount: prev.refreshCount + 1
}));
};
return { metrics, trackTokenLoad };
};
Memory Management Considerations
- Token Cleanup: Clear expired tokens from memory after 24 hours
- WeakMap Usage: Store token metadata in WeakMaps to prevent memory leaks
- Garbage Collection: Explicitly nullify token references when revoked
- Event Listener Cleanup: Remove all event listeners on component unmount
Bundle Analysis Strategy
- Webpack Bundle Analyzer: Run analysis for each route's bundle
- Tree Shaking: Ensure unused token utilities are excluded from bundles
- Deduplication: Prevent duplicate token handling logic across modules
- Code Splitting: Separate auth and token management into distinct bundles
Caching Strategy
- Memory Cache: Keep recent tokens in memory for 5 minutes
- Session Storage: Persist tokens across page refreshes but not browser sessions
- Cache Invalidation: Clear token cache on logout or token revocation
- Cache TTL: Implement time-based expiration for cached tokens
Rendering Optimization
- React.memo: Apply to all token display components
- useCallback: Memoize event handlers that depend on token data
- useMemo: Cache computed token properties (e.g., expiration status)
- Component Splitting: Separate token creation from display components
Accessibility Performance Considerations
- Focus Management: Ensure keyboard navigation through token lists doesn't cause re-renders
- ARIA Updates: Use
aria-livefor token status changes without full re-renders - Screen Reader Optimization: Minimize DOM changes that affect screen reader announcements
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. The placement strategy follows these principles:
- Application-level error boundary - Covers entire app to catch unexpected runtime errors
- Feature-specific boundaries - Wrap critical feature components (e.g., dashboard, forms)
- Component-level boundaries - For individual UI elements that might fail independently (e.g., charts, tables)
graph TD
A[App] --> B[ErrorBoundary]
B --> C[Dashboard]
B --> D[UserForm]
B --> E[Navigation]
C --> F[DataGrid]
C --> G[Chart]
D --> H[InputField]
D --> I[SubmitButton]
Global Error Handler Design
A centralized error handler manages uncaught exceptions and provides graceful degradation:
// ErrorService.ts
interface ErrorHandlerConfig {
onError: (error: Error, errorInfo: React.ErrorInfo) => void;
onNetworkError: () => void;
onAuthError: () => void;
}
class ErrorService {
private config: ErrorHandlerConfig;
constructor(config: ErrorHandlerConfig) {
this.config = config;
this.setupGlobalHandlers();
}
private setupGlobalHandlers() {
// Handle uncaught errors
window.addEventListener('error', (event) => {
this.handleGlobalError(event.error);
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.handleGlobalError(event.reason);
});
}
handleGlobalError(error: Error, errorInfo?: React.ErrorInfo) {
if (this.isNetworkError(error)) {
this.config.onNetworkError();
return;
}
if (this.isAuthError(error)) {
this.config.onAuthError();
return;
}
this.config.onError(error, errorInfo);
}
private isNetworkError(error: Error): boolean {
return (
error.message.includes('network') ||
error.message.includes('fetch') ||
error.name === 'TypeError'
);
}
private isAuthError(error: Error): boolean {
return error.message.includes('401') || error.message.includes('Unauthorized');
}
}
Offline / Network Failure Handling
Offline resilience is implemented through:
- Network status detection using
navigator.onLine - Retry mechanisms with exponential backoff
- Local storage fallbacks for critical operations
// NetworkService.ts
class NetworkService {
private isOnline = navigator.onLine;
private retryCount = 0;
private maxRetries = 3;
constructor() {
this.setupNetworkListeners();
}
setupNetworkListeners() {
window.addEventListener('online', () => {
this.isOnline = true;
this.retryFailedRequests();
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
}
async retryFailedRequests() {
const pendingRequests = this.getPendingRequests();
for (const request of pendingRequests) {
await this.retryRequest(request);
}
}
async retryRequest(request: PendingRequest) {
if (this.retryCount < this.maxRetries) {
try {
await this.executeRequest(request);
this.removePendingRequest(request.id);
this.retryCount = 0;
} catch (error) {
this.retryCount++;
setTimeout(() => this.retryRequest(request), 1000 * Math.pow(2, this.retryCount));
}
}
}
getPendingRequests(): PendingRequest[] {
const pending = localStorage.getItem('pendingRequests');
return pending ? JSON.parse(pending) : [];
}
}
Graceful Degradation Patterns
When critical services are unavailable, the system provides fallback experiences:
// FallbackUI.tsx
interface FallbackProps {
type: 'network' | 'auth' | 'data';
onRetry?: () => void;
}
const FallbackUI: React.FC<FallbackProps> = ({ type, onRetry }) => {
const getFallbackContent = () => {
switch (type) {
case 'network':
return (
<div className="fallback-network">
<h3>Network Unavailable</h3>
<p>Please check your connection and try again</p>
{onRetry && (
<button onClick={onRetry} className="retry-button">
Retry Connection
</button>
)}
</div>
);
case 'auth':
return (
<div className="fallback-auth">
<h3>Authentication Required</h3>
<p>Please sign in to continue</p>
{onRetry && (
<button onClick={onRetry} className="login-button">
Sign In
</button>
)}
</div>
);
default:
return (
<div className="fallback-generic">
<h3>Service Unavailable</h3>
<p>We're experiencing technical difficulties</p>
</div>
);
}
};
return <div className="fallback-container">{getFallbackContent()}</div>;
};
User Feedback on Errors
Comprehensive error feedback mechanisms:
// ToastService.ts
interface ToastConfig {
message: string;
type: 'error' | 'warning' | 'success' | 'info';
duration?: number;
}
class ToastService {
private toasts: Toast[] = [];
show(config: ToastConfig) {
const toast: Toast = {
id: Date.now(),
...config,
duration: config.duration || 5000
};
this.toasts.push(toast);
this.render();
if (toast.duration > 0) {
setTimeout(() => this.dismiss(toast.id), toast.duration);
}
}
dismiss(id: number) {
this.toasts = this.toasts.filter(toast => toast.id !== id);
this.render();
}
private render() {
// Render toast notifications using React Portal
}
}
// Usage in components
const handleNetworkError = () => {
ToastService.show({
message: 'Network connection lost. Attempting to reconnect...',
type: 'warning'
});
};
Error Boundary Implementation
// ErrorBoundary.tsx
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
errorInfo?: React.ErrorInfo;
}
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
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) {
// Log error to monitoring service
console.error('ErrorBoundary caught error:', error, errorInfo);
// Send to error tracking service (e.g., Sentry)
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { react: errorInfo }
});
}
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error?.toString()}
<br />
{this.state.errorInfo?.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
Offline Storage Fallback Strategy
Critical data operations are persisted to local storage when offline:
// OfflineStorageService.ts
class OfflineStorageService {
private storageKey = 'offline_data';
async saveToOfflineStorage(data: any, operation: string) {
const offlineData = this.getOfflineData();
const timestamp = Date.now();
const operationEntry = {
id: `${operation}_${timestamp}`,
operation,
data,
timestamp,
status: 'pending'
};
offlineData.push(operationEntry);
localStorage.setItem(this.storageKey, JSON.stringify(offlineData));
}
async processOfflineOperations() {
const offlineData = this.getOfflineData();
for (const entry of offlineData) {
if (entry.status === 'pending') {
try {
// Process the operation
await this.executeOfflineOperation(entry);
entry.status = 'completed';
} catch (error) {
entry.status = 'failed';
console.error('Failed to process offline operation:', error);
}
}
}
this.saveOfflineData(offlineData);
}
private getOfflineData(): OfflineOperation[] {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
}
private saveOfflineData(data: OfflineOperation[]) {
localStorage.setItem(this.storageKey, JSON.stringify(data));
}
}
interface OfflineOperation {
id: string;
operation: string;
data: any;
timestamp: number;
status: 'pending' | 'completed' | 'failed';
}
Retry Strategy Implementation
// RetryService.ts
class RetryService {
static async executeWithRetry<T>(
operation: () => Promise<T>,
maxRetries = 3,
delay = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries) {
// Exponential backoff
await this.delay(delay * Math.pow(2, attempt));
continue;
}
throw lastError;
}
}
throw lastError;
}
private static delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
8. Testing Strategy
Unit Testing Approach
Component Tests (70% coverage target):
- Test all interactive components with various prop combinations
- Verify proper rendering under different state conditions (loading, error, success)
- Test edge cases like empty states, invalid inputs, and boundary conditions
- Use React Testing Library with Jest for DOM-focused testing
Utility Tests (85% coverage target):
- Test token validation functions with valid/invalid formats
- Verify secure storage operations (set, get, remove) work correctly
- Test encryption/decryption logic with edge cases
- Validate error handling paths in utility functions
Hook Tests (90% coverage target):
- Test custom token hooks with mocked storage and API responses
- Verify proper state transitions and side-effect execution
- Test hook cleanup behavior and memory leak prevention
Integration Testing Strategy
API Integration Tests (50% coverage):
- Test token refresh flow with mocked backend responses
- Verify proper error handling when token refresh fails
- Validate session expiration and automatic logout behavior
- Test concurrent request handling with token refresh
Storage Integration Tests (100% coverage):
- Test cross-browser storage compatibility (localStorage vs. sessionStorage)
- Verify secure storage fallback mechanisms
- Validate token persistence across page reloads and browser sessions
- Test storage size limits and cleanup behavior
E2E Testing Scope
Cypress-based End-to-End Tests (30% coverage):
- User authentication flow from login to token storage and refresh
- Session persistence across browser tabs and windows
- Token invalidation and automatic logout on server-side expiration
- Secure storage validation in browser developer tools (no raw tokens visible)
- Cross-origin security boundary testing
Critical User Journeys:
- Login → Token storage → Session persistence
- Token refresh → Concurrent request handling
- Session timeout → Automatic logout and re-authentication
- Token invalidation → Server-side session cleanup
Visual Regression Testing
Screenshot Testing:
- Test token-related UI states (loading, error, success)
- Validate secure storage UI elements don't leak sensitive information
- Monitor for visual regressions in authentication flows
Accessibility Testing Automation
Axe-core Integration:
- Test token storage UI for proper ARIA attributes and keyboard navigation
- Verify error messages are announced to screen readers
- Ensure no focus traps in token-related modals or forms
Test Data Strategy
Test Fixtures:
- Mock token payloads with various expiration times (valid, expired, near-expiration)
- Create test scenarios for different storage types (localStorage, sessionStorage, secure cookies)
- Generate test data for edge cases like malformed tokens, missing claims, and invalid signatures
Test Environment Isolation:
- Use separate test storage namespaces to prevent cross-test contamination
- Mock all external dependencies (API calls, browser APIs) for consistent test results
- Implement clean test state between individual tests using beforeEach/afterEach hooks
Test Coverage Targets
Code Coverage:
- Component unit tests: 70% line coverage, 60% branch coverage
- Utility function tests: 85% line coverage, 80% branch coverage
- Hook integration tests: 90% line coverage, 85% branch coverage
- E2E user flows: 30% end-to-end coverage with critical paths
Security Test Coverage:
- Token storage security: 100% coverage of secure implementation paths
- XSS prevention: 100% coverage of input sanitization and DOM manipulation
- Session management: 100% coverage of authentication flow edge cases
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The token storage system must ensure accessibility compliance across all user interactions and UI elements. The following WCAG 2.1 AA success criteria are relevant:
- 1.1.1 Non-text Content (Level A) - All UI elements must have appropriate text alternatives or be implemented with accessible markup
- 1.3.1 Info and Relationships (Level A) - Proper semantic HTML structure must be maintained for token display and management
- 1.3.2 Meaningful Sequence (Level AA) - Logical reading order must be preserved in token lists and forms
- 1.4.1 Use of Color (Level A) - Token state indicators must be distinguishable by more than color alone
- 1.4.3 Contrast (Level AA) - Text and UI elements must meet minimum contrast ratios (4.5:1 for normal text)
- 2.1.1 Keyboard (Level A) - All token management actions must be operable via keyboard navigation
- 2.4.1 Bypass Blocks (Level A) - Clear navigation structure must exist for screen reader users
- 3.1.1 Language (Level A) - Content language must be programmatically determinable
Keyboard navigation support
All token storage operations must be fully keyboard accessible:
- Tab Navigation: Sequential focus order through token list items, action buttons, and form fields
- Enter/Space: Activation of interactive elements (delete tokens, refresh buttons)
- Escape: Close modals and cancel operations
- Arrow Keys: Navigation within token lists (when applicable)
- Home/End: Fast navigation to first/last tokens in a list
Focus management strategy
Focus handling is critical for token storage UI:
stateDiagram-v2
[*] --> Idle
Idle --> Editing : User focuses on token field
Editing --> Saved : Enter pressed or focus lost
Editing --> Cancelled : Escape pressed
Saved --> Idle : Focus returned to list
Cancelled --> Idle : Focus returned to list
- Focus Trapping: When modals open for token creation/editing, focus is trapped within the modal
- Focus Restoration: After modal close, focus returns to the previous context (token list or form)
- Focus Indicators: Clear visual focus states using CSS
:focus-visiblefor keyboard users
ARIA roles and attributes plan
// TokenList component ARIA implementation
interface TokenListProps {
tokens: Token[];
onTokenDelete: (id: string) => void;
onTokenRefresh: (id: string) => void;
}
// TokenItem component ARIA implementation
interface TokenItemProps {
token: Token;
onDelete: () => void;
onRefresh: () => void;
isFocused?: boolean;
}
ARIA roles and attributes for token storage UI:
role="list"on token container elementsrole="listitem"on individual token itemsaria-labelfor actionable buttons (delete, refresh)aria-describedbyfor token descriptions or status messagesaria-live="polite"for status updates (token created/deleted)aria-expandedfor token details toggle buttonsaria-current="page"for active token in navigation
Screen reader compatibility
Token storage UI must be fully compatible with screen readers:
- Descriptive Labels: All buttons and controls have meaningful labels
- Status Announcements: Token operations announce success/failure to screen readers
- Dynamic Content Updates: New tokens or status changes are announced without requiring focus repositioning
- Form Field Labels: All input fields have associated labels for screen reader users
- Error Messages: Validation errors are announced with clear context and suggestions
Example token list structure for screen readers:
<ul role="list" aria-label="Security tokens">
<li role="listitem">
<div>
<span aria-label="Token name: API Key">API Key</span>
<span aria-label="Token status: Active">●</span>
<button aria-label="Delete token API Key" ...>Delete</button>
<button aria-label="Refresh token API Key" ...>Refresh</button>
</div>
</li>
</ul>
Additional accessibility considerations
- High Contrast Mode Support: UI elements must maintain visibility in Windows High Contrast mode
- Zoom Compatibility: Token lists and forms must remain usable at 200% zoom
- Alternative Input Methods: Support for voice control and switch access
- Language Attributes: Proper
langattributes on content elements when mixed language is present - Skip Links: For direct navigation to token list from page header
Testing approach
Accessibility testing will include:
- Automated Testing: axe-core integration in CI pipeline for WCAG compliance checks
- Manual Keyboard Testing: Full end-to-end keyboard navigation through all token operations
- Screen Reader Testing: JAWS and NVDA testing for token list navigation and form interactions
- Color Contrast Analysis: Automated contrast ratio validation using axe-core
- Focus Order Validation: Manual verification of logical tab order through all token management flows
10. Security Considerations
XSS Prevention Strategy
Content Sanitization: All user-generated content must be sanitized before rendering to prevent script injection. Using DOMPurify for HTML sanitization:
import DOMPurify from 'dompurify';
const sanitizeHTML = (html: string): string => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em'],
ALLOWED_ATTR: [],
});
};
Input Validation: All user inputs must be validated server-side and client-side with strict sanitization:
const validateInput = (input: string): boolean => {
// Reject inputs containing script tags or other dangerous patterns
const dangerousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/on\w+\s*=\s*["'][^"']*["']/gi,
];
return !dangerousPatterns.some(pattern => pattern.test(input));
};
CSRF Protection
CSRF Token Implementation: All authenticated requests must include a CSRF token in headers:
// Token storage and injection
const CSRF_TOKEN_KEY = 'csrf_token';
const getCSRFToken = (): string | null => {
return localStorage.getItem(CSRF_TOKEN_KEY);
};
const setCSRFToken = (token: string): void => {
localStorage.setItem(CSRF_TOKEN_KEY, token);
};
// API client with automatic CSRF header injection
const apiClient = axios.create({
baseURL: '/api',
withCredentials: true,
});
apiClient.interceptors.request.use((config) => {
const token = getCSRFToken();
if (token && config.headers) {
config.headers['X-CSRF-Token'] = token;
}
return config;
});
Content Security Policy (CSP) Headers
CSP Configuration: Implement strict CSP headers to prevent XSS and data injection:
// Example CSP header configuration (to be set server-side)
const CSP_POLICY = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Note: minimal unsafe for legacy support
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' wss:",
"frame-ancestors 'none'",
"base-uri 'self'",
].join('; ');
// Server-side header
res.setHeader('Content-Security-Policy', CSP_POLICY);
Sensitive Data Handling in Frontend State
Token Isolation: JWT tokens are never stored in localStorage or sessionStorage due to XSS risks. Instead, use secure HTTP-only cookies:
// Secure token handling with cookie-based approach
const TOKEN_COOKIE_NAME = 'auth_token';
const getSecureToken = (): string | null => {
// Read from cookie using document.cookie (limited access)
const name = `${TOKEN_COOKIE_NAME}=`;
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return null;
};
const clearSecureToken = (): void => {
// Clear cookie with secure flags
document.cookie = `${TOKEN_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
};
Memory Management: Tokens are stored in memory and cleared on logout:
// In-memory token storage for sensitive data
let currentToken: string | null = null;
const setTokenInMemory = (token: string): void => {
currentToken = token;
};
const getTokenFromMemory = (): string | null => {
return currentToken;
};
const clearTokenFromMemory = (): void => {
currentToken = null;
};
Input Sanitization Approach
Comprehensive Input Validation: Implement strict input sanitization across all user-facing forms:
// Input sanitizer for form fields
const sanitizeInput = (input: string): string => {
// Remove potentially dangerous characters
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
};
// Sanitize form data before submission
const sanitizeFormData = (formData: Record<string, any>): Record<string, any> => {
const sanitized = {};
Object.keys(formData).forEach(key => {
if (typeof formData[key] === 'string') {
sanitized[key] = sanitizeInput(formData[key]);
} else {
sanitized[key] = formData[key];
}
});
return sanitized;
};
Token Storage Strategy
Secure Cookie-Based Authentication: For production, tokens are stored in secure HTTP-only cookies:
// Token storage strategy with fallbacks
const STORAGE_STRATEGIES = {
COOKIE: 'cookie',
MEMORY: 'memory',
LOCAL_STORAGE: 'local_storage',
};
// Determine best storage method based on environment
const getStorageStrategy = (): string => {
// In production, prefer cookies for security
if (process.env.NODE_ENV === 'production') {
return STORAGE_STRATEGIES.COOKIE;
}
// For development, allow localStorage with warnings
return STORAGE_STRATEGIES.LOCAL_STORAGE;
};
// Token manager for different storage methods
class TokenManager {
static setToken(token: string, strategy?: string): void {
const storage = strategy || getStorageStrategy();
switch (storage) {
case STORAGE_STRATEGIES.COOKIE:
document.cookie = `auth_token=${token}; path=/; secure; samesite=strict`;
break;
case STORAGE_STRATEGIES.MEMORY:
setTokenInMemory(token);
break;
case STORAGE_STRATEGIES.LOCAL_STORAGE:
localStorage.setItem('auth_token', token);
break;
}
}
static getToken(strategy?: string): string | null {
const storage = strategy || getStorageStrategy();
switch (storage) {
case STORAGE_STRATEGIES.COOKIE:
return getSecureToken();
case STORAGE_STRATEGIES.MEMORY:
return getTokenFromMemory();
case STORAGE_STRATEGIES.LOCAL_STORAGE:
return localStorage.getItem('auth_token');
default:
return null;
}
}
static clearToken(strategy?: string): void {
const storage = strategy || getStorageStrategy();
switch (storage) {
case STORAGE_STRATEGIES.COOKIE:
clearSecureToken();
break;
case STORAGE_STRATEGIES.MEMORY:
clearTokenFromMemory();
break;
case STORAGE_STRATEGIES.LOCAL_STORAGE:
localStorage.removeItem('auth_token');
break;
}
}
}
Session Management
Session Timeout and Renewal: Implement automatic session timeout with renewal capability:
// Session management for token lifecycle
class SessionManager {
private static sessionTimeout: number = 30 * 60 * 1000; // 30 minutes
private static refreshThreshold: number = 5 * 60 * 1000; // 5 minutes
static isSessionActive(): boolean {
const lastActivity = localStorage.getItem('last_activity');
if (!lastActivity) return false;
const now = Date.now();
const last = parseInt(lastActivity, 10);
return (now - last) < this.sessionTimeout;
}
static extendSession(): void {
localStorage.setItem('last_activity', Date.now().toString());
}
static handleSessionExpiry(): void {
// Clear tokens and redirect to login
TokenManager.clearToken();
window.location.href = '/login';
}
}
Security Headers Implementation
Security Header Configuration: Implement comprehensive security headers:
// Security header configuration for API responses
const SECURITY_HEADERS = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Referrer-Policy': 'strict-origin-when-cross-origin',
};
// Apply headers to API responses (server-side)
const applySecurityHeaders = (res: Response): void => {
Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
res.setHeader(key, value);
});
};
Token Rotation and Refresh
Secure Token Rotation: Implement refresh token rotation for enhanced security:
// Token rotation strategy with refresh tokens
const handleTokenRotation = async (): Promise<void> => {
try {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
const response = await apiClient.post('/auth/refresh', {
refresh_token: refreshToken,
});
const { access_token, refresh_token } = response.data;
// Store new tokens securely
TokenManager.setToken(access_token);
localStorage.setItem('refresh_token', refresh_token);
// Update token in API client
apiClient.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
}
} catch (error) {
// Handle refresh failure - redirect to login
TokenManager.clearToken();
window.location.href = '/login';
}
};
Security Testing Considerations
Vulnerability Scanning: Integration with security scanning tools:
// Security testing configuration
const SECURITY_TESTING = {
XSS: true,
CSRF: true,
CORS: true,
HEADER_INJECTION: true,
};
// Security audit hook for development
const runSecurityAudit = (): void => {
if (process.env.NODE_ENV === 'development') {
console.warn('Security audit in development mode');
// Run security checks
}
};
Risk Mitigation
Security Risk Assessment: Regular risk assessment and mitigation:
- Token Exposure: Use secure cookies in production, memory storage in development
- Session Hijacking: Implement session timeout and renewal
- Cross-Site Scripting: Comprehensive input sanitization and CSP
- Data Leakage: Secure token handling with proper scope management
The design prioritizes security through multiple layers of protection, with clear separation between secure and insecure storage methods based on environment and security requirements.
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": {
"title": "Login",
"email_label": "Email Address",
"password_label": "Password",
"submit_button": "Sign In"
},
"logout": {
"confirm_message": "Are you sure you want to log out?",
"confirm_button": "Logout"
}
},
"common": {
"actions": {
"save": "Save",
"cancel": "Cancel"
}
}
}
Keys will use lowercase snake_case with descriptive names that clearly indicate context and usage. Keys will be grouped by feature areas to prevent naming conflicts.
RTL Support Considerations
RTL (Right-to-Left) language support will be implemented through CSS classes and React context:
// i18n-context.tsx
import { createContext, useContext } from 'react';
interface I18nContextType {
locale: string;
direction: 'ltr' | 'rtl';
translations: Record<string, any>;
}
const I18nContext = createContext<I18nContextType>({
locale: 'en',
direction: 'ltr',
translations: {}
});
export const useI18n = () => useContext(I18nContext);
// RTL-aware component wrapper
const RtlAwareComponent = ({ children }: { children: React.ReactNode }) => {
const { direction } = useI18n();
return (
<div
className={`rtl-${direction}`}
dir={direction}
lang={useI18n().locale}
>
{children}
</div>
);
};
Date, Number, Currency Formatting Strategy
All date, number, and currency formatting will use the Intl API with proper locale handling:
// format-utils.ts
export const formatDate = (date: Date, locale: string): string => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
};
export const formatCurrency = (amount: number, locale: string, currency: string): string => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
};
export const formatNumber = (num: number, locale: string): string => {
return new Intl.NumberFormat(locale).format(num);
};
Locale Detection and Switching Strategy
Automatic locale detection will be implemented using browser navigator settings:
// i18n-service.ts
class I18nService {
private supportedLocales = ['en', 'es', 'fr', 'ar'];
detectLocale(): string {
const browserLang = navigator.language.split('-')[0];
return this.supportedLocales.includes(browserLang) ? browserLang : 'en';
}
switchLocale(locale: string): void {
if (this.supportedLocales.includes(locale)) {
localStorage.setItem('preferred-locale', locale);
window.location.reload();
}
}
getPreferredLocale(): string {
return localStorage.getItem('preferred-locale') || this.detectLocale();
}
}
The system will:
- Detect user's browser language as default
- Allow manual override via UI settings
- Persist preferred locale in localStorage with fallback to browser language
- Support automatic RTL detection for RTL languages (Arabic, Hebrew)
- Provide a fallback to English for unsupported locales
Implementation Considerations
-
Translation Key Management: Keys will be extracted automatically using tools like
i18next-parserto maintain consistency and prevent orphaned keys. -
Fallback Strategy: All translations will have English as fallback with proper error logging for missing keys.
-
Dynamic Imports: Language files will be loaded dynamically to reduce initial bundle size:
// lazy-load-translation.ts
const loadTranslations = async (locale: string) => {
try {
const translations = await import(`../locales/${locale}/translation.json`);
return translations.default;
} catch (error) {
console.warn(`Failed to load translations for ${locale}`, error);
return {};
}
};
-
Type Safety: TypeScript interfaces will be generated from translation files to ensure type safety and prevent runtime errors.
-
Performance: Translation keys will be cached in memory to avoid repeated lookups and reduce DOM rendering time.
12. Monitoring & Observability
Real User Monitoring (RUM) metrics to track
Core Performance Metrics:
- LCP (Largest Contentful Paint): Target <2.5s for 90% of users, with 4s as threshold for poor performance
- FID (First Input Delay): Target <100ms for 90% of users, with 200ms as threshold for poor performance
- CLS (Cumulative Layout Shift): Target <0.1 for 90% of users, with 0.25 as threshold for poor performance
Custom RUM Events:
- Token refresh success/failure rates
- Authentication flow completion rates (login, signup, reset)
- Session duration and idle time distribution
- Secure storage access patterns (read/write operations)
- Error rate by token type (access vs refresh)
Error tracking integration
Primary Integration: Sentry with custom error categorization:
- Authentication errors (401, 403) → "auth_error" category
- Storage access errors (localStorage blocked, IndexedDB issues) → "storage_error" category
- Token parsing/validation failures → "token_error" category
- Network errors during token refresh → "network_error" category
Error grouping strategy:
- Group by error type and user context (session ID, device fingerprint)
- Log token storage method used (localStorage vs. IndexedDB vs. Memory)
- Include stack trace with token metadata for debugging
Custom events and user journey tracking
Authentication Journey Events:
// Event tracking for authentication flow
const trackAuthEvent = (event: string, properties?: Record<string, any>) => {
// Send to analytics backend or RUM service
const payload = {
event,
timestamp: Date.now(),
userId: getCurrentUserId(),
sessionId: getSessionId(),
tokenType: getTokenType(),
...properties
};
// Example: "auth_token_refresh_success", "auth_session_expired"
sendToAnalytics(payload);
};
Storage Method Usage:
- Track which secure storage method is used per user session
- Monitor fallback scenarios (e.g., localStorage → IndexedDB)
- Record token storage success/failure rates by method
Performance monitoring and regression detection
Performance Baseline:
- Establish baseline performance metrics for token operations (read/write latency)
- Set up automated regression detection using historical performance data
- Alert thresholds: 20% degradation in token operation time triggers alert
Token Operation Monitoring:
// Performance monitoring for token operations
const measureTokenOperation = async <T>(
operation: string,
fn: () => Promise<T>
): Promise<T> => {
const start = performance.now();
try {
const result = await fn();
const end = performance.now();
// Log performance metrics
trackPerformanceMetric({
operation,
duration: end - start,
success: true,
timestamp: Date.now()
});
return result;
} catch (error) {
const end = performance.now();
trackPerformanceMetric({
operation,
duration: end - start,
success: false,
error: error.message,
timestamp: Date.now()
});
throw error;
}
};
Alerting Strategy:
- High-severity: Token refresh failures > 5% of requests
- Medium-severity: Storage access errors > 1% of sessions
- Low-severity: Performance degradation > 20% in token operations
Dashboard Metrics:
- Token refresh success rate (target: 99.5%)
- Secure storage method adoption rates
- Authentication flow completion rates
- Session lifetime distribution
- Error rate by token type and operation
Integration with existing monitoring tools:
- Connect to existing APM (Application Performance Monitoring) systems
- Integrate with logging infrastructure for audit trail of token operations
- Support for distributed tracing across microservices that handle authentication tokens
13. File & Folder Structure
Proposed Directory Structure
/src
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types/
├── shared/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ ├── types/
│ └── constants/
├── core/
│ ├── api/
│ ├── state/
│ └── config/
└── app/
├── routes/
├── layouts/
└── providers/
Module Boundaries and Import Rules
- Feature boundaries: Each feature (auth, dashboard, profile) is self-contained with its own components, hooks, and services
- Shared module: Reusable utilities, types, and components that don't belong to any specific feature
- Core module: Infrastructure-level code like API clients, global state management, and configuration that can be imported across features
- No cross-feature imports: Features should not directly import from other features to maintain loose coupling
Import Strategy
// Good - Feature can import from shared
import { Button } from 'shared/components/Button'
import { useAuth } from 'features/auth/hooks/useAuth'
// Good - Shared can import from core
import { apiClient } from 'core/api/client'
// Bad - Cross-feature import
import { UserProfile } from 'features/profile/components/UserProfile'
Barrel Export Strategy
Each feature and shared module exports a barrel file to simplify imports:
// features/auth/index.ts
export * from './components'
export * from './hooks'
export * from './services'
This allows clean imports like:
import { useAuth } from 'features/auth'
Folder Structure Justification
Feature-based structure is chosen over layer-based because:
- Features are the primary unit of business logic
- Easier to understand and maintain feature boundaries
- Better for team ownership and parallel development
- Easier to test features in isolation
The structure supports:
- Clear separation of concerns
- Scalable organization as the codebase grows
- Easy onboarding for new developers
- Maintainable architecture that scales with feature complexity
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. @azure/core-http or axios
- Justification: For robust HTTP client capabilities with built-in interceptors, request/response transformation, and error handling. Axios provides better out-of-the-box support for request/response interceptors and is more widely adopted in the React ecosystem.
- Alternatives:
fetch(native but lacks interceptors and better error handling),ky(modern but less established in enterprise) - Bundle Impact: ~15KB minified
2. @auth0/auth0-react or react-app-auth
- Justification: For handling authentication flows, token management, and secure session handling. Auth0 React SDK provides comprehensive features like silent authentication, refresh tokens, and automatic token renewal.
- Alternatives:
react-oauth2-provider(more lightweight but less feature-complete), custom implementation (high maintenance cost) - Bundle Impact: ~25KB minified
3. crypto-js or @noble/ciphers
- Justification: For encryption operations when tokens need to be stored in encrypted form.
@noble/ciphersis modern, audited, and lightweight (~1KB), whilecrypto-jsoffers broader compatibility but larger bundle size. - Alternatives:
Web Crypto API(native but not supported in older browsers) - Bundle Impact: ~1KB (noble) vs ~20KB (crypto-js)
4. react-query or swr
- Justification: For server state management and caching. React Query provides better developer experience with automatic refetching, optimistic updates, and built-in stale-while-revalidate patterns.
- Alternatives:
react-reduxwith Redux Toolkit (overkill for simple state management), custom hooks (more boilerplate) - Bundle Impact: ~15KB minified
5. zod or joi
- Justification: For runtime validation of token payloads and API responses. Zod provides type-level validation with excellent TypeScript integration.
- Alternatives:
yup(more verbose), custom validation logic (error-prone) - Bundle Impact: ~5KB minified
Alternatives Considered and Rejected
JWT Libraries (jsonwebtoken, jose)
- Rejected: These libraries are primarily for JWT decoding/encoding, not secure storage. They don't address the core security concerns of token persistence and encryption.
localStorage Polyfill (localforage)
- Rejected: While localforage provides better storage handling, it's unnecessary when using secure storage methods like
HttpOnlycookies orIndexedDBwith encryption.
secure-ls
- Rejected: Provides encryption but has limited browser support and is not actively maintained. It's also not compatible with modern security standards like SameSite cookies.
Licensing and Bundle Size Impact Assessment
Licensing Compliance:
- All selected libraries are MIT or Apache 2.0 licensed, suitable for enterprise use
- No GPL dependencies that would require open-sourcing the application
Bundle Size Budget Allocation:
- Core HTTP client (axios): 15KB
- Authentication SDK: 25KB
- Encryption utilities: 1KB (noble)
- State management (react-query): 15KB
- Validation (zod): 5KB
- Total: ~61KB (minified + gzipped)
Optimization Strategy:
- Tree-shaking enabled for all libraries
- Dynamic imports for non-critical features (e.g., auth UI components)
- Code splitting to load authentication modules only when needed
Security Considerations in Library Selection
Auditability:
- All selected libraries have active maintenance and security review processes
- No known vulnerabilities in the current versions (as of Q4 2023)
Token Storage Compatibility:
- Libraries support secure cookie storage when required
- Encryption libraries provide AES-GCM support for token encryption
- HTTP client libraries support SameSite cookie configuration
Browser Compatibility:
- All libraries support modern browsers (Chrome 80+, Firefox 70+, Safari 13+)
- Fallback mechanisms for older browsers where necessary (e.g., polyfills for Web Crypto API)
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Token persistence strategy: Should tokens persist across browser sessions or be session-only? This directly impacts UX (auto-login vs. forced re-authentication) and security posture.
- Cross-origin token handling: Will the application need to handle tokens from different domains or subdomains? This affects storage strategy and security policies.
- Multi-tab token synchronization: Should tokens be synchronized across browser tabs? This introduces complexity in managing concurrent access and potential race conditions.
Known trade-offs made in this design
- Storage security vs. convenience: Using
HttpOnlycookies provides better security but requires server-side implementation and adds complexity to client-server communication. Session storage offers convenience but is less secure. - Cross-browser compatibility vs. modern APIs: Leveraging
SecureContextandSameSiteattributes provides better security but requires modern browser support, potentially excluding older browsers. - Token refresh vs. long-lived tokens: Implementing automatic refresh provides better UX but increases complexity and potential security risks if not properly implemented.
Risks and mitigations
- Storage-based token theft: If tokens are stored in localStorage/sessionStorage, they're vulnerable to XSS attacks. Mitigation: Use HttpOnly cookies when possible and implement strict Content Security Policy.
- Cross-site scripting (XSS) vulnerability: Malicious scripts can access stored tokens. Mitigation: Implement comprehensive XSS prevention, sanitize all user inputs, and use secure storage mechanisms.
- Token refresh race conditions: Concurrent refresh requests can lead to token invalidation. Mitigation: Implement request queuing and token validation before refresh.
- Browser compatibility issues: Older browsers may not support modern security features. Mitigation: Implement graceful degradation and provide fallback mechanisms.
Technical debt acknowledged
- Cookie handling complexity: HttpOnly cookie implementation requires server-side support and adds complexity to the authentication flow, creating technical debt in cross-origin scenarios.
- Storage migration path: Future changes to token storage strategy may require migration logic, which is not currently implemented.
- Token refresh logic complexity: The current implementation assumes single token type and may not scale well for multi-token scenarios (e.g., refresh tokens, access tokens).
- Limited token lifecycle management: Current design doesn't handle token expiration tracking beyond basic validation, which could lead to stale tokens in the system.
- Security feature dependency: Reliance on modern browser security features creates technical debt if these features are deprecated or have inconsistent implementations across browsers.