Skip to main content

Client-Side Authentication Manager Design

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

This document presents a robust client-side authentication manager architecture that handles secure token storage, automatic refresh cycles, and seamless user session management while maintaining optimal performance and security standards across modern web applications.

1. Problem Statement & Requirements

Clarifying questions

  1. What authentication methods are we supporting? OAuth 2.0, SAML, JWT, or session-based cookies?
  2. What's the target browser support matrix? IE11, mobile Safari, Chrome/Firefox on Android/iOS?
  3. Do we need to support multi-factor authentication (MFA) flows? How should MFA be handled in the client-side state?
  4. Should authentication state persist across browser sessions? If yes, what storage mechanism (localStorage vs. sessionStorage vs. cookies)?
  5. Are there specific security requirements for token handling? Should tokens be stored in HTTP-only cookies or client-side?
  6. What's the expected scale of concurrent users? Will this impact caching strategy or session management?
  7. Do we need to support internationalization (i18n) in auth flows? Should error messages and UI be localized?

Functional requirements

  • User login with email/password or social providers (Google, GitHub)
  • Session management (auto-refresh tokens, logout, session timeout handling)
  • Protected route access control based on user roles/permissions
  • Password reset flow with email verification
  • Multi-factor authentication (MFA) support
  • Remember me functionality for session persistence
  • User registration flow with validation and confirmation
  • Social login integration (OAuth 2.0 flows)
  • Token refresh mechanism for long-lived sessions

Non-functional requirements

  • Performance: Authentication state should be available within 100ms of app load
  • Security: Tokens must not be exposed in URL parameters or console logs
  • Scalability: State management should handle 10K+ concurrent authenticated users
  • Accessibility: All auth flows must be keyboard-navigable and screen reader compatible
  • Reliability: Graceful handling of network failures during authentication flows

Constraints and assumptions

  • Authentication state is managed client-side only (no server-side session storage)
  • Tokens are stored in browser's secure HTTP-only cookies or localStorage (depending on security requirements)
  • The system assumes a single-page application architecture with React Router
  • Authentication flows are handled within the same browser context (no cross-origin support)
  • User session timeout is set to 30 minutes of inactivity
  • All authentication endpoints are assumed to be secured with HTTPS

Out of scope

  • Server-side session management or session validation
  • Third-party identity provider integration beyond OAuth 2.0 (e.g., Okta, Auth0)
  • Password strength validation rules or complexity requirements
  • Multi-tenant authentication (e.g., SaaS multi-org support)
  • Authentication with biometric or hardware security keys
  • Token encryption at rest (only transport security is assumed)
  • Cross-browser session sharing or sync across devices
  • Authentication via mobile app deep links or web-to-native bridges

2. Component Architecture

graph TD
    A[AuthManager] --> B[AuthProvider]
    A --> C[AuthContext]
    B --> D[LoginModal]
    B --> E[LogoutButton]
    B --> F[ProtectedRoute]
    D --> G[LoginForm]
    D --> H[ForgotPasswordForm]
    G --> I[EmailInput]
    G --> J[PasswordInput]
    G --> K[SubmitButton]
    H --> L[EmailInput]
    H --> M[SubmitButton]
    F --> N[ProtectedComponent]
    N --> O[UserMenu]
    O --> P[UserProfile]
    O --> Q[SettingsPanel]

Component Tree Breakdown

Smart Components (Container/Logic):

  • AuthManager - Main orchestrator that manages auth state lifecycle and side effects
  • AuthProvider - Context provider that exposes auth state to the app
  • ProtectedRoute - Route guard that protects authenticated routes

Dumb Components (Presentational):

  • LoginForm - Form for user credentials
  • ForgotPasswordForm - Password recovery form
  • LogoutButton - UI trigger for logout
  • LoginModal - Modal container for auth flows
  • UserMenu - User dropdown menu
  • UserProfile - User profile display
  • SettingsPanel - User settings UI

TypeScript Props Contracts

// AuthManager Props
interface AuthManagerProps {
  children: React.ReactNode;
  onAuthChange?: (authState: AuthState) => void;
  onLogout?: () => void;
}

// LoginForm Props
interface LoginFormProps {
  onSubmit: (credentials: { email: string; password: string }) => Promise<void>;
  onForgotPassword?: () => void;
  loading?: boolean;
  error?: string;
}

// ProtectedRoute Props
interface ProtectedRouteProps {
  children: React.ReactNode;
  redirectPath?: string;
}

// AuthContextValue
interface AuthContextValue {
  isAuthenticated: boolean;
  user?: User | null;
  isLoading: boolean;
  login: (credentials: { email: string; password: string }) => Promise<void>;
  logout: () => void;
  forgotPassword: (email: string) => Promise<void>;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> LoggingIn : startLogin
    LoggingIn --> Authenticated : loginSuccess
    LoggingIn --> LoginError : loginFailure
    Authenticated --> LoggingOut : startLogout
    LoggingOut --> Idle : logoutSuccess
    LoginError --> Idle : resetError
    
    state LoginError {
        [*] --> InvalidCredentials
        InvalidCredentials --> Idle : retryLogin
    }

Component Responsibilities

AuthManager:

  • Initializes auth state from localStorage/sessionStorage
  • Handles auth token refresh logic
  • Manages authentication lifecycle (login, logout, token expiration)
  • Provides global auth context to app

AuthProvider:

  • Wraps app with authentication context
  • Exposes auth state and actions to components
  • Handles auth state persistence

ProtectedRoute:

  • Guards authenticated routes
  • Redirects unauthenticated users to login
  • Renders protected content when authorized

LoginModal:

  • Contains login and forgot password flows
  • Manages form state and validation
  • Displays error messages

Reusability Decisions

The auth system is designed for maximum reusability:

  • AuthContext provides a clean abstraction layer
  • ProtectedRoute can be reused across any protected feature
  • Form components are generic and customizable
  • Authentication state is centralized for consistent UX

Component Lifecycle Considerations

  1. Mounting: AuthManager checks localStorage for existing tokens
  2. Updating: Context updates trigger re-renders in dependent components
  3. Unmounting: Cleanup handlers for token refresh intervals
  4. Error Handling: Graceful degradation when auth fails
  5. Cleanup: Token refresh cleanup on logout or session expiration

The component architecture follows React best practices with proper separation of concerns, ensuring that authentication logic is centralized while UI components remain pure and testable.

3. State Management Design

Local state vs. global state decisions

For client-side authentication, we need to distinguish between local and global state:

  • Local state: Session-specific data like login form fields, password strength indicators, and temporary UI states (e.g., loading spinners)
  • Global state: Authentication status, user profile data, token storage, and session persistence across the application

Authentication state is inherently global since it affects the entire app's behavior and navigation flow.

State management approach

Zustand is selected as the primary state management solution for the following reasons:

  1. Lightweight: No boilerplate, minimal bundle size impact (~2KB)
  2. Simple API: Easy to understand and maintain for team onboarding
  3. TypeScript support: Excellent type inference capabilities
  4. Server-side rendering friendly: Works well with SSR environments
  5. No provider wrapping required: Reduces component tree complexity

Zustand's approach of using a simple store with actions and selectors provides better developer experience than Redux Toolkit for this use case, where we don't need complex middleware or extensive dev tools.

State shape and normalization strategy

interface AuthState {
  // Authentication status
  isAuthenticated: boolean;
  isAuthenticating: boolean;
  
  // User profile data (normalized)
  user: {
    id: string;
    email: string;
    name?: string;
    roles?: string[];
  } | null;
  
  // Token management
  accessToken: string | null;
  refreshToken: string | null;
  
  // Session metadata
  sessionExpiry: Date | null;
  lastLogin: Date | null;
  
