DNAπŸ—οΈ System DesignAuthentication & Security Architecture
πŸ¦–DinosaurSystem DesignAuthenticationSecurity

Authentication & Security Architecture

OAuth flows, token storage trade-offs, XSS prevention, CSP policies β€” the security architecture that protects your users and your reputation.

Authentication & Security Architecture

Security is the one area where "it works" is never enough. A working auth flow with a token in localStorage is a data breach waiting to happen. Senior engineers understand the threat model and design defenses in layers β€” because attackers only need to find one gap.

Authentication Flows

OAuth 2.0 + OIDC

OAuth 2.0 handles authorization (what can you access). OpenID Connect (OIDC) adds authentication (who are you) on top.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  User    β”‚     β”‚  Your App    β”‚     β”‚  Identity Providerβ”‚
β”‚ (Browser)β”‚     β”‚  (SPA/BFF)   β”‚     β”‚  (Google, Okta)  β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚                  β”‚                      β”‚
     β”‚  Click "Login"   β”‚                      β”‚
     │─────────────────▢│                      β”‚
     β”‚                  β”‚  /authorize +        β”‚
     β”‚                  β”‚  code_challenge      β”‚
     β”‚                  │─────────────────────▢│
     β”‚                  β”‚                      β”‚
     β”‚  ◀──────────── Redirect to IdP ────────│
     β”‚                  β”‚                      β”‚
     β”‚  User logs in    β”‚                      β”‚
     │─────────────────────────────────────────▢│
     β”‚                  β”‚                      β”‚
     β”‚  ◀──── Redirect back with auth code ────│
     β”‚                  β”‚                      β”‚
     β”‚  Code to app     β”‚                      β”‚
     │─────────────────▢│                      β”‚
     β”‚                  β”‚  Exchange code +     β”‚
     β”‚                  β”‚  code_verifier for   β”‚
     β”‚                  β”‚  tokens              β”‚
     β”‚                  │─────────────────────▢│
     β”‚                  β”‚                      β”‚
     β”‚                  │◀── access_token ─────│
     β”‚                  │◀── id_token ─────────│
     β”‚                  │◀── refresh_token ────│
     β”‚                  β”‚                      β”‚
     │◀── Session ──────│                      β”‚
     β”‚                  β”‚                      β”‚

PKCE (Proof Key for Code Exchange)

SPAs can't store a client secret. PKCE replaces it with a cryptographic challenge:

async function generatePKCE() {
  const verifier = crypto.randomUUID() + crypto.randomUUID();
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
 
  return { verifier, challenge };
}
 
// Step 1: Start auth with challenge
const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier);
 
const authUrl = new URL('https://idp.example.com/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
window.location.href = authUrl.toString();
 
// Step 2: Exchange code with verifier
async function handleCallback(code: string) {
  const verifier = sessionStorage.getItem('pkce_verifier');
  const response = await fetch('https://idp.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      code_verifier: verifier!,
    }),
  });
  return response.json();
}

Token Storage: The Critical Decision

Where you store tokens determines your threat surface. There is no perfect option β€” only trade-offs.

Threat Analysis Matrix

StorageXSS VulnerableCSRF VulnerablePersists RefreshAccessible by JS
localStorageYESNoYesYes
sessionStorageYESNoNo (tab only)Yes
HttpOnly CookieNoYESYesNo
In-Memory (JS var)PartialNoNo (lost on refresh)Yes (same context)
HttpOnly Cookie + CSRF tokenNoNoYesNo

