Back to Blog

Zero Trust Frontend Architecture: Client-Side Security, API Gateway Patterns, and Defense in Depth for React Applications

April 18, 2026119 min read0 views

Zero Trust Frontend Architecture: Client-Side Security, API Gateway Patterns, and Defense in Depth for React Applications

Zero Trust assumes breach. Every request, component, and data flow is treated as potentially compromised. This architecture eliminates implicit trust in the client, enforces continuous verification, and implements security at every layer. This deep dive covers implementing Zero Trust principles in frontend applications with React.


Zero Trust Principles for Frontend

Zero Trust Frontend Architecture
┌─────────────────────────────────────────────────────────────────┐
│                                                                  │
│  Core Principles:                                                │
│  1. Never trust, always verify                                   │
│  2. Assume breach                                                │
│  3. Verify explicitly                                            │
│  4. Use least privilege access                                   │
│  5. Assume the network is hostile                                │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │                    Frontend Application                     │ │
│  │                                                              │ │
│  │  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐    │ │
│  │  │ Auth State   │   │ API Layer    │   │ Data Layer   │    │ │
│  │  │ • Token      │   │ • Signed reqs│   │ • Encrypted  │    │ │
│  │  │ • Refresh    │   │ • Validated  │   │ • Validated  │    │ │
│  │  │ • Session    │   │ • Retried    │   │ • Sanitized  │    │ │
│  │  └──────┬───────┘   └──────┬───────┘   └──────┬───────┘    │ │
│  │         │                  │                  │             │ │
│  │         └──────────────────┼──────────────────┘             │ │
│  │                            │                                 │ │
│  └────────────────────────────┼─────────────────────────────────┘ │
│                               │                                   │
│                     ┌─────────▼─────────┐                        │
│                     │   API Gateway     │                        │
│                     │ • AuthN / AuthZ   │                        │
│                     │ • Rate limiting   │                        │
│                     │ • Request signing │                        │
│                     │ • Threat detect   │                        │
│                     └─────────┬─────────┘                        │
│                               │                                   │
│                     ┌─────────▼─────────┐                        │
│                     │  Backend Services │                        │
│                     │ • Re-validate     │                        │
│                     │ • Never trust     │                        │
│                     │   frontend        │                        │
│                     └───────────────────┘                        │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Authentication Architecture

Short-Lived Token Strategy

interface TokenConfig {
  accessTokenTTL: number;      // 15 minutes
  refreshTokenTTL: number;     // 7 days
  idleTimeout: number;         // 30 minutes
  absoluteTimeout: number;     // 24 hours
}

interface AuthTokens {
  accessToken: string;
  refreshToken: string;
  expiresAt: number;
  issuedAt: number;
}

class ZeroTrustAuthManager {
  private config: TokenConfig;
  private tokens: AuthTokens | null = null;
  private refreshPromise: Promise<AuthTokens> | null = null;
  private lastActivity: number = Date.now();
  private sessionStart: number = Date.now();

  constructor(config: TokenConfig) {
    this.config = config;
    this.setupActivityTracking();
    this.setupPeriodicValidation();
  }

  // Get valid access token (auto-refresh if needed)
  async getAccessToken(): Promise<string | null> {
    // Check session timeouts
    if (this.isSessionExpired()) {
      await this.logout('Session expired');
      return null;
    }

    if (!this.tokens) {
      return null;
    }

    // Token is still valid
    if (Date.now() < this.tokens.expiresAt - 60000) {
      return this.tokens.accessToken;
    }

    // Need to refresh
    return this.refreshAccessToken();
  }

  private async refreshAccessToken(): Promise<string | null> {
    // Prevent concurrent refreshes
    if (this.refreshPromise) {
      const tokens = await this.refreshPromise;
      return tokens.accessToken;
    }

    this.refreshPromise = this.doRefresh();

    try {
      const tokens = await this.refreshPromise;
      this.tokens = tokens;
      return tokens.accessToken;
    } catch (error) {
      await this.logout('Token refresh failed');
      return null;
    } finally {
      this.refreshPromise = null;
    }
  }

