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
- Never trust client state - always revalidate on server
- Short-lived tokens - minimize blast radius of token theft
- Defense in depth - client validation + server validation
- Permission checks everywhere - route, component, and API levels
- Sign requests - prevent replay and tampering
- Strict CSP - prevent XSS and injection attacks
- Monitor and alert - detect attacks in real-time
- Encrypt sensitive state - protect data at rest in browser
What did you think?