Skip to main content

JWT Token Management System Design

Comprehensive low-level design for secure JWT token handling, refresh mechanisms, and session management in modern web applications.

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

  1. What authentication method is being used (OAuth, SAML, custom JWT, etc.)?
  2. Are we building a full authentication system or just token management utilities for an existing auth flow?
  3. What's the target browser support matrix (ES6+ browsers, IE11, mobile-only, etc.)?
  4. Do we need to support refresh token rotation or just access token expiration handling?
  5. Should the system handle multi-tab/window token synchronization (e.g., when a token expires in one tab, others should invalidate)?
  6. Are there specific security requirements for token storage (e.g., HTTPOnly cookies vs localStorage)?
  7. 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 logic
  • TokenProvider - Context provider that exposes auth state to the app
  • AuthInterceptor - HTTP interceptor that handles automatic token injection and refresh
  • AuthGuard - Route guard that protects authenticated routes

Dumb Components (Presentational):

  • LoginView - Form for user credentials
  • LogoutButton - UI trigger for logout flow
  • UserSession - Displays current user session info
  • TokenStorage - 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 management
  • TokenProvider: Provides authentication context to the application using React Context API
  • AuthInterceptor: Intercepts HTTP requests to inject tokens and handle refresh logic
  • AuthGuard: Protects route access based on authentication state
  • UserSession: 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

  1. Mounting: AuthManager initializes token state from storage and sets up interceptors
  2. Updating: TokenProvider updates context when auth state changes
  3. Unmounting: AuthManager cleans up event listeners and timers
  4. 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

  1. Token expiration triggers cache invalidation
  2. Manual logout clears all caches
  3. Failed refresh attempts invalidate token and trigger login flow
  4. 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.

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:

  1. User navigates to protected route without valid token
  2. ProtectedRoute component redirects to /login
  3. After successful login, user is redirected back to original route
  4. 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

  1. Critical CSS Inlining:

    • Inline critical CSS for login and auth flows
    • Defer non-critical CSS to prevent render-blocking
  2. Resource Hints:

    <link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="prefetch" href="/api/user/profile">
  3. Code Splitting Strategy:

    • Route-based: /auth → AuthLazyComponent
    • Component-based: Token refresh logic isolated in useTokenRefresh

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

  1. Font Display Optimization:
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-display: swap;
}
  1. Preload Critical Fonts:
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>

Memoization Strategy

  1. useCallback for Event Handlers:
const handleTokenRefresh = useCallback(() => {
  // refresh logic
}, [token, refreshToken]);

const handleLogout = useCallback(() => {
  // logout logic
}, [authToken]);
  1. React.memo for Presentational Components:
const AuthStatus = React.memo(({ status }: { status: string }) => (
  <div className="auth-status">{status}</div>
));
  1. 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

  1. 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
  });
};
  1. Route Preloading:
// Preload auth routes when user navigates to protected areas
const preloadAuthRoutes = () => {
  if (isUserAuthenticated()) {
    preload('/dashboard');
  }
};
  1. 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

  1. 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'] });
};
  1. 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);
  }
};
  1. 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:

  1. Successful authentication flow
  2. Token refresh during active session
  3. Session timeout and automatic logout
  4. Invalid token handling and recovery
  5. 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-invalid when validation fails
  • Validation Button: aria-label="Validate JWT Token" for screen readers, aria-describedby="validation-status"
  • Modal Dialog: role="dialog" with aria-modal="true", aria-labelledby="modal-title"
  • Loading States: aria-busy="true" on loading elements, aria-live="polite" for status updates
  • Error Messages: role="alert" with aria-live="assertive" for immediate feedback
  • Progress Indicators: aria-valuenow, aria-valuemin, aria-valuemax for progress bars

Screen reader compatibility

All interactive elements must be properly labeled:

  • Buttons with icons must have aria-label or aria-labelledby attributes
  • 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, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;');
  }
  
  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:

  1. No sensitive data in localStorage/sessionStorage - Tokens stored in memory only
  2. CSP implementation - Prevents XSS and content injection attacks
  3. CSRF protection - Token validation for all API requests
  4. Token format validation - JWT structure and signature verification
  5. Secure token rotation - Refresh tokens with proper validation
  6. Input sanitization - Prevents malicious input injection
  7. Security headers - Comprehensive HTTP security headers
  8. 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:

  1. CSS classes that flip layout direction (direction: rtl)
  2. Text alignment adjustments (text-align: right)
  3. Margin/padding overrides for directional consistency
  4. 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:

  1. Browser navigator.language as primary fallback
  2. User preference stored in localStorage or cookie
  3. Manual override via language selector UI component

The locale switching strategy will:

  • Update the i18n context 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:

  1. Real-time Authentication Metrics: Live dashboard showing token refresh rates, success/failure ratios
  2. Error Rate Trends: Historical view of authentication errors and token-related failures
  3. User Session Health: Overview of active sessions, expiration patterns, refresh frequency
  4. 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

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend LLD

    Automatic Token Refresh System Design

    Comprehensive low-level design for implementing automatic JWT token refresh in frontend applications with error handling and concurrency control

  • Frontend LLD

    Client-Side Authentication Manager Design

    Comprehensive low-level design for a secure, scalable client-side authentication system with token management and session handling.

  • Backend LLD

    JWT Token Management System Design

    Comprehensive low-level design for a secure JWT token management system with refresh token handling, blacklisting, and distributed session management.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.