  private async doRefresh(): Promise<AuthTokens> {
    if (!this.tokens?.refreshToken) {
      throw new Error('No refresh token');
    }

    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        refreshToken: this.tokens.refreshToken,
      }),
      credentials: 'include',
    });

    if (!response.ok) {
      throw new Error(`Refresh failed: ${response.status}`);
    }

    const data = await response.json();

    return {
      accessToken: data.accessToken,
      refreshToken: data.refreshToken,
      expiresAt: Date.now() + data.expiresIn * 1000,
      issuedAt: Date.now(),
    };
  }

  private isSessionExpired(): boolean {
    const now = Date.now();

    // Idle timeout
    if (now - this.lastActivity > this.config.idleTimeout) {
      return true;
    }

    // Absolute timeout
    if (now - this.sessionStart > this.config.absoluteTimeout) {
      return true;
    }

    return false;
  }

  private setupActivityTracking(): void {
    const updateActivity = () => {
      this.lastActivity = Date.now();
    };

    window.addEventListener('click', updateActivity, { passive: true });
    window.addEventListener('keydown', updateActivity, { passive: true });
    window.addEventListener('scroll', updateActivity, { passive: true });
    window.addEventListener('mousemove', updateActivity, { passive: true });
  }

  private setupPeriodicValidation(): void {
    // Validate session every minute
    setInterval(() => {
      if (this.isSessionExpired()) {
        this.logout('Session timeout');
      }
    }, 60000);
  }

  async logout(reason?: string): Promise<void> {
    // Revoke tokens server-side
    if (this.tokens?.refreshToken) {
      try {
        await fetch('/api/auth/logout', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            refreshToken: this.tokens.refreshToken,
          }),
        });
      } catch {
        // Log but don't block logout
      }
    }

    // Clear local state
    this.tokens = null;
    this.sessionStart = 0;
    this.lastActivity = 0;

    // Clear storage
    sessionStorage.clear();
    localStorage.removeItem('auth');

    // Redirect to login
    window.location.href = `/login?reason=${encodeURIComponent(reason || '')}`;
  }
}

Secure Token Storage

// NEVER store tokens in localStorage for sensitive applications
// Use secure, httpOnly cookies or memory-only storage

class SecureTokenStorage {
  private memoryToken: string | null = null;
  private encryptionKey: CryptoKey | null = null;

  async initialize(): Promise<void> {
    // Generate per-session encryption key
    this.encryptionKey = await crypto.subtle.generateKey(
      { name: 'AES-GCM', length: 256 },
      false,  // Not extractable
      ['encrypt', 'decrypt']
    );
  }

  async storeToken(token: string): Promise<void> {
    // Store in memory
    this.memoryToken = token;

    // Optionally encrypt and store in sessionStorage
    // (survives page refresh but not tab close)
    if (this.encryptionKey) {
      const encrypted = await this.encrypt(token);
      sessionStorage.setItem('__enc_token', encrypted);
    }
  }

  async getToken(): Promise<string | null> {
    // Prefer memory
    if (this.memoryToken) {
      return this.memoryToken;
    }

    // Try to recover from sessionStorage
    const encrypted = sessionStorage.getItem('__enc_token');
    if (encrypted && this.encryptionKey) {
      try {
        this.memoryToken = await this.decrypt(encrypted);
        return this.memoryToken;
      } catch {
        // Decryption failed (key changed)
        sessionStorage.removeItem('__enc_token');
      }
    }

    return null;
  }

  private async encrypt(data: string): Promise<string> {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encoded = new TextEncoder().encode(data);

    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.encryptionKey!,
      encoded
    );

    // Combine IV and ciphertext
    const combined = new Uint8Array(iv.length + encrypted.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(encrypted), iv.length);

    return btoa(String.fromCharCode(...combined));
  }

  private async decrypt(data: string): Promise<string> {
    const combined = new Uint8Array(
      atob(data).split('').map(c => c.charCodeAt(0))
    );

    const iv = combined.slice(0, 12);
    const ciphertext = combined.slice(12);

    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv },
      this.encryptionKey!,
      ciphertext
    );

    return new TextDecoder().decode(decrypted);
  }

  clear(): void {
    this.memoryToken = null;
    sessionStorage.removeItem('__enc_token');
  }
}

API Request Security

Signed Request Pattern

interface SignedRequestConfig {
  method: string;
  url: string;
  body?: unknown;
  timestamp: number;
  nonce: string;
}