The Recommended Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Token Storage Architecture              β”‚
β”‚                                                     β”‚
β”‚  Access Token: In-memory JavaScript variable        β”‚
β”‚  β”œβ”€ Short-lived (5-15 minutes)                     β”‚
β”‚  β”œβ”€ Sent via Authorization header                   β”‚
β”‚  β”œβ”€ Lost on page refresh (by design)               β”‚
β”‚  └─ Not accessible via XSS (not in storage)        β”‚
β”‚                                                     β”‚
β”‚  Refresh Token: HttpOnly, Secure, SameSite cookie   β”‚
β”‚  β”œβ”€ Long-lived (days to weeks)                     β”‚
β”‚  β”œβ”€ Sent automatically by browser                   β”‚
β”‚  β”œβ”€ Not accessible via JavaScript                   β”‚
β”‚  └─ Protected from CSRF via SameSite=Strict        β”‚
β”‚                                                     β”‚
β”‚  On page load: silent refresh to get new access tokenβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
let accessToken: string | null = null;
 
async function getAccessToken(): Promise<string> {
  if (accessToken && !isExpired(accessToken)) {
    return accessToken;
  }
  return silentRefresh();
}
 
async function silentRefresh(): Promise<string> {
  const response = await fetch('/api/auth/refresh', {
    method: 'POST',
    credentials: 'include',
  });
 
  if (!response.ok) {
    accessToken = null;
    throw new AuthError('Session expired');
  }
 
  const { access_token } = await response.json();
  accessToken = access_token;
  scheduleRefresh(access_token);
  return access_token;
}
 
function scheduleRefresh(token: string) {
  const payload = JSON.parse(atob(token.split('.')[1]));
  const expiresIn = payload.exp * 1000 - Date.now();
  const refreshAt = expiresIn - 60_000; // refresh 1 minute before expiry
  setTimeout(silentRefresh, Math.max(refreshAt, 0));
}

JWT Anatomy

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.     ← Header (algorithm, type)
eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJBbGljZSJ9. ← Payload (claims)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c   ← Signature
 
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Header                                  β”‚
β”‚  { "alg": "RS256", "typ": "JWT" }       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Payload (Claims)                        β”‚
β”‚  {                                       β”‚
β”‚    "sub": "user_123",                    β”‚
β”‚    "name": "Alice",                      β”‚
β”‚    "email": "alice@example.com",         β”‚
β”‚    "roles": ["admin", "editor"],         β”‚
β”‚    "iat": 1711900800,                    β”‚
β”‚    "exp": 1711901700,                    β”‚
β”‚    "iss": "https://auth.example.com"     β”‚
β”‚  }                                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Signature                               β”‚
β”‚  RS256(                                  β”‚
β”‚    base64(header) + "." + base64(payload),β”‚
β”‚    private_key                           β”‚
β”‚  )                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Frontend verification: The frontend should NOT verify JWT signatures β€” that's the backend's job. The frontend can decode the payload to read claims (user info, roles, expiration) but should never trust the JWT without server validation.

function decodeJwtPayload(token: string): JwtPayload {
  const payload = token.split('.')[1];
  return JSON.parse(atob(payload));
}
 
function isExpired(token: string): boolean {
  const { exp } = decodeJwtPayload(token);
  return Date.now() >= exp * 1000;
}

Token Refresh Patterns

Silent Refresh (Recommended for SPAs)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Client  β”‚                          β”‚  Server  β”‚
β”‚          β”‚                          β”‚          β”‚
β”‚ Access token expired                β”‚          β”‚
β”‚          │── POST /auth/refresh ───▢│          β”‚
β”‚          β”‚   (HttpOnly cookie)      β”‚          β”‚
β”‚          β”‚                          β”‚ Validate β”‚
β”‚          β”‚                          β”‚ refresh  β”‚
β”‚          β”‚                          β”‚ token    β”‚
β”‚          │◀── New access token ─────│          β”‚
β”‚          β”‚                          β”‚          β”‚
β”‚ Store in memory                     β”‚          β”‚
β”‚ Schedule next refresh               β”‚          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Refresh Token Rotation

Each time a refresh token is used, the server issues a new one and invalidates the old one. If an attacker steals a refresh token and the legitimate user refreshes first, the attacker's token is invalid:

Legitimate user: refresh_token_v1 β†’ gets refresh_token_v2 (v1 invalidated)
Attacker:        refresh_token_v1 β†’ REJECTED (v1 already used)
                 Server detects token reuse β†’ invalidates ALL tokens for user