  // Error and validation state
  authError: string | null;
  validationErrors: Record<string, string>;
  
  // UI state
  showLoginModal: boolean;
}

Server state vs. client state separation

  • Server state: User data fetched from API (profile, permissions, roles)
  • Client state: Authentication flow state, session tokens, UI flags, and local validation

Server state is managed through React Query for caching and automatic refetching, while client state is handled by Zustand.

Cache invalidation strategy

  1. Token expiration: Automatically invalidate session when access token expires
  2. Manual logout: Clear all tokens and user data from state
  3. Token refresh: Invalidate old tokens when new ones are received
  4. Network errors: Retry failed auth operations with exponential backoff

Optimistic updates design

For login flow, we implement optimistic UI updates:

  1. Show loading state immediately on form submission
  2. Display success state before server confirmation
  3. Revert to error state if authentication fails
  4. Update session tokens and user profile atomically

Critical-path hook implementation

// src/features/auth/hooks/useAuthStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { AuthState, LoginCredentials, RegisterData } from '../types';

interface AuthStore extends AuthState {
  login: (credentials: LoginCredentials) => Promise<void>;
  register: (userData: RegisterData) => Promise<void>;
  logout: () => void;
  refreshAuth: () => Promise<boolean>;
  setAuthError: (error: string) => void;
  clearAuthError: () => void;
}

const useAuthStore = create<AuthStore>()(
  persist(
    (set, get) => ({
      // Initial state
      isAuthenticated: false,
      isAuthenticating: false,
      user: null,
      accessToken: null,
      refreshToken: null,
      sessionExpiry: null,
      lastLogin: null,
      authError: null,
      validationErrors: {},
      showLoginModal: false,

      // Authentication actions
      login: async (credentials) => {
        set({ isAuthenticating: true, authError: null });
        
        try {
          // Optimistic update
          set({ isAuthenticated: true, isAuthenticating: false });
          
          // Simulate API call
          const response = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(credentials),
          });
          
          if (!response.ok) {
            const error = await response.json();
            throw new Error(error.message || 'Authentication failed');
          }
          
          const { accessToken, refreshToken, user } = await response.json();
          
          // Update state with real data
          set({
            accessToken,
            refreshToken,
            user,
            isAuthenticated: true,
            isAuthenticating: false,
            lastLogin: new Date(),
          });
          
        } catch (error) {
          set({
            isAuthenticated: false,
            isAuthenticating: false,
            authError: error instanceof Error ? error.message : 'Authentication failed',
          });
        }
      },

      register: async (userData) => {
        set({ isAuthenticating: true, authError: null });
        
        try {
          const response = await fetch('/api/auth/register', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(userData),
          });
          
          if (!response.ok) {
            const error = await response.json();
            throw new Error(error.message || 'Registration failed');
          }
          
          // Auto-login after successful registration
          const { accessToken, refreshToken, user } = await response.json();
          
          set({
            accessToken,
            refreshToken,
            user,
            isAuthenticated: true,
            isAuthenticating: false,
            lastLogin: new Date(),
          });
          
        } catch (error) {
          set({
            isAuthenticated: false,
            isAuthenticating: false,
            authError: error instanceof Error ? error.message : 'Registration failed',
          });
        }
      },

      logout: () => {
        set({
          isAuthenticated: false,
          user: null,
          accessToken: null,
          refreshToken: null,
          sessionExpiry: null,
          lastLogin: null,
        });
      },

      refreshAuth: async () => {
        const { refreshToken } = get();
        
        if (!refreshToken) {
          return false;
        }
        
        try {
          const response = await fetch('/api/auth/refresh', {
            method: 'POST',
            headers: { 
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${refreshToken}`,
            },
          });
          
          if (!response.ok) {
            throw new Error('Token refresh failed');
          }
          
          const { accessToken, refreshToken: newRefreshToken } = await response.json();
          
          set({
            accessToken,
            refreshToken: newRefreshToken,
          });
          
          return true;
        } catch (error) {
          set({
            isAuthenticated: false,
            accessToken: null,
            refreshToken: null,
          });
          return false;
        }
      },

      setAuthError: (error) => set({ authError: error }),
      clearAuthError: () => set({ authError: null }),
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({
        accessToken: state.accessToken,
        refreshToken: state.refreshToken,
        isAuthenticated: state.isAuthenticated,
      }),
    }
  )
);

export default useAuthStore;

The hook returns:

  • State: Authentication status, user data, tokens, and error states
  • Actions: Login, register, logout, refresh, and error handling methods
  • Persistence: Session tokens are persisted to localStorage for seamless user experience across browser sessions

Key state transitions:

  1. isAuthenticating → isAuthenticated on successful login
  2. isAuthenticated → false on logout or token expiration
  3. authError cleared when successful operations complete

Side-effect handlers include:

  • Token refresh on expiration
  • Session persistence across page reloads
  • Error state management for failed operations

4. Routing & Navigation

Route Structure and Hierarchy

The authentication system requires a clear route structure to handle different user states and flows:

graph TD
    A[Root] --> B{Auth State}
    B -->|Unauthenticated| C[/login]
    B -->|Authenticated| D[/dashboard]
    D --> E[/profile]
    D --> F[/settings]
    C --> G[/forgot-password]
    C --> H[/reset-password]
    C --> I[/register]
  • Public Routes: /login, /forgot-password, /reset-password, /register
  • Protected Routes: /dashboard, /profile, /settings
  • Layout Routes: / (public layout), /auth (auth layout with login/register flow)
  • Nested Routes:
    • /dashboard → /dashboard/analytics, /dashboard/projects
    • /profile → /profile/edit, /profile/security

Code Splitting Strategy

// routes/auth.tsx - Route-based code splitting for auth flows
const Login = React.lazy(() => import('pages/auth/Login'));
const Register = React.lazy(() => import('pages/auth/Register'));
const ForgotPassword = React.lazy(() => import('pages/auth/ForgotPassword'));
const ResetPassword = React.lazy(() => import('pages/auth/ResetPassword'));

// routes/dashboard.tsx - Route-based code splitting for dashboard
const Dashboard = React.lazy(() => import('pages/dashboard/Dashboard'));
const Profile = React.lazy(() => import('pages/dashboard/Profile'));
const Settings = React.lazy(() => import('pages/dashboard/Settings'));

// Code splitting implementation
const AuthRoutes = () => (
  <Suspense fallback={<LoadingSpinner />}>
    <Routes>
      <Route path="/login" element={<Login />} />
      <Route path="/register" element={<Register />} />
      <Route path="/forgot-password" element={<ForgotPassword />} />
      <Route path="/reset-password" element={<ResetPassword />} />
    </Routes>
  </Suspense>
);

const ProtectedRoutes = () => (
  <Suspense fallback={<LoadingSpinner />}>
    <Routes>
      <Route path="/dashboard" element={<Dashboard />} />
      <Route path="/profile" element={<Profile />} />
      <Route path="/settings" element={<Settings />} />
    </Routes>
  </Suspense>
);

Justification:

  • Auth pages are less frequently accessed, so lazy loading reduces initial bundle size
  • Dashboard routes contain heavy components that benefit from code splitting
  • Suspense fallback provides better UX during loading states
// auth-guard.tsx - Authentication guard with navigation logic
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from 'stores/authStore';

const AuthGuard = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const { isAuthenticated, isInitialized, user } = useAuthStore();

  useEffect(() => {
    if (!isInitialized) return;
    
    if (!isAuthenticated && !isPublicRoute(location.pathname)) {
      navigate('/login', { replace: true });
    } else if (isAuthenticated && isPublicRoute(location.pathname)) {
      navigate('/dashboard', { replace: true });
    }
  }, [isAuthenticated, isInitialized]);

  return <>{children}</>;
};

const isPublicRoute = (pathname: string) => {
  return ['/login', '/register', '/forgot-password', '/reset-password'].includes(pathname);
};

Deep Linking Support and URL Parameter Design

// auth-utils.ts - URL parameter handling for deep linking
export const getRedirectUrl = (searchParams: URLSearchParams): string => {
  const redirect = searchParams.get('redirect') || '/dashboard';
  // Validate redirect URL to prevent open redirects
  if (isValidRedirectUrl(redirect)) {
    return redirect;
  }
  return '/dashboard';
};

export const buildAuthRedirectUrl = (redirectPath: string): string => {
  return `/login?redirect=${encodeURIComponent(redirectPath)}`;
};

const isValidRedirectUrl = (url: string): boolean => {
  try {
    const urlObj = new URL(url, window.location.origin);
    return urlObj.origin === window.location.origin;
  } catch {
    return false;
  }
};

Browser History Management and Scroll Restoration

// scroll-restoration.ts - Custom scroll restoration logic
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

const ScrollToTop = () => {
  const location = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [location.pathname]);

  return null;
};

// In main App component
const App = () => {
  return (
    <>
      <ScrollToTop />
      <BrowserRouter>
        <Routes>
          {/* ... routes */}
        </Routes>
      </BrowserRouter>
    </>
  );
};

// Custom scroll restoration for specific components
const DashboardScrollRestoration = () => {
  const location = useLocation();
  
  useEffect(() => {
    // Restore scroll position for dashboard sub-routes
    const savedScrollPosition = sessionStorage.getItem(`scroll-position-${location.pathname}`);
    if (savedScrollPosition) {
      window.scrollTo(0, parseInt(savedScrollPosition));
    }
  }, [location.pathname]);

  useEffect(() => {
    const handleScroll = () => {
      sessionStorage.setItem(`scroll-position-${location.pathname}`, window.scrollY.toString());
    };

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [location.pathname]);

  return null;
};

Route Protection Implementation

// ProtectedRoute.tsx - Complete protected route implementation
import { useEffect } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from 'stores/authStore';

interface ProtectedRouteProps {
  children: React.ReactNode;
  roles?: string[];
}

const ProtectedRoute = ({ children, roles }: ProtectedRouteProps) => {
  const location = useLocation();
  const { isAuthenticated, user, isLoading } = useAuthStore();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (roles && user && !roles.includes(user.role)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return <>{children}</>;
};

// Usage in route configuration
const AppRoutes = () => {
  return (
    <Routes>
      <Route path="/login" element={<Login />} />
      <Route path="/register" element={<Register />} />
      
      <Route 
        path="/dashboard" 
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        } 
      />
      
      <Route 
        path="/admin" 
        element={
          <ProtectedRoute roles={['admin']}>
            <AdminDashboard />
          </ProtectedRoute>
        } 
      />
    </Routes>
  );
};

Authentication Flow Navigation

// auth-navigation.ts - Navigation helper for authentication flows
export const navigateToAuthFlow = (flow: 'login' | 'register' | 'forgot') => {
  const currentPath = window.location.pathname;
  
  switch (flow) {
    case 'login':
      if (currentPath === '/register') {
        window.history.replaceState({}, '', '/login');
      } else {
        window.location.href = '/login';
      }
      break;
    case 'register':
      if (currentPath === '/login') {
        window.history.replaceState({}, '', '/register');
      } else {
        window.location.href = '/register';
      }
      break;
    case 'forgot':
      window.location.href = '/forgot-password';
      break;
  }
};

// Navigation context for consistent auth flow handling
const AuthNavigationContext = createContext<{
  navigateToLogin: () => void;
  navigateToRegister: () => void;
  navigateToForgotPassword: () => void;
}>({
  navigateToLogin: () => {},
  navigateToRegister: () => {},
  navigateToForgotPassword: () => {},
});

export const AuthNavigationProvider = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  
  const navigateToLogin = useCallback(() => {
    navigate('/login');
  }, [navigate]);

  const navigateToRegister = useCallback(() => {
    navigate('/register');
  }, [navigate]);

  const navigateToForgotPassword = useCallback(() => {
    navigate('/forgot-password');
  }, [navigate]);

  return (
    <AuthNavigationContext.Provider value={{
      navigateToLogin,
      navigateToRegister,
      navigateToForgotPassword
    }}>
      {children}
    </AuthNavigationContext.Provider>
  );
};

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized Axios instance with request/response interceptors to handle authentication and API consistency:

// api/client.ts
import axios, { AxiosInstance } from 'axios';

const createApiClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
    timeout: 10000,
    headers: {
      'Content-Type': 'application/json',
    },
  });

  // Request interceptor for adding auth token
  client.interceptors.request.use(
    (config) => {
      const token = localStorage.getItem('authToken');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    (error) => Promise.reject(error)
  );

  // Response interceptor for handling auth errors
  client.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 axios(originalRequest);
        } catch (refreshError) {
          // Redirect to login if refresh fails
          localStorage.removeItem('authToken');
          window.location.href = '/login';
          return Promise.reject(refreshError);
        }
      }
      
      return Promise.reject(error);
    }
  );

  return client;
};

export const apiClient = createApiClient();

Request/Response Interceptors

The interceptors handle:

  1. Authentication Token Injection: Automatically adds Bearer token to all requests
  2. Token Refresh Logic: Handles 401 responses by refreshing tokens
  3. Error Normalization: Standardizes error responses for consistent handling
  4. Response Transformation: Converts raw API responses to domain models

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 = (error: any): ApiError => {
  if (error.response) {
    const { status, data } = error.response;
    return new ApiError(
      status,
      data.message || 'API request failed',
      data.code
    );
  }
  
  if (error.request) {
    return new ApiError(503, 'Network error - please check your connection');
  }
  
  return new ApiError(500, 'Unexpected error occurred');
};

// Retry strategy with exponential backoff
export const retryWithBackoff = async (
  fn: () => Promise<any>,
  retries = 3,
  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

// api/types.ts
export type ApiStatus = 'idle' | 'loading' | 'success' | 'error';

export interface ApiResponse<T> {
  data: T | null;
  status: ApiStatus;
  error: ApiError | null;
}

export interface AuthResponse {
  token: string;
  user: User;
}

export interface User {
  id: string;
  email: string;
  name: string;
}

Data Fetching Strategy

For authentication flows, we'll use Client-Side Rendering (CSR) with:

  • Initial Load: Direct API call on component mount
  • Token Refresh: Background refresh when token is near expiry (5 minutes before)
  • Error Recovery: Automatic retry with exponential backoff
  • Caching Strategy: No client-side caching for auth endpoints (security-sensitive)
// api/auth.ts
export const authenticateUser = async (
  credentials: { email: string; password: string }
): Promise<AuthResponse> => {
  try {
    const response = await apiClient.post<AuthResponse>('/auth/login', credentials);
    return response.data;
  } catch (error) {
    throw handleApiError(error);
  }
};

export const refreshAuthToken = async (): Promise<string> => {
  const refreshToken = localStorage.getItem('refreshToken');
  if (!refreshToken) {
    throw new Error('No refresh token available');
  }
  
  const response = await apiClient.post<{ token: string }>('/auth/refresh', {
    refreshToken,
  });
  
  return response.data.token;
};

API Response Transformation Layer

// api/transformers.ts
import { User } from './types';

export const transformUserResponse = (rawUser: any): User => ({
  id: rawUser.id,
  email: rawUser.email,
  name: rawUser.name || rawUser.displayName,
});

export const transformAuthResponse = (rawResponse: any): AuthResponse => ({
  token: rawResponse.token,
  user: transformUserResponse(rawResponse.user),
});

Sequence Diagram for Primary Authentication Flow

sequenceDiagram
    participant U as User
    participant C as Client
    participant A as Auth API
    
    U->>C: Submit login form
    C->>A: POST /auth/login
    A-->>C: 200 OK with token
    C->>C: Store token in localStorage
    C->>A: GET /user/profile
    A-->>C: 200 OK with user data
    C->>U: Render dashboard with authenticated state

The authentication manager handles all API interactions through a single, well-structured client that:

  1. Manages token lifecycle automatically
  2. Provides consistent error handling across the application
  3. Supports retry logic for transient network issues
  4. Maintains security through proper token handling and refresh mechanisms
  5. Integrates cleanly with React's component lifecycle and state management patterns

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for authenticated routes, <1.5s for public routes
  • Interaction to Next Paint (INP): <200ms for user interactions
  • Cumulative Layout Shift (CLS): <0.1 for authenticated content, <0.05 for public content

Bundle Size Budget per Route

  • Public routes (Login, Signup): <200KB total (including dependencies)
  • Authenticated dashboard: <400KB total (including feature modules)
  • All authenticated routes combined: <600KB total (excluding large media assets)

Critical Rendering Path Optimization

  • Critical CSS extraction: Inline critical styles for login and dashboard routes
  • Resource hints: Preload key fonts, critical API endpoints, and essential assets
  • Code splitting: Route-based chunking with dynamic imports for feature modules
  • Lazy loading: Non-critical components (e.g., settings panel, help center) loaded on demand

Image Optimization Strategy

  • Format selection: WebP for modern browsers, JPEG fallback for legacy
  • Lazy loading: loading="lazy" attribute on all user avatar and profile images
  • Responsive image delivery: srcSet with multiple sizes for different viewport widths
  • Image compression: 20% reduction in file size without perceptible quality loss

Font Loading Strategy

  • Font display strategy: font-display: swap to prevent FOIT
  • Preload critical fonts: preload for system UI and primary brand fonts
  • Font subsetting: Only load required character sets (Latin, Cyrillic, etc.)
  • Fallback fonts: Immediate fallback to system fonts during font loading

Memoization Strategy

// Memoize expensive computations in auth context
const AuthContext = React.createContext<AuthContextType>({
  user: null,
  isAuthenticated: false,
  isLoading: false,
  login: () => Promise.resolve(),
  logout: () => {},
});

// Use React.memo for component rendering
const UserProfileCard = React.memo(({ user }: { user: User }) => {
  return (
    <div className="user-profile-card">
      <img src={user.avatarUrl} alt={user.name} />
      <span>{user.name}</span>
    </div>
  );
});

// Use useCallback for event handlers
const handleLogin = useCallback((credentials) => {
  return authClient.login(credentials);
}, []);

// Use useMemo for derived state calculations
const userPermissions = useMemo(() => {
  return calculateUserPermissions(user);
}, [user]);

Virtualization for Large Lists

  • Infinite scroll lists: Use react-window or react-virtual for user permissions, role assignments
  • Virtualized dropdowns: Implement virtualization for large select menus (e.g., role selection)
  • Windowing strategy: Only render visible items, calculate item heights dynamically

Prefetching and Preloading Strategy

  • Token refresh prefetch: Preemptively fetch new tokens 30 seconds before expiration
  • Route preloading: Preload critical route dependencies (e.g., dashboard data) when user is idle
  • API preflight: Use prefetch headers for critical authentication endpoints
  • Feature preloading: Load feature modules when user navigates to authenticated routes
// Prefetch strategy example for auth flow
const prefetchAuthData = async () => {
  // Preload user session data
  const sessionPromise = fetch('/api/session');
  
  // Preload permissions for dashboard
  const permissionsPromise = fetch('/api/permissions');
  
  // Preload user preferences
  const preferencesPromise = fetch('/api/preferences');
  
  return Promise.all([sessionPromise, permissionsPromise, preferencesPromise]);
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. The placement hierarchy is:

  1. Application-level error boundary - Catches unhandled errors in the entire app
  2. Feature-specific error boundaries - Wrap major feature components (e.g., dashboard, auth forms)
  3. Component-level error boundaries - Handle specific UI component failures (e.g., charts, tables)
graph TD
    A[App Root] --> B[ErrorBoundary]
    B --> C[AuthManager]
    B --> D[Dashboard]
    B --> E[UserSettings]
    C --> F[LoginForm]
    C --> G[RegisterForm]
    F --> H[InputField]
    F --> I[SubmitButton]

Global Error Handler Design

A centralized error handler manages all uncaught errors and provides consistent user feedback:

// src/lib/error-handler.ts
interface ErrorHandlerConfig {
  onError: (error: Error, errorInfo: React.ErrorInfo) => void;
  onNetworkError: (error: NetworkError) => void;
  onAuthError: (error: AuthError) => void;
}

class GlobalErrorHandler {
  private config: ErrorHandlerConfig;

  constructor(config: ErrorHandlerConfig) {
    this.config = config;
    
    // Handle unhandled promise rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.handleUnhandledRejection(event.reason);
    });
    
    // Handle uncaught errors
    window.addEventListener('error', (event) => {
      this.handleUncaughtError(event.error);
    });
  }

  handleUnhandledRejection(error: unknown) {
    const errorObj = error instanceof Error ? error : new Error(String(error));
    this.config.onError(errorObj, {} as React.ErrorInfo);
  }

  handleUncaughtError(error: Error) {
    this.config.onError(error, {} as React.ErrorInfo);
  }

  handleNetworkError(error: NetworkError) {
    this.config.onNetworkError(error);
  }

  handleAuthError(error: AuthError) {
    this.config.onAuthError(error);
  }
}

export default GlobalErrorHandler;

Offline / Network Failure Handling

Offline support is implemented through network status monitoring and graceful degradation:

// src/hooks/use-network-status.ts
import { useState, useEffect } from 'react';

export interface NetworkStatus {
  online: boolean;
  status: 'online' | 'offline' | 'reconnecting';
  lastUpdated: Date;
}

export const useNetworkStatus = (): NetworkStatus => {
  const [networkStatus, setNetworkStatus] = useState<NetworkStatus>({
    online: navigator.onLine,
    status: navigator.onLine ? 'online' : 'offline',
    lastUpdated: new Date(),
  });

  useEffect(() => {
    const handleOnline = () => {
      setNetworkStatus({
        online: true,
        status: 'online',
        lastUpdated: new Date(),
      });
    };

    const handleOffline = () => {
      setNetworkStatus({
        online: false,
        status: 'offline',
        lastUpdated: new Date(),
      });
    };

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return networkStatus;
};

Graceful Degradation Patterns

When authentication services are unavailable, the system provides fallback behavior:

// src/components/AuthFallback.tsx
import React from 'react';

interface AuthFallbackProps {
  status: 'offline' | 'service-unavailable';
  onRetry: () => void;
}

const AuthFallback: React.FC<AuthFallbackProps> = ({ status, onRetry }) => {
  const message = status === 'offline' 
    ? "Authentication services are currently unavailable due to network issues."
    : "Authentication service is temporarily down. Please try again later.";

  return (
    <div className="auth-fallback">
      <p>{message}</p>
      <button onClick={onRetry} disabled={status === 'service-unavailable'}>
        Retry Connection
      </button>
    </div>
  );
};

export default AuthFallback;

User Feedback on Errors

Comprehensive error feedback using toast notifications and inline error displays:

// src/lib/toast-manager.ts
import { toast } from 'react-toastify';

export class ToastManager {
  static showAuthError(error: Error) {
    toast.error(`Authentication failed: ${error.message}`, {
      position: "top-right",
      autoClose: 5000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
    });
  }

  static showNetworkError(error: NetworkError) {
    toast.error("Network error occurred. Please check your connection.", {
      position: "bottom-center",
      autoClose: 3000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
    });
  }

  static showGenericError(message: string) {
    toast.error(message, {
      position: "top-center",
      autoClose: 4000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
    });
  }
}

Error Recovery Strategy

The authentication manager implements automatic recovery for transient errors:

// src/lib/auth-recovery.ts
export class AuthRecovery {
  private retryCount = 0;
  private maxRetries = 3;
  private retryDelay = 1000;

  async attemptRecovery(
    operation: () => Promise<any>,
    error: Error
  ): Promise<any> {
    if (this.shouldRetry(error)) {
      this.retryCount++;
      
      // Exponential backoff
      const delay = this.retryDelay * Math.pow(2, this.retryCount - 1);
      
      await new Promise(resolve => setTimeout(resolve, delay));
      
      return operation();
    }
    
    throw error;
  }

  private shouldRetry(error: Error): boolean {
    const networkErrors = ['Network Error', 'Failed to fetch', 'timeout'];
    const authErrors = ['token expired', 'invalid token', 'unauthorized'];
    
    return (
      this.retryCount < this.maxRetries &&
      (networkErrors.some(msg => error.message.includes(msg)) ||
       authErrors.some(msg => error.message.toLowerCase().includes(msg)))
    );
  }
}

Error Boundary Implementation

// src/components/ErrorBoundary.tsx
import React from 'react';

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): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log error to monitoring service
    console.error('Auth Error Boundary caught:', error, errorInfo);
    
    this.setState({ errorInfo });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong with authentication.</h2>
          <p>Please refresh the page or contact support.</p>
          {this.state.error && (
            <details style={{ whiteSpace: 'pre-wrap' }}>
              <summary>Error details</summary>
              {this.state.error.toString()}
              {this.state.errorInfo?.componentStack}
            </details>
          )}
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Offline Authentication Fallback

// src/hooks/use-offline-auth.ts
import { useState, useEffect } from 'react';
import { useNetworkStatus } from './use-network-status';

export const useOfflineAuth = () => {
  const [isOffline, setIsOffline] = useState(false);
  const networkStatus = useNetworkStatus();

  useEffect(() => {
    setIsOffline(!networkStatus.online);
  }, [networkStatus]);

  return {
    isOffline,
    canAttemptAuth: networkStatus.online || networkStatus.status === 'reconnecting'
  };
};

8. Testing Strategy

Unit Testing Approach

Component Tests (70% coverage target):

  • Authentication context provider and consumer components
  • Login/Logout forms with validation logic
  • Protected route wrappers and redirect components
  • Token refresh middleware components
  • Custom hook integration tests (useAuth, useToken)

Utility Tests (90% coverage target):

  • JWT token parsing and validation utilities
  • Auth state serialization/deserialization functions
  • Token refresh logic with edge case handling
  • Error message normalization utilities

Test Coverage Strategy:

// Example: Unit test for auth utility functions
describe('Auth Utilities', () => {
  describe('isTokenExpired', () => {
    it('should return true for expired tokens', () => {
      const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjF9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
      expect(isTokenExpired(expiredToken)).toBe(true);
    });
    
    it('should return false for valid tokens', () => {
      const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjF9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
      expect(isTokenExpired(validToken)).toBe(false);
    });
  });
});

Integration Testing Strategy

Authentication Flow Tests (100% coverage):

  • End-to-end login/logout cycles with mock API responses
  • Token refresh sequence integration testing
  • Protected route access control validation
  • Session persistence across page reloads
  • Concurrent request handling during token refresh

Cross-Component Integration:

  • Auth context state propagation to protected components
  • Route guard behavior with different auth states
  • Error boundary integration with auth error handling

E2E Testing Scope

Testing Tools:

  • Playwright for end-to-end testing (browser-based)
  • Cypress for component-focused E2E testing

Key User Flows to Test:

  1. Successful login with valid credentials
  2. Login failure with invalid credentials (401/403)
  3. Token expiration and automatic refresh
  4. Session timeout and re-authentication flow
  5. Protected route access with various auth states
  6. Logout and session cleanup
  7. Concurrent request handling during refresh
// Example: Playwright E2E test for login flow
test('should handle successful login and redirect', async ({ page }) => {
  await page.goto('/login');
  
  await page.fill('input[name="username"]', 'testuser');
  await page.fill('input[name="password"]', 'testpass');
  
  const responsePromise = page.waitForResponse('**/auth/login');
  await page.click('button[type="submit"]');
  
  const response = await responsePromise;
  expect(response.status()).toBe(200);
  
  // Verify redirect to dashboard
  await page.waitForURL('/dashboard');
  expect(page.url()).toContain('/dashboard');
});

Visual Regression Testing

Testing Framework: Percy.js or Chromatic

  • Test authentication modals and forms across different screen sizes
  • Verify error states and loading states for visual consistency
  • Test responsive behavior of auth components on mobile/desktop

Accessibility Testing Automation

Testing Framework: axe-core with Cypress

  • WCAG 2.1 AA compliance checks for auth forms
  • Keyboard navigation testing for login form elements
  • Screen reader compatibility verification
  • Focus order validation in auth flows
// Example: Accessibility test for login form
describe('Login Form A11y', () => {
  it('should pass WCAG AA standards', async () => {
    await page.goto('/login');
    const results = await axe.run(page);
    expect(results.violations).toHaveLength(0);
  });
});

Test Data Strategy

Test Fixtures:

  • Mock authentication responses (success, 401, 403, network errors)
  • Token fixtures with various expiration times
  • User profile data for different auth roles (admin, user, guest)
  • Edge case scenarios (malformed tokens, missing claims)

Test Data Management:

  • Use factories for generating test user data
  • Maintain consistent test user credentials across suites
  • Isolate test environments with dedicated auth endpoints

Test Environment Considerations

Isolation Strategy:

  • Use separate test authentication servers or mocks
  • Implement in-memory session storage for tests
  • Mock external auth providers (Google, GitHub) with deterministic responses

Test Data Cleanup:

  • Ensure test users don't persist between test runs
  • Clear local storage and session storage after each test suite
  • Reset auth context state between tests

Performance Testing Integration

Load Testing:

  • Simulate concurrent authentication requests
  • Test token refresh under high load conditions
  • Verify response times for auth endpoints in test environment

Memory Leak Detection:

  • Monitor for auth context state leaks in long-running tests
  • Verify proper cleanup of event listeners and subscriptions
  • Test garbage collection behavior for auth-related state objects

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The authentication manager must meet WCAG 2.1 AA standards across all user flows:

  • Text alternatives: All non-text content has appropriate alt text or ARIA labels (e.g., logo, error icons)
  • Color contrast: Minimum 4.5:1 ratio for text and 3:1 for large text, with sufficient color differentiation for interactive elements
  • Keyboard navigation: Full keyboard accessibility for all form fields, buttons, and modal interactions
  • Focus management: Clear visual focus indicators that follow logical tab order
  • Screen reader compatibility: Proper ARIA roles and semantic HTML for all interactive elements
  • Input validation: Clear error messages with programmatically associated labels and error indicators
  • Time limits: No arbitrary time limits for authentication flows (or sufficient warnings)
  • Orientation support: Works in both portrait and landscape modes

Keyboard navigation support

All authentication flows must be fully operable via keyboard:

  • Tab order follows logical content flow (username → password → submit)
  • Escape key closes modals and dropdowns
  • Enter key activates buttons and form submissions
  • Space key toggles checkboxes and radio buttons
  • Arrow keys navigate dropdown menus and selection lists

Focus management strategy

Focus handling is critical for accessible authentication:

  • Focus trapping: When modals open, focus is trapped within the modal and restored to previous element on close
  • Focus restoration: After authentication success or failure, focus returns to the appropriate element (e.g., login button or form field)
  • Focus indicators: Visual focus rings are clearly visible and follow the system's native focus styles
  • Programmatic focus: Auto-focus on first input field when form loads, and focus on error fields after validation

ARIA roles and attributes plan

// Example accessible login form component with proper ARIA attributes
const LoginForm = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [isLoading, setIsLoading] = useState(false);

  return (
    <form 
      aria-label="Login form"
      onSubmit={(e) => {
        e.preventDefault();
        // handle login
      }}
    >
      <div>
        <label htmlFor="username">Username</label>
        <input
          id="username"
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          aria-invalid={!!errors.username}
          aria-describedby="username-error"
        />
        {errors.username && (
          <span 
            id="username-error" 
            role="alert"
            className="error-message"
          >
            {errors.username}
          </span>
        )}
      </div>

      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          aria-invalid={!!errors.password}
          aria-describedby="password-error"
        />
        {errors.password && (
          <span 
            id="password-error" 
            role="alert"
            className="error-message"
          >
            {errors.password}
          </span>
        )}
      </div>

      <button 
        type="submit"
        disabled={isLoading}
        aria-busy={isLoading}
      >
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
};

Screen reader compatibility

All interactive elements must be properly announced:

  • Form fields: Proper labels associated with inputs using htmlFor/id pairs
  • Error states: Error messages are programmatically associated with fields using aria-describedby
  • Loading states: aria-busy and aria-live regions announce loading progress
  • Modal dialogs: role="dialog" with aria-modal="true" and proper focus trapping
  • Buttons: aria-label for icon-only buttons, clear button text for accessibility

Keyboard navigation flow for authentication modal

stateDiagram-v2
    [*] --> ModalOpen
    ModalOpen --> FirstFieldFocus
    FirstFieldFocus --> NextFieldFocus
    NextFieldFocus --> SubmitButtonFocus
    SubmitButtonFocus --> ModalClose
    ModalClose --> [*]

Focus trapping implementation

// Custom hook for focus trapping in modals
const useFocusTrap = (isOpen: boolean, modalRef: React.RefObject<HTMLDivElement>) => {
  useEffect(() => {
    if (!isOpen) return;

    const handleFocusTrap = (e: KeyboardEvent) => {
      if (e.key === 'Tab') {
        const focusableElements = modalRef.current?.querySelectorAll(
          'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
        ) || [];
        
        const firstElement = focusableElements[0] as HTMLElement;
        const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

        if (e.shiftKey && document.activeElement === firstElement) {
          lastElement.focus();
          e.preventDefault();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          firstElement.focus();
          e.preventDefault();
        }
      }
    };

    document.addEventListener('keydown', handleFocusTrap);
    return () => document.removeEventListener('keydown', handleFocusTrap);
  }, [isOpen]);
};

ARIA roles for authentication states

// Authentication state indicators with proper ARIA
const AuthStatusIndicator = ({ status }: { status: 'idle' | 'loading' | 'success' | 'error' }) => {
  const statusText = {
    idle: 'Ready for authentication',
    loading: 'Processing login request',
    success: 'Login successful',
    error: 'Authentication failed'
  };

  return (
    <div 
      role="status"
      aria-live="polite"
      aria-label={statusText[status]}
    >
      {status === 'loading' && <span>Authenticating...</span>}
      {status === 'success' && <span>✓ Login successful</span>}
      {status === 'error' && <span>✗ Authentication failed</span>}
    </div>
  );
};

Accessible error handling

All authentication errors must be clearly announced to screen readers:

  • Error messages use role="alert" for immediate announcement
  • Errors are associated with their respective form fields using aria-describedby
  • Success messages use aria-live="polite" for non-intrusive announcement
  • Focus is programmatically moved to the first error field when validation fails

Dynamic content updates

When authentication state changes dynamically:

  • Use aria-live="polite" for status updates that don't require immediate attention
  • Use aria-live="assertive" for critical errors or success messages that need immediate attention
  • Ensure content updates don't cause focus loss unless necessary
  • Provide clear visual and semantic feedback for all state transitions

Keyboard-accessible dropdowns and menus

For any dropdown elements in authentication flows (e.g., language selector):

  • Use role="menu" and role="menuitem"
  • Support arrow keys for navigation
  • Space/Enter to select items
  • Escape key closes the menu
  • Proper focus management within menus

10. Security Considerations

XSS Prevention Strategy

  • Input sanitization: All user-provided data must be sanitized before rendering to prevent script injection
  • Content Security Policy (CSP): Implement strict CSP headers with script-src 'self' to prevent inline script execution
  • DOM-based XSS protection: Never use dangerouslySetInnerHTML with unsanitized content; always validate and escape
  • Template literal escaping: All dynamic content in templates must be properly escaped to prevent injection
// Example of secure input handling
const sanitizeInput = (input: string): string => {
  return input
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
};

const SecureComponent = ({ content }: { content: string }) => {
  // Never do this: dangerouslySetInnerHTML={{ __html: content }}
  return <div>{sanitizeInput(content)}</div>;
};

CSRF Protection

  • CSRF tokens: Implement automatic token generation and injection for all POST/PUT/DELETE requests
  • SameSite cookies: Ensure session cookies are set with SameSite=Strict or SameSite=Secure
  • Token validation: Server-side validation of CSRF tokens for all state-modifying requests
  • Token rotation: Rotate CSRF tokens periodically or per session

Content Security Policy (CSP) Headers

  • Strict CSP: Configure Content-Security-Policy header with:
    • script-src 'self' 'unsafe-inline' 'unsafe-eval' (temporarily for development, strict in production)
    • default-src 'self'
    • img-src 'self' data: https: for image loading
  • Report-only mode: Use Content-Security-Policy-Report-Only during development to monitor violations

Sensitive Data Handling in Frontend State

  • Token storage: Never store auth tokens in localStorage or sessionStorage; use HttpOnly cookies with secure flags
  • Memory cleanup: Clear sensitive data from memory immediately after use
  • State obfuscation: Do not log auth tokens or sensitive user data to console or error tracking systems
  • Secure context: Ensure all auth flows happen in secure contexts (HTTPS only)

Input Sanitization Approach

  • Form validation: Client-side validation with server-side re-validation
  • API parameter sanitization: All API parameters must be validated and sanitized before processing
  • File upload validation: Validate file types, sizes, and content for uploads
  • Rate limiting: Implement client-side rate limiting to prevent brute force attacks

Additional Security Measures

  • Secure headers: Implement security headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection)
  • Session management: Implement automatic session timeout and idle detection
  • Secure authentication flow: Use OAuth2/OIDC with proper redirect handling and state parameters
  • Security testing: Regular security scanning of frontend dependencies and code

Risk Mitigation for Authentication Manager

  • Token exposure: Implement automatic token refresh with proper error handling
  • Session hijacking: Use secure, HttpOnly session cookies where possible
  • Cross-origin attacks: Implement proper CORS configuration and origin validation
  • Replay attacks: Use timestamp validation for authentication tokens where appropriate

The authentication manager must never store sensitive credentials in client-side memory that could be accessed via browser developer tools or memory dumps. All security measures should be implemented at both the application level and through proper server-side configuration.

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"
    forgot_password: "Forgot Password?"
    signup_prompt: "Don't have an account? Sign up"
  signup:
    title: "Sign Up"
    name_label: "Full Name"
    email_label: "Email Address"
    password_label: "Password"
    confirm_password_label: "Confirm Password"
    submit_button: "Create Account"
  forgot_password:
    title: "Reset Password"
    email_label: "Email Address"
    submit_button: "Send Reset Link"
  errors:
    invalid_credentials: "Invalid email or password"
    account_locked: "Account locked due to too many failed attempts"
    network_error: "Network error occurred. Please try again."

RTL Support Considerations

RTL support will be implemented using CSS logical properties and direction-aware layout components:

  • Layout containers will use direction: rtl for RTL locales
  • Text alignment will use text-align: start/end instead of left/right
  • Padding/margin will use logical properties (margin-inline-start, padding-block-end)
  • Component alignment (e.g., buttons, forms) will adapt based on text direction

Date, Number, Currency Formatting Strategy

All date/time and numeric formatting will leverage the browser's built-in Intl API:

// Example usage in auth context
const formatDate = (date: Date, locale: string) => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date);
};

const formatCurrency = (amount: number, locale: string, currency: string) => {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(amount);
};

Locale Detection and Switching Strategy

Automatic Detection:

  • Primary detection from navigator.language or navigator.languages[0]
  • Fallback to default locale (en-US) if no supported language found
  • User preference stored in localStorage for persistence across sessions

Manual Switching:

  • Language switcher component with dropdown menu
  • Route parameter support (/en/login, /ar/login) for deep linking
  • Session-based locale persistence (user preference survives page refresh)

Locale Loading:

  • Translation files will be loaded dynamically via import() for code splitting
  • Fallback to English for missing keys in any locale
// i18n service implementation
class I18nService {
  private currentLocale: string = 'en-US';
  private translations: Record<string, any> = {};
  
  async loadLocale(locale: string) {
    try {
      const translationModule = await import(`../locales/${locale}.json`);
      this.translations[locale] = translationModule.default;
      this.currentLocale = locale;
    } catch (error) {
      console.warn(`Failed to load locale ${locale}`, error);
      this.currentLocale = 'en-US';
    }
  }
  
  t(key: string, params?: Record<string, any>): string {
    const keys = key.split('.');
    let translation = this.translations[this.currentLocale];
    
    for (const k of keys) {
      translation = translation?.[k];
    }
    
    if (!translation) {
      return key; // Fallback to key name
    }
    
    // Handle parameter interpolation
    if (params && typeof translation === 'string') {
      return translation.replace(/\{([^}]+)\}/g, (match, param) => 
        params[param] || match
      );
    }
    
    return translation;
  }
}

i18n Integration with Authentication Components

Authentication forms will dynamically adjust based on locale:

// AuthForm component with i18n support
const AuthForm = ({ 
  mode, 
  locale 
}: { 
  mode: 'login' | 'signup' | 'forgot'; 
  locale: string; 
}) => {
  const t = useTranslation(locale);
  
  return (
    <form>
      <h2>{t(`auth.${mode}.title`)}</h2>
      {/* Dynamic labels based on locale */}
      <label htmlFor="email">{t('auth.login.email_label')}</label>
      <input id="email" type="email" />
      {/* ... other fields */}
    </form>
  );
};

Translation File Structure

Each locale will have a structured JSON file:

{
  "auth": {
    "login": {
      "title": "Sign In",
      "email_label": "Email Address",
      "password_label": "Password",
      "submit_button": "Sign In"
    },
    "signup": {
      "title": "Create Account",
      "email_label": "Email Address"
    }
  }
}

Testing i18n Implementation

  • Translation key coverage testing (ensure all keys are present in each locale)
  • RTL layout verification using visual regression tests
  • Dynamic locale switching with Cypress E2E tests
  • Missing translation fallback validation

Performance Considerations

  • Translation files will be split by locale and loaded on-demand
  • Fallback mechanism to prevent broken UI during translation loading
  • Caching strategy for frequently accessed translations
  • Bundle size optimization through selective locale loading

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The authentication manager should capture key performance indicators to understand user experience and system health:

  • Authentication Flow Duration: Time from initial auth request to successful login/logout completion (target: <2s for 90% of flows)
  • Token Refresh Latency: Time to refresh access tokens (target: <500ms for 95% of refreshes)
  • Auth State Transitions: Count and timing of state changes (e.g., from idle to authenticated)
  • Network Error Rates: Percentage of failed auth API calls (target: <1% error rate)
  • Session Duration: Average time users remain authenticated before logout (target: >30min for 80% of sessions)

Error Tracking Integration

Integrate with error monitoring platforms like Sentry or Datadog to capture:

  • Authentication API failures (401, 403, 500 responses)
  • Token parsing and validation errors
  • Session expiration handling failures
  • Network connectivity issues during auth flows

Custom Events and User Journey Tracking

Implement custom event tracking for key authentication user journeys:

// Example event tracking structure
interface AuthEvent {
  eventName: 'auth_login_start' | 'auth_login_success' | 'auth_login_failure' |
           'auth_logout_start' | 'auth_logout_success' | 'auth_token_refresh' |
           'auth_session_expired' | 'auth_redirect_to_login';
  timestamp: number;
  userId?: string;
  metadata?: Record<string, any>;
}

Performance Monitoring and Regression Detection

  • Bundle Size Tracking: Monitor auth manager bundle size (target: <5KB gzipped)
  • Memory Usage: Track memory consumption during token refresh cycles
  • CPU Usage: Monitor auth flow impact on main thread (target: <50ms per auth operation)
  • Regression Detection: Alert on >10% increase in authentication flow duration

Integration Points

  • Sentry: For error tracking and performance monitoring
  • Datadog RUM: For real user experience monitoring
  • Custom Analytics Service: For tracking authentication journey completion rates and user behavior patterns

Alerting Strategy

  • Critical Alerts: Auth API failures >5% of requests
  • Warning Alerts: Token refresh latency >1s for 5% of users
  • Info Alerts: Session expiration rate >20% of active sessions

Data Privacy Considerations

All monitoring data should exclude sensitive authentication information:

  • User credentials (passwords, tokens) are never sent to monitoring services
  • Session IDs are hashed before logging
  • User identifiers are anonymized where possible

Dashboard Requirements

Dashboard should include:

  • Authentication success/failure rates over time
  • Token refresh frequency and latency trends
  • User session duration distributions
  • Error rate by authentication method (OAuth, SSO, password)
  • Performance metrics for key auth operations

This monitoring approach enables proactive identification of authentication issues while maintaining user privacy and system performance standards expected in FAANG environments.

13. File & Folder Structure

Proposed Directory Structure

src/
├── features/
│   └── auth/
│       ├── components/
│       │   ├── LoginForm.tsx
│       │   ├── RegisterForm.tsx
│       │   └── AuthLayout.tsx
│       ├── hooks/
│       │   ├── useAuth.ts
│       │   └── useAuthRedirect.ts
│       ├── services/
│       │   ├── authClient.ts
│       │   └── tokenManager.ts
│       ├── types/
│       │   └── auth.types.ts
│       ├── utils/
│       │   └── authUtils.ts
│       └── index.ts
├── shared/
│   ├── hooks/
│   │   └── useLocalStorage.ts
│   ├── utils/
│   │   └── validation.ts
│   └── types/
│       └── common.types.ts
├── stores/
│   └── authStore.ts
└── routes/
    ├── protected/
    │   └── ProtectedRoute.tsx
    └── public/
        └── PublicRoute.tsx

Module Boundaries and Import Rules

  • Auth feature boundary: All authentication-related logic contained within features/auth/ to prevent cross-feature imports
  • Shared modules: Only shared utilities and types that don't depend on feature-specific logic can be imported from shared/
  • Store isolation: Auth store is self-contained and doesn't import from other feature stores
  • Barrel exports: features/auth/index.ts exports all public interfaces and components for clean imports

Shared vs Feature-Specific Code Separation

Shared code (src/shared/):

  • Local storage hook for persistence abstraction
  • Common validation utilities
  • Generic type definitions

Feature-specific code (src/features/auth/):

  • Authentication business logic and state management
  • Component implementations specific to auth flows
  • API client and token handling logic

This structure enables clean separation of concerns while maintaining a flat import hierarchy that prevents circular dependencies and supports scalable feature development.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React Query (TanStack Query)

  • Justification: Excellent for server state management, automatic caching, background updates, and optimistic UI patterns. Provides built-in stale-while-revalidate and cache invalidation strategies that are critical for authentication flows.
  • Bundle size: ~15KB gzipped
  • Alternative considered: Redux Toolkit - Too verbose for simple auth state management; React Query's built-in caching and refetching is more appropriate.

2. Axios

  • Justification: Robust HTTP client with built-in request/response interceptors, automatic JSON parsing, and extensive error handling. Ideal for consistent API communication in auth flows.
  • Bundle size: ~20KB gzipped
  • Alternative considered: Fetch API - Requires more boilerplate for interceptors and error handling.

3. React Router DOM

  • Justification: Industry-standard routing solution with built-in authentication guards, nested routes, and proper URL handling. Essential for implementing protected vs public routes.
  • Bundle size: ~15KB gzipped
  • Alternative considered: Reach Router - Less mature ecosystem and fewer auth-specific utilities.

4. Zustand

  • Justification: Lightweight state management with minimal boilerplate, ideal for managing auth session state and user metadata. Better performance than Redux for simple use cases.
  • Bundle size: ~2KB gzipped
  • Alternative considered: Context API - Requires more boilerplate for state updates and subscriptions.

5. React Hook Form

  • Justification: High-performance form handling with built-in validation, error management, and field-level reactivity. Critical for login/signup forms.
  • Bundle size: ~10KB gzipped
  • Alternative considered: Formik - More verbose and heavier for simple auth forms.

6. React Toastify

  • Justification: Lightweight, customizable toast notifications for auth success/error messages with minimal setup. Essential for user feedback in auth flows.
  • Bundle size: ~5KB gzipped
  • Alternative considered: Custom toast implementation - Would require significant boilerplate and maintenance.

Alternatives Considered and Rejected

1. Redux Toolkit

  • Rejected: Too heavy for simple auth state management. Auth flows are typically read-heavy and don't require the complexity of Redux's middleware ecosystem.
  • Rationale: Auth state is relatively simple and can be managed with Zustand or Context API with less boilerplate.

2. Auth0 SDK

  • Rejected: Overkill for custom auth implementation. Would introduce unnecessary dependencies and complexity for a self-hosted solution.
  • Rationale: Custom auth implementation provides better control over security and user experience.

3. Firebase Auth

  • Rejected: Not suitable for custom authentication flows that require specific backend integration.
  • Rationale: Custom auth systems need to maintain direct control over token handling and session management.

Licensing and Bundle Size Impact Assessment

Bundle Size Budget Allocation:

  • Core auth stack (React Query + Axios + Zustand + React Router): ~50KB gzipped
  • Form handling and UI components (React Hook Form + React Toastify): ~15KB gzipped
  • Total auth-related dependencies: ~65KB gzipped (under 100KB target)

Licensing Compliance:

  • All dependencies are MIT licensed (except React which is BSD)
  • No proprietary or restrictive licensing concerns
  • Open source community support ensures long-term maintainability

Tree-shaking Strategy:

  • All libraries support tree-shaking for minimal bundle impact
  • React Query and Zustand allow selective imports to reduce size
  • Axios provides modular import options for HTTP client customization

Performance Considerations

Bundle Size Budget:

  • Target: 100KB total for auth-related dependencies (excluding React ecosystem)
  • Current estimate: ~65KB gzipped with proper tree-shaking
  • Room for optimization through selective imports and custom builds

Loading Strategy:

  • Route-based code splitting will lazy-load auth components
  • Auth-related modules will be bundled separately from core app
  • Critical path dependencies (Zustand, React Query) loaded with main bundle

Dependency Maintenance:

  • Regular security audit of dependencies using npm audit
  • Automated dependency update monitoring with renovate or dependabot
  • Migration path documented for major version changes

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Token storage strategy: Should we use HTTP-only cookies (more secure but require backend support) or localStorage/sessionStorage (easier to implement but more vulnerable to XSS). This decision requires backend API support and security team approval.
  • Refresh token policy: How aggressively should we refresh tokens? Should we refresh on every API call or use a time-based approach? This affects both security and performance.
  • Multi-tab synchronization: Should authentication state be synchronized across browser tabs? This adds complexity but improves user experience when logging in/out across multiple windows.
  • Passwordless authentication support: Do we need to support passwordless flows (magic links, biometrics)? This significantly impacts the authentication flow design and security considerations.

Known trade-offs made in this design

  • Security vs. Convenience: Using localStorage for tokens provides better UX (easier to debug, no CORS issues) but introduces XSS vulnerabilities. HTTP-only cookies would be more secure but require additional backend infrastructure.
  • State Management Simplicity vs. Scalability: Using React Context for authentication state provides clean implementation but may not scale well to complex multi-user scenarios or server-side rendering. Redux/Zustand would be better for large-scale applications.
  • Client-side Only vs. Full Auth System: This design assumes a client-side only authentication manager, which means we're not handling server-side session management or OAuth flows. For production use, a full auth system with backend support is required.
  • Single Auth Flow vs. Multi-Provider: The design assumes a single authentication flow (username/password). In reality, most applications support multiple providers (Google, GitHub, etc.) which would require additional complexity in the state management and UI.

Risks and mitigations

  • XSS Vulnerability: Storing tokens in localStorage makes the application vulnerable to XSS attacks. Mitigation includes implementing Content Security Policy (CSP) headers, input sanitization, and regular security audits.
  • Token Theft: If tokens are stolen (through XSS or man-in-the-middle attacks), the attacker gains access to the user's account. Mitigation includes short-lived access tokens, refresh token rotation, and secure HTTP headers.
  • Session Management Issues: If tokens expire during an active session, users may lose their authentication state unexpectedly. Mitigation includes proactive token refresh and user-friendly error handling with clear re-authentication prompts.
  • Cross-Origin Issues: If the application is embedded in iframes or used across domains, authentication tokens may not be accessible. Mitigation includes proper CORS configuration and secure cookie settings.
  • Offline State Handling: If the user loses network connectivity during authentication, the system needs to handle this gracefully. Mitigation includes offline detection and proper error states with retry mechanisms.

Technical debt acknowledged

  • Limited Auth Provider Support: The current implementation only handles basic username/password authentication. Adding support for OAuth providers, SAML, or multi-factor authentication would require significant refactoring of the current architecture.
  • No Session Persistence Across Browser Sessions: The design doesn't handle persistence across browser restarts or long-term session management, which is a common requirement in enterprise applications.
  • No Role-Based Access Control (RBAC): The authentication manager currently only handles login/logout but doesn't implement RBAC or permission checking, which is essential for most enterprise applications.
  • No Token Refresh Mechanism: The current implementation doesn't handle automatic token refresh, which means users will be logged out after their access tokens expire, requiring manual re-authentication.
  • No Multi-User Support: The design assumes a single user session, but enterprise applications often need to support multiple concurrent users or switch between accounts.
  • No Audit Logging: There's no built-in capability to log authentication events for security auditing or compliance purposes.
  • No Password Policy Enforcement: The client-side implementation doesn't enforce password strength or complexity requirements, which should be handled by the backend but requires client-side validation for better UX.
  • No Rate Limiting Protection: The authentication manager doesn't implement rate limiting or brute force protection, which is crucial for preventing account compromise attacks.

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

    JWT Token Management System Design

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

  • 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.