class SecureApiClient {
  private authManager: ZeroTrustAuthManager;
  private signingKey: CryptoKey | null = null;

  constructor(authManager: ZeroTrustAuthManager) {
    this.authManager = authManager;
  }

  async request<T>(
    method: string,
    url: string,
    options: RequestOptions = {}
  ): Promise<T> {
    const accessToken = await this.authManager.getAccessToken();
    if (!accessToken) {
      throw new AuthenticationError('Not authenticated');
    }

    // Generate request metadata
    const timestamp = Date.now();
    const nonce = crypto.randomUUID();

    // Create request signature
    const signature = await this.signRequest({
      method,
      url,
      body: options.body,
      timestamp,
      nonce,
    });

    // Build headers
    const headers: Record<string, string> = {
      'Authorization': `Bearer ${accessToken}`,
      'X-Request-Timestamp': String(timestamp),
      'X-Request-Nonce': nonce,
      'X-Request-Signature': signature,
      'Content-Type': 'application/json',
      ...options.headers,
    };

    // Add CSRF token if available
    const csrfToken = this.getCsrfToken();
    if (csrfToken) {
      headers['X-CSRF-Token'] = csrfToken;
    }

    const response = await fetch(url, {
      method,
      headers,
      body: options.body ? JSON.stringify(options.body) : undefined,
      credentials: 'include',  // Send cookies for CSRF
    });

    // Handle response
    if (response.status === 401) {
      // Token may have been revoked server-side
      await this.authManager.logout('Session invalidated');
      throw new AuthenticationError('Session invalidated');
    }

    if (!response.ok) {
      throw new ApiError(response.status, await response.text());
    }

    // Verify response signature
    const responseSignature = response.headers.get('X-Response-Signature');
    if (responseSignature) {
      const responseBody = await response.clone().text();
      const validSignature = await this.verifyResponseSignature(
        responseBody,
        responseSignature
      );

      if (!validSignature) {
        throw new SecurityError('Response signature invalid');
      }
    }

    return response.json();
  }

  private async signRequest(config: SignedRequestConfig): Promise<string> {
    // Create canonical request string
    const canonical = [
      config.method.toUpperCase(),
      config.url,
      config.timestamp,
      config.nonce,
      config.body ? JSON.stringify(config.body) : '',
    ].join('\n');

    // Sign with HMAC-SHA256
    const encoder = new TextEncoder();
    const data = encoder.encode(canonical);

    if (!this.signingKey) {
      // In production, derive from session
      this.signingKey = await this.deriveSigningKey();
    }

    const signature = await crypto.subtle.sign(
      'HMAC',
      this.signingKey,
      data
    );

    return btoa(String.fromCharCode(...new Uint8Array(signature)));
  }

  private async deriveSigningKey(): Promise<CryptoKey> {
    // Derive from access token or separate signing secret
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode('session-specific-secret'),
      { name: 'PBKDF2' },
      false,
      ['deriveBits', 'deriveKey']
    );

    return crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: new TextEncoder().encode('request-signing'),
        iterations: 100000,
        hash: 'SHA-256',
      },
      keyMaterial,
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign', 'verify']
    );
  }

  private getCsrfToken(): string | null {
    // Get from cookie or meta tag
    const cookie = document.cookie
      .split('; ')
      .find(row => row.startsWith('csrf_token='));

    return cookie?.split('=')[1] || null;
  }

  private async verifyResponseSignature(
    body: string,
    signature: string
  ): Promise<boolean> {
    // Server-side verification would use different key
    // This is client-side validation of server response
    return true;  // Placeholder
  }
}

interface RequestOptions {
  headers?: Record<string, string>;
  body?: unknown;
}

class AuthenticationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AuthenticationError';
  }
}

class ApiError extends Error {
  status: number;

  constructor(status: number, message: string) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
  }
}

class SecurityError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'SecurityError';
  }
}

Authorization at Every Layer

Component-Level Authorization

import React, { createContext, useContext, ReactNode } from 'react';

interface Permission {
  resource: string;
  action: 'create' | 'read' | 'update' | 'delete' | 'admin';
  scope?: string;
}

interface AuthorizationContext {
  permissions: Permission[];
  hasPermission: (resource: string, action: string, scope?: string) => boolean;
  hasRole: (role: string) => boolean;
  roles: string[];
}