RBAC and ABAC

Role-Based Access Control (RBAC)

type Role = 'viewer' | 'editor' | 'admin' | 'superadmin';
 
const permissions: Record<Role, string[]> = {
  viewer:     ['read:posts', 'read:comments'],
  editor:     ['read:posts', 'read:comments', 'write:posts', 'write:comments'],
  admin:      ['read:posts', 'read:comments', 'write:posts', 'write:comments', 'manage:users'],
  superadmin: ['*'],
};
 
function hasPermission(userRole: Role, permission: string): boolean {
  const userPermissions = permissions[userRole];
  return userPermissions.includes('*') || userPermissions.includes(permission);
}

Attribute-Based Access Control (ABAC)

More granular β€” decisions based on user attributes, resource attributes, and context:

interface Policy {
  effect: 'allow' | 'deny';
  condition: (context: {
    user: User;
    resource: Resource;
    action: string;
    environment: { time: Date; ip: string };
  }) => boolean;
}
 
const policies: Policy[] = [
  {
    effect: 'allow',
    condition: ({ user, resource, action }) =>
      action === 'edit' && resource.ownerId === user.id,
  },
  {
    effect: 'allow',
    condition: ({ user, action }) =>
      action === 'edit' && user.roles.includes('admin'),
  },
  {
    effect: 'deny',
    condition: ({ environment }) => {
      const hour = environment.time.getHours();
      return hour < 6 || hour > 22; // deny outside business-adjacent hours
    },
  },
];

Route Protection

Client-Side Guards

function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
  const { user, isLoading } = useAuth();
 
  if (isLoading) return <PageSkeleton />;
  if (!user) return <Navigate to="/login" replace />;
  if (requiredRole && !hasRole(user, requiredRole)) return <Navigate to="/unauthorized" />;
 
  return children;
}
 
// Usage
<Route path="/admin/*" element={
  <ProtectedRoute requiredRole="admin">
    <AdminDashboard />
  </ProtectedRoute>
} />

Critical: Client-side guards are UX features, not security features. A determined user can bypass them. The server must enforce authorization on every API call.

Middleware (Next.js / Server-Side)

// middleware.ts (Next.js)
import { NextRequest, NextResponse } from 'next/server';
 
export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value;
 
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
 
  if (token) {
    const payload = decodeJwtPayload(token);
    if (request.nextUrl.pathname.startsWith('/admin') && !payload.roles.includes('admin')) {
      return NextResponse.redirect(new URL('/unauthorized', request.url));
    }
  }
 
  return NextResponse.next();
}
 
export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
};

CSRF Protection in SPAs

Cross-Site Request Forgery tricks the browser into making authenticated requests to your API from a malicious site.

The SameSite Cookie Defense

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/

SameSite=Strict prevents the cookie from being sent on cross-origin requests β€” the primary CSRF vector.

Double-Submit Cookie Pattern

For APIs that need cross-origin support:

// Server sets a CSRF token cookie (readable by JS, NOT HttpOnly)
// Set-Cookie: csrf_token=random123; Secure; SameSite=Lax
 
// Client reads the cookie and sends it as a header
const csrfToken = document.cookie
  .split('; ')
  .find(row => row.startsWith('csrf_token='))
  ?.split('=')[1];
 
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken!,
  },
  credentials: 'include',
  body: JSON.stringify({ amount: 100 }),
});

The attacker can trigger the request (cookie sent automatically) but cannot read the cookie value to set the header.

XSS Prevention Architecture

XSS is the most common web vulnerability. Defense is layered:

Layer 1: Output Encoding

React escapes by default. The danger is dangerouslySetInnerHTML:

// βœ… Safe β€” React escapes automatically
<div>{userInput}</div>
 
// ❌ Dangerous β€” raw HTML injection
<div dangerouslySetInnerHTML={{ __html: userInput }} />
 
// βœ… If you must render HTML, sanitize first
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