const AuthzContext = createContext<AuthorizationContext | null>(null);

// Permission-based component wrapper
function Authorized({
  resource,
  action,
  scope,
  fallback = null,
  children,
}: {
  resource: string;
  action: string;
  scope?: string;
  fallback?: ReactNode;
  children: ReactNode;
}): JSX.Element {
  const authz = useContext(AuthzContext);

  if (!authz || !authz.hasPermission(resource, action, scope)) {
    return <>{fallback}</>;
  }

  return <>{children}</>;
}

// Role-based component wrapper
function RequireRole({
  role,
  fallback = null,
  children,
}: {
  role: string;
  fallback?: ReactNode;
  children: ReactNode;
}): JSX.Element {
  const authz = useContext(AuthzContext);

  if (!authz || !authz.hasRole(role)) {
    return <>{fallback}</>;
  }

  return <>{children}</>;
}

// Usage in components
function AdminDashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* Only visible with read permission */}
      <Authorized resource="users" action="read">
        <UserList />
      </Authorized>

      {/* Only visible with admin role */}
      <RequireRole role="admin">
        <AdminSettings />
      </RequireRole>

      {/* With fallback for unauthorized */}
      <Authorized
        resource="billing"
        action="read"
        fallback={<p>Contact admin for billing access</p>}
      >
        <BillingInfo />
      </Authorized>
    </div>
  );
}

// Permission provider implementation
function AuthorizationProvider({
  children,
}: {
  children: ReactNode;
}): JSX.Element {
  const [permissions, setPermissions] = useState<Permission[]>([]);
  const [roles, setRoles] = useState<string[]>([]);

  useEffect(() => {
    // Fetch permissions from server on mount and periodically
    const fetchPermissions = async () => {
      const response = await secureApiClient.request<{
        permissions: Permission[];
        roles: string[];
      }>('GET', '/api/auth/permissions');

      setPermissions(response.permissions);
      setRoles(response.roles);
    };

    fetchPermissions();

    // Re-fetch every 5 minutes
    const interval = setInterval(fetchPermissions, 300000);
    return () => clearInterval(interval);
  }, []);

  const hasPermission = useCallback(
    (resource: string, action: string, scope?: string): boolean => {
      return permissions.some(p =>
        p.resource === resource &&
        p.action === action &&
        (scope === undefined || p.scope === scope || p.scope === '*')
      );
    },
    [permissions]
  );

  const hasRole = useCallback(
    (role: string): boolean => roles.includes(role),
    [roles]
  );

  return (
    <AuthzContext.Provider value={{ permissions, hasPermission, hasRole, roles }}>
      {children}
    </AuthzContext.Provider>
  );
}

Route-Level Authorization

import { Navigate, useLocation } from 'react-router-dom';

interface ProtectedRouteProps {
  children: ReactNode;
  requiredPermissions?: Array<{ resource: string; action: string }>;
  requiredRoles?: string[];
  requireAll?: boolean;  // true = AND, false = OR
}

function ProtectedRoute({
  children,
  requiredPermissions = [],
  requiredRoles = [],
  requireAll = true,
}: ProtectedRouteProps): JSX.Element {
  const authz = useContext(AuthzContext);
  const location = useLocation();

  if (!authz) {
    // Auth context not loaded
    return <LoadingSpinner />;
  }

  // Check permissions
  const permissionChecks = requiredPermissions.map(
    p => authz.hasPermission(p.resource, p.action)
  );

  const roleChecks = requiredRoles.map(
    r => authz.hasRole(r)
  );

  const allChecks = [...permissionChecks, ...roleChecks];

  const authorized = requireAll
    ? allChecks.every(Boolean)
    : allChecks.some(Boolean);

  if (!authorized) {
    // Log unauthorized access attempt
    console.warn('Unauthorized route access', {
      path: location.pathname,
      requiredPermissions,
      requiredRoles,
    });

    return <Navigate to="/unauthorized" state={{ from: location }} replace />;
  }

  return <>{children}</>;
}

// Route configuration
const routes = [
  {
    path: '/admin',
    element: (
      <ProtectedRoute requiredRoles={['admin']}>
        <AdminLayout />
      </ProtectedRoute>
    ),
    children: [
      {
        path: 'users',
        element: (
          <ProtectedRoute
            requiredPermissions={[
              { resource: 'users', action: 'read' },
            ]}
          >
            <UserManagement />
          </ProtectedRoute>
        ),
      },
      {
        path: 'billing',
        element: (
          <ProtectedRoute
            requiredPermissions={[
              { resource: 'billing', action: 'read' },
              { resource: 'billing', action: 'update' },
            ]}
          >
            <BillingManagement />
          </ProtectedRoute>
        ),
      },
    ],
  },
];

Input Validation and Sanitization

Client-Side Validation (Defense in Depth)

import { z } from 'zod';
import DOMPurify from 'dompurify';

// Define strict schemas for all user inputs
const UserInputSchema = z.object({
  email: z.string().email().max(255),
  name: z.string()
    .min(1)
    .max(100)
    .regex(/^[a-zA-Z\s'-]+$/),  // Only safe characters
  bio: z.string()
    .max(1000)
    .transform(val => DOMPurify.sanitize(val)),  // Sanitize HTML
  website: z.string()
    .url()
    .refine(
      url => ['http:', 'https:'].includes(new URL(url).protocol),
      'Only HTTP(S) URLs allowed'
    )
    .optional(),
});

// Form component with validation
function UserProfileForm() {
  const [errors, setErrors] = useState<Record<string, string>>({});

  const handleSubmit = async (formData: FormData) => {
    const input = {
      email: formData.get('email'),
      name: formData.get('name'),
      bio: formData.get('bio'),
      website: formData.get('website') || undefined,
    };

    // Validate
    const result = UserInputSchema.safeParse(input);

    if (!result.success) {
      const fieldErrors: Record<string, string> = {};
      result.error.errors.forEach(err => {
        fieldErrors[err.path[0]] = err.message;
      });
      setErrors(fieldErrors);
      return;
    }

    // Submit validated and sanitized data
    try {
      await secureApiClient.request('PUT', '/api/profile', {
        body: result.data,
      });
    } catch (error) {
      // Handle error
    }
  };

  // Form JSX...
}

// XSS-safe rendering
function SafeHtmlContent({ html }: { html: string }) {
  const sanitized = useMemo(
    () => DOMPurify.sanitize(html, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
      ALLOWED_ATTR: ['href', 'target', 'rel'],
      ALLOW_DATA_ATTR: false,
    }),
    [html]
  );

  return (
    <div
      dangerouslySetInnerHTML={{ __html: sanitized }}
    />
  );
}

URL Parameter Validation

import { useSearchParams, useParams } from 'react-router-dom';

// Validated URL parameters
function useValidatedParams<T extends z.ZodType>(
  schema: T
): z.infer<T> | null {
  const params = useParams();

  const result = schema.safeParse(params);

  if (!result.success) {
    console.error('Invalid URL params', result.error);
    return null;
  }

  return result.data;
}

function useValidatedSearchParams<T extends z.ZodType>(
  schema: T
): z.infer<T> | null {
  const [searchParams] = useSearchParams();
  const params = Object.fromEntries(searchParams.entries());

  const result = schema.safeParse(params);

  if (!result.success) {
    console.error('Invalid search params', result.error);
    return null;
  }

  return result.data;
}

// Usage
const ProductParamsSchema = z.object({
  productId: z.string().uuid(),
});

const ProductSearchSchema = z.object({
  category: z.string().optional(),
  page: z.coerce.number().int().positive().default(1),
  sort: z.enum(['price', 'name', 'date']).default('name'),
});

function ProductPage() {
  const params = useValidatedParams(ProductParamsSchema);
  const search = useValidatedSearchParams(ProductSearchSchema);

  if (!params) {
    return <Navigate to="/404" />;
  }

  // Use validated params safely
  return <ProductDetail id={params.productId} {...search} />;
}

Content Security Policy

Strict CSP Implementation

// Generate CSP header with nonces
function generateCSP(nonce: string): string {
  const directives = {
    'default-src': ["'self'"],
    'script-src': [
      "'self'",
      `'nonce-${nonce}'`,
      // No 'unsafe-inline' or 'unsafe-eval'
    ],
    'style-src': [
      "'self'",
      `'nonce-${nonce}'`,
    ],
    'img-src': [
      "'self'",
      'data:',
      'https://cdn.example.com',
    ],
    'font-src': [
      "'self'",
      'https://fonts.gstatic.com',
    ],
    'connect-src': [
      "'self'",
      'https://api.example.com',
      'wss://realtime.example.com',
    ],
    'frame-src': ["'none'"],
    'frame-ancestors': ["'none'"],
    'object-src': ["'none'"],
    'base-uri': ["'self'"],
    'form-action': ["'self'"],
    'upgrade-insecure-requests': [],
    'block-all-mixed-content': [],
  };

  return Object.entries(directives)
    .map(([key, values]) =>
      values.length > 0
        ? `${key} ${values.join(' ')}`
        : key
    )
    .join('; ');
}

// Express middleware
function cspMiddleware(req: Request, res: Response, next: NextFunction): void {
  // Generate unique nonce per request
  const nonce = crypto.randomBytes(16).toString('base64');

  // Store for use in templates
  res.locals.cspNonce = nonce;

  // Set CSP header
  res.setHeader('Content-Security-Policy', generateCSP(nonce));

  // Additional security headers
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '0');  // Deprecated, but set to 0
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.setHeader(
    'Permissions-Policy',
    'accelerometer=(), camera=(), geolocation=(), microphone=()'
  );

  next();
}