Layer 2: Content Security Policy

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-abc123';
  style-src 'self' 'unsafe-inline';
  img-src 'self' https://cdn.example.com;
  connect-src 'self' https://api.example.com;
  font-src 'self' https://fonts.googleapis.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';

What each directive prevents:

DirectivePrevents
script-src 'self'Inline scripts, external script injection
frame-ancestors 'none'Clickjacking (no iframe embedding)
base-uri 'self'Base tag injection
connect-src 'self'Data exfiltration to external domains

Layer 3: Sanitize User Input

const ALLOWED_TAGS = ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br'];
const ALLOWED_ATTRS = { a: ['href', 'title'] };
 
function sanitizeUserContent(html: string): string {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS,
    ALLOWED_ATTR: Object.values(ALLOWED_ATTRS).flat(),
  });
}

Subresource Integrity (SRI)

Ensure CDN-hosted scripts haven't been tampered with:

<script
  src="https://cdn.example.com/vendor.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
  crossorigin="anonymous"
></script>

If the file's hash doesn't match, the browser refuses to execute it.

Secure Headers Checklist

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Header                         β”‚ Purpose                    β”‚
│─────────────────────────────────┼────────────────────────────│
β”‚  Content-Security-Policy        β”‚ XSS, injection prevention  β”‚
β”‚  Strict-Transport-Security      β”‚ Force HTTPS (HSTS)         β”‚
β”‚  X-Content-Type-Options         β”‚ Prevent MIME sniffing      β”‚
β”‚  X-Frame-Options                β”‚ Clickjacking prevention    β”‚
β”‚  Referrer-Policy                β”‚ Control referrer leakage   β”‚
β”‚  Permissions-Policy             β”‚ Restrict browser features  β”‚
β”‚  Cross-Origin-Opener-Policy     β”‚ Isolate browsing context   β”‚
β”‚  Cross-Origin-Resource-Policy   β”‚ Prevent cross-origin reads β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementation (Next.js)

// next.config.js
const securityHeaders = [
  { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-XSS-Protection', value: '0' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
  { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
];
 
module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }];
  },
};

Security Architecture Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Security Layers                             β”‚
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Browser Layer                                        β”‚  β”‚
β”‚  β”‚  CSP β”‚ SRI β”‚ CORS β”‚ SameSite Cookies β”‚ HSTS          β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                             β”‚                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Application Layer                                    β”‚  β”‚
β”‚  β”‚  Input Sanitization β”‚ Output Encoding β”‚ CSRF Tokens  β”‚  β”‚
β”‚  β”‚  Route Guards β”‚ Token Management β”‚ Error Handling     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                             β”‚                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Network Layer                                        β”‚  β”‚
β”‚  β”‚  HTTPS β”‚ Certificate Pinning β”‚ API Gateway β”‚ WAF      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                             β”‚                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Server Layer                                         β”‚  β”‚
β”‚  β”‚  Auth Middleware β”‚ Rate Limiting β”‚ Input Validation   β”‚  β”‚
β”‚  β”‚  RBAC/ABAC β”‚ Audit Logging β”‚ Token Verification      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The Interview Framing

When asked about auth and security in a system design interview, structure your answer as:

  1. Authentication flow β€” "I'd use OAuth 2.0 + PKCE since this is an SPA. No client secret storage needed."
  2. Token storage β€” "Access token in memory, refresh token in HttpOnly Secure SameSite cookie."
  3. Session lifecycle β€” "Silent refresh before expiry, refresh token rotation for theft detection."
  4. Authorization β€” "RBAC for route protection client-side, enforced server-side on every API call."
  5. XSS prevention β€” "CSP headers, DOMPurify for user-generated content, no dangerouslySetInnerHTML."
  6. CSRF prevention β€” "SameSite=Strict cookies. If cross-origin needed, double-submit pattern."

Security isn't one feature. It's a posture β€” a set of layered defenses where each layer assumes the others might fail. That's defense in depth. That's what architects build.