// React component for inline scripts with nonce
function NonceScript({
  children,
  nonce,
}: {
  children: string;
  nonce: string;
}) {
  return (
    <script
      nonce={nonce}
      dangerouslySetInnerHTML={{ __html: children }}
    />
  );
}

Secure State Management

Encrypted Client-Side State

// Encrypt sensitive state before storage
class SecureStateManager<T> {
  private encryptionKey: CryptoKey | null = null;
  private storageKey: string;

  constructor(storageKey: string) {
    this.storageKey = storageKey;
  }

  async initialize(): Promise<void> {
    // Derive key from user session
    const sessionToken = await authManager.getAccessToken();
    if (!sessionToken) {
      throw new Error('Must be authenticated');
    }

    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode(sessionToken.substring(0, 32)),
      { name: 'PBKDF2' },
      false,
      ['deriveBits', 'deriveKey']
    );

    this.encryptionKey = await crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: new TextEncoder().encode(this.storageKey),
        iterations: 100000,
        hash: 'SHA-256',
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt', 'decrypt']
    );
  }

  async save(state: T): Promise<void> {
    if (!this.encryptionKey) {
      await this.initialize();
    }

    const json = JSON.stringify(state);
    const iv = crypto.getRandomValues(new Uint8Array(12));

    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.encryptionKey!,
      new TextEncoder().encode(json)
    );

    const combined = new Uint8Array(iv.length + encrypted.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(encrypted), iv.length);

    sessionStorage.setItem(
      this.storageKey,
      btoa(String.fromCharCode(...combined))
    );
  }

  async load(): Promise<T | null> {
    if (!this.encryptionKey) {
      await this.initialize();
    }

    const stored = sessionStorage.getItem(this.storageKey);
    if (!stored) {
      return null;
    }

    try {
      const combined = new Uint8Array(
        atob(stored).split('').map(c => c.charCodeAt(0))
      );

      const iv = combined.slice(0, 12);
      const ciphertext = combined.slice(12);

      const decrypted = await crypto.subtle.decrypt(
        { name: 'AES-GCM', iv },
        this.encryptionKey!,
        ciphertext
      );

      return JSON.parse(new TextDecoder().decode(decrypted));
    } catch {
      // Decryption failed - clear corrupted data
      sessionStorage.removeItem(this.storageKey);
      return null;
    }
  }

  clear(): void {
    sessionStorage.removeItem(this.storageKey);
  }
}

// Usage
const secureCart = new SecureStateManager<CartState>('secure_cart');
await secureCart.save({ items: [...] });
const cart = await secureCart.load();

Monitoring and Incident Response

Client-Side Security Monitoring

// Security event tracking
interface SecurityEvent {
  type: 'csp_violation' | 'auth_failure' | 'validation_error' |
        'suspicious_activity' | 'integrity_violation';
  severity: 'low' | 'medium' | 'high' | 'critical';
  details: Record<string, unknown>;
  timestamp: string;
  userAgent: string;
  url: string;
}

class SecurityMonitor {
  private events: SecurityEvent[] = [];
  private flushInterval: NodeJS.Timer;

  constructor() {
    this.setupCSPReporting();
    this.setupIntegrityChecking();
    this.flushInterval = setInterval(() => this.flush(), 30000);
  }

  private setupCSPReporting(): void {
    document.addEventListener('securitypolicyviolation', (event) => {
      this.report({
        type: 'csp_violation',
        severity: 'high',
        details: {
          violatedDirective: event.violatedDirective,
          blockedURI: event.blockedURI,
          sourceFile: event.sourceFile,
          lineNumber: event.lineNumber,
          columnNumber: event.columnNumber,
        },
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent,
        url: window.location.href,
      });
    });
  }

  private setupIntegrityChecking(): void {
    // Monitor for DOM manipulation attacks
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (mutation.type === 'childList') {
          for (const node of Array.from(mutation.addedNodes)) {
            if (node instanceof HTMLScriptElement) {
              // Unexpected script added
              this.report({
                type: 'integrity_violation',
                severity: 'critical',
                details: {
                  tagName: 'SCRIPT',
                  src: node.src,
                  innerHTML: node.innerHTML.substring(0, 200),
                },
                timestamp: new Date().toISOString(),
                userAgent: navigator.userAgent,
                url: window.location.href,
              });
            }
          }
        }
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });
  }

  report(event: SecurityEvent): void {
    this.events.push(event);

    // Immediate flush for critical events
    if (event.severity === 'critical') {
      this.flush();
    }
  }

  private async flush(): Promise<void> {
    if (this.events.length === 0) return;

    const eventsToSend = [...this.events];
    this.events = [];

    try {
      await fetch('/api/security/events', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ events: eventsToSend }),
        keepalive: true,  // Survives page unload
      });
    } catch {
      // Queue for retry
      this.events.unshift(...eventsToSend);
    }
  }
}

const securityMonitor = new SecurityMonitor();

Security Checklist

Zero Trust Frontend Checklist
┌────────────────────────────────────────────────────────────────┐
│                                                                 │
│  ☐ Authentication                                               │
│    ☐ Short-lived access tokens (15 min)                        │
│    ☐ Secure token storage (memory preferred)                   │
│    ☐ Automatic token refresh                                   │
│    ☐ Session timeouts (idle + absolute)                        │
│    ☐ Server-side session validation                            │
│                                                                 │
│  ☐ Authorization                                                │
│    ☐ Permission checks at component level                      │
│    ☐ Route-level access control                                │
│    ☐ Periodic permission re-validation                         │
│    ☐ Audit logging for access attempts                         │
│                                                                 │
│  ☐ Request Security                                             │
│    ☐ CSRF tokens on state-changing requests                    │
│    ☐ Request signing for sensitive operations                  │
│    ☐ Timestamp + nonce for replay prevention                   │
│    ☐ Response validation                                       │
│                                                                 │
│  ☐ Input Validation                                             │
│    ☐ Schema validation on all inputs                           │
│    ☐ HTML sanitization with DOMPurify                          │
│    ☐ URL parameter validation                                  │
│    ☐ File upload restrictions                                  │
│                                                                 │
│  ☐ Content Security                                             │
│    ☐ Strict CSP with nonces                                    │
│    ☐ No unsafe-inline or unsafe-eval                           │
│    ☐ Subresource Integrity for CDN scripts                     │
│    ☐ X-Frame-Options: DENY                                     │
│                                                                 │
│  ☐ Monitoring                                                   │
│    ☐ CSP violation reporting                                   │
│    ☐ Authentication failure tracking                           │
│    ☐ DOM integrity monitoring                                  │
│    ☐ Anomaly detection                                         │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

Key Takeaways

  1. Never trust client state - always revalidate on server
  2. Short-lived tokens - minimize blast radius of token theft
  3. Defense in depth - client validation + server validation
  4. Permission checks everywhere - route, component, and API levels
  5. Sign requests - prevent replay and tampering
  6. Strict CSP - prevent XSS and injection attacks
  7. Monitor and alert - detect attacks in real-time
  8. Encrypt sensitive state - protect data at rest in browser

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.