Chapter 8: Security Fundamentals

Introduction

Security is not a featureβ€”it's a fundamental property of system design. Every distributed system must answer: Who are you? (Authentication) and What can you do? (Authorization). This chapter covers the complete security stack from cryptographic primitives to zero trust architectures.

Interview Signal: Security questions test depth across the stack. Expect "How would you secure service-to-service communication?" or "Design an auth system for 1B users." Show you understand trade-offs (stateless vs stateful, symmetric vs asymmetric), threat models, and real-world attacks.
graph TB subgraph "Security Layers" A[Identity Layer] --> B[Authentication Layer] B --> C[Authorization Layer] C --> D[Application Layer] D --> E[Transport Layer] E --> F[Network Layer] F --> G[Infrastructure Layer] end B -.->|Who are you?| H[Password, MFA, Certificate] C -.->|What can you do?| I[RBAC, ABAC, Policies] E -.->|Secure channel| J[TLS, mTLS] F -.->|Network isolation| K[Firewall, VPC] G -.->|Encryption at rest| L[KMS, Disk encryption]

1. Authentication vs Authorization

Clear Definitions

Aspect Authentication (AuthN) Authorization (AuthZ)
Question Who are you? What can you do?
Purpose Verify identity Grant/deny access
Happens Once per session/token Every protected operation
Examples Login, SSO, mTLS handshake Can user edit doc? Can service call API?
Protocols OAuth 2.0, OIDC, SAML, Kerberos RBAC, ABAC, ACLs, Policies
Failure 401 Unauthorized 403 Forbidden

Request Flow

sequenceDiagram participant User participant Gateway participant AuthN Service participant AuthZ Service participant API User->>Gateway: GET /api/resource (with token) rect rgb(40, 40, 80) Note over Gateway,AuthN Service: Authentication Phase Gateway->>AuthN Service: Validate token AuthN Service->>AuthN Service: Check signature, expiry AuthN Service-->>Gateway: βœ“ Valid (user_id: 12345) end rect rgb(40, 80, 40) Note over Gateway,AuthZ Service: Authorization Phase Gateway->>AuthZ Service: Can user 12345 read /api/resource? AuthZ Service->>AuthZ Service: Check permissions (RBAC/ABAC) AuthZ Service-->>Gateway: βœ“ Allowed end Gateway->>API: Forward request (user context) API-->>Gateway: 200 OK (data) Gateway-->>User: 200 OK (data) Note over User,API: AuthN happens ONCE (token valid)
AuthZ happens PER REQUEST

Why Separate Concerns?

  • Scalability: AuthN can be centralized (issue token once), AuthZ distributed (policy enforcement at edge)
  • Security: Compromised token doesn't bypass authorization checks
  • Flexibility: Change permissions without re-authenticating users
  • Auditability: Log who accessed what, not just who logged in
Common Anti-Pattern: Storing permissions in JWT. Token can't be revoked until expiry. Use JWT for identity only, fetch permissions from AuthZ service.

2. Authentication Methods

2.1 Password-Based Authentication

Password Hashing: Why and How

Never store plaintext passwords. Hashing is one-wayβ€”verify by hashing input and comparing. But hashing alone is vulnerable to rainbow tables.

graph LR A[User Password:
'hunter2'] --> B[Hash Function] B --> C[Hash:
'2c26b46b...'] C --> D[(Database)] E[Login Attempt:
'hunter2'] --> F[Same Hash Function] F --> G[Hash:
'2c26b46b...'] G --> H{Compare} D --> H H -->|Match| I[βœ“ Authenticated] H -->|No Match| J[βœ— Rejected] style A fill:#4a4 style E fill:#4a4 style I fill:#4a4 style J fill:#a44

Salt: Defeat Rainbow Tables

Salt is random data mixed with password before hashing. Each user gets unique salt (stored alongside hash). Same password β†’ different hash per user.

// Without salt (vulnerable to rainbow table)
hash('password123') β†’ '482c811da5d5b4bc6d497ffa98491e38'
// Attacker precomputes hash β†’ finds password instantly

// With salt (each user unique)
user1_salt = random_bytes(16)  // e.g., '9f86d081884c7d659a2feaa0c55ad015'
user1_hash = hash('password123' + user1_salt) β†’ 'a1b2c3d4...'

user2_salt = random_bytes(16)  // e.g., '7c211433f02071597741e6ff5a8ea34b'
user2_hash = hash('password123' + user2_salt) β†’ 'e5f6g7h8...'
// Attacker must bruteforce each user separately

Pepper: Server Secret

Pepper is a secret key stored separately (environment variable, KMS), NOT in database. Added to password+salt before hashing. If database is compromised, attacker still needs pepper.

pepper = env('PASSWORD_PEPPER')  // Server secret, not in DB
final_hash = hash(password + salt + pepper)

Algorithm Comparison

Algorithm Type Year Pros Cons Use When
bcrypt Key derivation 1999 Battle-tested, built-in salt, configurable cost (work factor) Max 72 bytes input, only CPU-hard (not memory-hard) Default choice for most apps
scrypt Key derivation 2009 Memory-hard (resists GPU/ASIC attacks), tunable CPU/memory cost More complex to configure, less widely adopted High-security apps (crypto wallets)
Argon2 Key derivation 2015 Won Password Hashing Competition, memory-hard, resists side-channel attacks, 3 variants (Argon2d/i/id) Newer (less battle-tested than bcrypt) New systems with high security requirements
PBKDF2 Key derivation 2000 FIPS-approved, simple, widely supported Only CPU-hard, weaker than bcrypt/scrypt/Argon2 Compliance requirements (FIPS 140-2)
SHA-256 Hash (NOT password hash) 2001 Fast Too fast! Vulnerable to bruteforce, no built-in salt NEVER for passwords (use for data integrity)
2024 Recommendation: Use Argon2id for new systems (best security), bcrypt if you need battle-tested stability. Configure work factor to take ~250-500ms on your server hardware. Re-hash on login if user has old algorithm (transparent migration).

Work Factor (Cost Parameter)

Higher cost = slower hashing = harder to bruteforce. But too slow impacts user experience. Balance security vs performance.

// bcrypt cost parameter (2^cost rounds)
cost = 10  β†’  ~150ms (2010 hardware)
cost = 12  β†’  ~600ms (2020 hardware)  ← recommended
cost = 14  β†’ ~2400ms (too slow for login)

// Argon2 parameters
time_cost = 3       // Iterations
memory_cost = 65536 // 64 MB
parallelism = 4     // Threads

2.2 Multi-Factor Authentication (MFA)

Three Factors

  • Something you know: Password, PIN
  • Something you have: Phone (TOTP app), hardware key (YubiKey), smart card
  • Something you are: Fingerprint, face, iris

MFA requires 2+ factors from different categories. Password + security question = same factor (both "know").

TOTP (Time-Based One-Time Password)

Algorithm: HOTP (RFC 4226) + time window. Shared secret between server and authenticator app.

sequenceDiagram participant User participant App as Authenticator App participant Server Note over User,Server: Setup Phase Server->>Server: Generate secret key Server->>User: Show QR code (secret + issuer) User->>App: Scan QR code App->>App: Store secret locally Note over User,Server: Login Phase User->>Server: Username + password Server-->>User: Request TOTP code par Generate code App->>App: code = TOTP(secret, current_time) App->>App: code = truncate(HMAC-SHA1(secret, time_window)) and Server generates same code Server->>Server: expected = TOTP(secret, current_time) end User->>Server: Enter code (e.g., 123456) Server->>Server: Compare code Β± time drift (30s window) Server-->>User: βœ“ Authenticated Note over User,Server: TOTP window typically 30s
Server accepts Β±1 window (90s total)

FIDO2/WebAuthn (Hardware Keys)

Public key cryptography. Private key never leaves hardware device. Resistant to phishing (domain-bound).

sequenceDiagram participant User participant Browser participant Server participant Security Key Note over User,Server: Registration User->>Server: Begin registration Server->>Browser: Challenge (random bytes) Browser->>Security Key: Create credential (challenge, domain) Security Key->>Security Key: Generate key pair
(private key stays in device) Security Key-->>Browser: Public key + attestation Browser-->>Server: Public key + attestation Server->>Server: Store public key for user Note over User,Server: Authentication User->>Server: Begin login Server->>Browser: Challenge (random bytes) Browser->>Security Key: Sign challenge (domain) Security Key->>User: Touch key to confirm User->>Security Key: [Touch] Security Key->>Security Key: Sign with private key Security Key-->>Browser: Signature Browser-->>Server: Signature Server->>Server: Verify with stored public key Server-->>Browser: βœ“ Authenticated

SMS OTP (One-Time Password)

Server sends code via SMS. User enters code to verify phone possession.

SMS Security Issues:
  • SIM swapping: Attacker convinces carrier to transfer number
  • SS7 vulnerabilities: Telecom network protocol attacks
  • Phishing: User gives code to attacker
  • Delivery delays: SMS not guaranteed real-time
SMS is weakest MFA method. Use only if TOTP/WebAuthn unavailable.

MFA Security Comparison

Method Phishing Resistant Offline UX Cost Security Rating
FIDO2/WebAuthn βœ“ Yes (domain-bound) βœ“ Yes Excellent (tap key) $20-50 per key ⭐⭐⭐⭐⭐
TOTP βœ— No βœ“ Yes Good (type 6 digits) Free ⭐⭐⭐⭐
Push notification Partial (user must check) βœ— No (needs internet) Excellent (tap approve) Free ⭐⭐⭐
SMS OTP βœ— No βœ— No Fair (wait for SMS) $0.01-0.05 per SMS ⭐⭐

2.3 Certificate-Based Authentication

Client presents X.509 certificate to prove identity. Used in mTLS, smart cards, device trust.

graph TB subgraph "Certificate Authority (CA)" CA[CA Private Key] end subgraph "Client" CPriv[Client Private Key] CCert[Client Certificate] end subgraph "Server" SPub[CA Public Key
Trusted root] end CA -->|Signs| CCert CPriv -.->|Owns| CCert CCert -->|Presented during handshake| SPub SPub -->|Verifies signature| CCert style CA fill:#a4a style CPriv fill:#4a4 style SPub fill:#4aa

Certificate Contents (X.509)

Subject: CN=client.example.com, O=MyOrg, C=US
Issuer: CN=Internal CA, O=MyOrg
Serial Number: 04:3f:7e:...
Valid From: 2024-01-01 00:00:00 UTC
Valid To: 2025-01-01 00:00:00 UTC
Public Key: RSA 2048 bits
            30:82:01:0a:02:82:01:01:00:...
Signature Algorithm: SHA256-RSA
Extensions:
  - Key Usage: Digital Signature, Key Encipherment
  - Extended Key Usage: TLS Client Authentication
  - Subject Alternative Name: DNS:client.example.com

When to Use

  • Service-to-service: mTLS for zero trust
  • IoT devices: Each device has unique cert
  • Employee access: Smart card with certificate
  • API clients: Instead of API keys (harder to leak)

2.4 API Keys

Long-lived credentials for programmatic access. Simple but requires careful management.

Anatomy

API Key Structure:
  prefix.identifier.secret
  β””β”€β”¬β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”¬β”€β”€β”˜
    β”‚        β”‚          └─ Random bytes (hash before storing)
    β”‚        └─────────── Key ID (for lookup, not secret)
    └──────────────────── Prefix (e.g., 'sk_live_', 'prod_')

Example: sk_live_EXAMPLE_DO_NOT_USE_12345

Database stores:
  key_id: 4eC39HqLyjWDarjtT1zdp7dc
  hash: bcrypt(secret)
  permissions: [read:users, write:orders]
  created: 2024-01-15
  last_used: 2024-03-20

Rotation Strategy

graph LR A[Old Key Active] --> B[Generate New Key] B --> C[Both Keys Valid
30-day overlap] C --> D[Client Updates to New Key] D --> E[Revoke Old Key] C -.->|Monitor usage| F{Old key still used?} F -->|Yes| G[Alert team] F -->|No| E style A fill:#4a4 style C fill##aa4 style E fill:#a44

API Key vs OAuth

Aspect API Key OAuth 2.0
Use Case Server-to-server (trusted client) Third-party apps (untrusted)
User Context No (key = app identity) Yes (token = user + app)
Expiration Long-lived (months/years) Short-lived (hours/minutes)
Revocation Manual Automatic (token expiry)
Scopes All-or-nothing (typically) Fine-grained (read:profile, write:posts)
API Key Best Practices:
  • Hash before storing (like passwords)
  • Support multiple keys per user (rotate without downtime)
  • Log every usage (detect compromise)
  • Rate limit per key (prevent abuse)
  • Prefix keys with environment (sk_live_, sk_test_) to prevent misuse
  • Provide key scanner for repos (detect leaked keys in Git)

2.5 Biometric Authentication

How It Works

graph TB subgraph "Enrollment" A[Fingerprint Scan] --> B[Extract Features] B --> C[Generate Template
Mathematical representation] C --> D[(Secure Enclave/
TEE Storage)] end subgraph "Authentication" E[Fingerprint Scan] --> F[Extract Features] F --> G[Compare to Template] D --> G G --> H{Match Score > Threshold?} H -->|Yes| I[βœ“ Authenticated] H -->|No| J[βœ— Rejected] end style D fill:#a4a style I fill:#4a4 style J fill:#a44

Biometric Types

Type How It Works False Acceptance Rate (FAR) False Rejection Rate (FRR) Spoofing Risk
Fingerprint Ridge patterns, minutiae points 1 in 50,000 2-3% Medium (fake fingerprints)
Face Facial landmarks, depth map (3D) 1 in 1,000,000 (Face ID) 5-10% Medium (photos, masks)
Iris Iris texture patterns 1 in 1,200,000 1-2% Low (hard to fake)
Voice Vocal patterns, pitch 1 in 1,000 10-15% High (recordings)

Security Considerations

  • Biometric β‰  Secret: You can't change your fingerprint if compromised. Use biometric to unlock secret (private key), not as the secret itself.
  • Local Processing: Template should never leave device. Server doesn't store biometric data.
  • Liveness Detection: Detect fake fingerprints (conductivity), photos (depth sensing), deepfakes (challenge-response).
  • Fallback Required: Biometric can fail (injury, gloves). Always provide password fallback.

System Architecture (Mobile)

graph TB subgraph "Device" A[Biometric Sensor] --> B[Secure Enclave/TEE] B --> C[Private Key] C --> D[Sign Challenge] end subgraph "Server" E[Public Key
Registered during setup] end D -->|Signature| E E -->|Verify| F[βœ“ Authenticated] B -.->|Never leaves device| G[Biometric Template] style B fill:#a4a style C fill:#4a4
Interview Tip: When asked about biometric auth, emphasize that biometric is a possession factor (something you haveβ€”your body), not just "something you are." Explain local-only storage and using biometric to unlock cryptographic keys, not transmitting biometric data.

3. Session Management

Server-Side Sessions

How It Works

sequenceDiagram participant Browser participant Server participant Session Store as Session Store - Redis or DB Browser->>Server: POST /login (credentials) Server->>Server: Validate credentials Server->>Session Store: Create session (user_id, metadata) Session Store-->>Server: session_id: abc123 Browser->>Server: GET /dashboard (Cookie: session_id=abc123) Server->>Session Store: Lookup session abc123 Session Store-->>Server: {user_id: 42, roles: ['admin']} Server->>Server: Authorize & process Server-->>Browser: 200 OK (dashboard data)

Session Store Options

Store Pros Cons Use When
Redis Fast (in-memory), TTL built-in, pub/sub for invalidation Requires separate service, data loss if Redis crashes (use persistence) Default choice for most apps
Database Durable, can join with user data, familiar Slower than Redis, adds DB load Low traffic or need durability
Memcached Simple, fast No persistence, no TTL per key (only LRU eviction) Pure cache (tolerate data loss)
In-Memory (app) No external dependency Lost on restart, doesn't scale horizontally, sticky sessions required Development only

Cookie Security Flags

Set-Cookie: session_id=abc123;
  HttpOnly;     // Can't be read by JavaScript (XSS protection)
  Secure;       // Only sent over HTTPS
  SameSite=Lax; // CSRF protection (Lax allows top-level navigation)
  Domain=.example.com; // Subdomain sharing
  Path=/;       // Available to all paths
  Max-Age=3600  // Expires in 1 hour
Flag Protection Required?
HttpOnly Prevents JavaScript access (XSS mitigation) βœ“ YES
Secure Only sent over HTTPS (man-in-middle protection) βœ“ YES (production)
SameSite=Strict Never sent on cross-site requests (CSRF protection) Use if no legitimate cross-site needs
SameSite=Lax Sent on top-level navigation (e.g., clicking link), not subrequests βœ“ Default (balances security & UX)
SameSite=None Sent on all requests (requires Secure flag) Only for legitimate cross-site (embedded widgets)

Stateless Tokens (JWT)

Architecture Comparison

graph TB subgraph "Server-Side Session (Stateful)" A1[Client] -->|session_id| A2[Server] A2 -->|Lookup| A3[(Session Store)] A3 -->|user data| A2 end subgraph "JWT (Stateless)" B1[Client] -->|JWT token| B2[Server] B2 -->|Verify signature| B2 B2 -.->|No DB lookup| B3[(Database)] end style A3 fill:#4aa style B3 fill:#888

Trade-offs

Aspect Server-Side Session JWT (Stateless)
State Stateful (requires session store) Stateless (self-contained token)
Revocation Instant (delete session) Hard (need blacklist or short expiry)
Scalability Session store is bottleneck No central store (scales horizontally)
Payload Size Small (just ID in cookie) Large (user data + signature in token)
Permissions Update Instant (update session) Wait for token expiry
Infrastructure Needs Redis/DB No external dependency

Session Attacks & Prevention

Session Fixation

Attacker tricks user into using attacker-controlled session ID.

sequenceDiagram participant Attacker participant Victim participant Server Attacker->>Server: GET /login (get session ID) Server-->>Attacker: session_id=malicious123 Attacker->>Victim: Send link: example.com?session_id=malicious123 Victim->>Server: GET /?session_id=malicious123 Victim->>Server: POST /login (credentials) Server->>Server: ❌ BUG: Reuse same session ID Attacker->>Server: GET /dashboard (session_id=malicious123) Server-->>Attacker: βœ“ Authenticated as victim Note over Server: PREVENTION: Regenerate session ID after login

Prevention: Always regenerate session ID after authentication.

// Before login
session_id = 'abc123'  // Anonymous session

// After successful login
old_session_id = 'abc123'
new_session_id = generate_random()  // 'xyz789'
copy_session_data(old_session_id, new_session_id)
delete_session(old_session_id)
set_cookie('session_id', new_session_id)

Session Hijacking

Attacker steals valid session ID (XSS, network sniffing, malware).

Prevention:

  • HTTPS only: Secure flag on cookies
  • HttpOnly: Prevent JavaScript access
  • Bind session to IP/User-Agent: Reject if changed (impacts mobile users roaming networks)
  • Short expiry: 15-30 minutes, refresh token for longer access
  • Concurrent session limits: Force logout of old sessions when new login

CSRF (Cross-Site Request Forgery)

Attacker tricks user's browser into making authenticated request.

sequenceDiagram participant Victim participant Attacker Site participant Bank Victim->>Bank: Login (gets session cookie) Bank-->>Victim: Set-Cookie: session_id=valid Victim->>Attacker Site: Visit malicious site Attacker Site-->>Victim: Hidden form + auto-submit:



Victim->>Bank: POST /transfer (Cookie: session_id=valid)
Body: to=attacker&amount=10000 Bank->>Bank: ❌ BUG: No CSRF check Bank-->>Victim: Transfer complete Note over Bank: PREVENTION: Require CSRF token in form

Prevention:

  • SameSite cookie: Browser won't send cookie on cross-site POST (modern browsers)
  • CSRF token: Secret token in form, validated server-side (for older browsers)
  • Custom headers: Require X-Requested-With: XMLHttpRequest (AJAX can't be forged cross-domain)
  • Double-submit cookie: CSRF token in cookie + form, server compares both

Token Rotation & Refresh

sequenceDiagram participant Client participant API participant Auth Service Note over Client,API: Initial Login Client->>Auth Service: POST /login (credentials) Auth Service-->>Client: access_token (15 min)
refresh_token (7 days) Note over Client,API: API Calls (Access Token Valid) Client->>API: GET /data (Bearer access_token) API->>API: Validate access_token API-->>Client: 200 OK (data) Note over Client,API: Access Token Expired Client->>API: GET /data (Bearer expired_token) API-->>Client: 401 Unauthorized Client->>Auth Service: POST /refresh (refresh_token) Auth Service->>Auth Service: Validate refresh_token Auth Service-->>Client: NEW access_token (15 min)
NEW refresh_token (7 days) Client->>API: GET /data (Bearer new_access_token) API-->>Client: 200 OK (data) Note over Client,Auth Service: Refresh Token Rotation prevents reuse

Why Two Tokens?

Token Lifetime Usage Storage Revocable
Access Token Short (5-15 min) Every API call Memory (SPA) or HttpOnly cookie No (expires quickly)
Refresh Token Long (7-90 days) Only to get new access token HttpOnly cookie or secure storage Yes (stored in DB)

Rotation Prevents Replay: Each refresh returns NEW refresh token, old one invalidated. If attacker steals refresh token and uses it, legitimate user's next refresh fails β†’ detects compromise.

Refresh Token Best Practices:
  • Rotate on every use (automatic breach detection)
  • Store hash in database (like passwords)
  • Bind to device/IP (optional, impacts mobile)
  • Allow multiple refresh tokens per user (multi-device)
  • Revoke all tokens on password change
  • Limit total tokens per user (prevent token hoarding)

4. OAuth 2.0 Deep Dive

OAuth 2.0 is an authorization framework (NOT authentication protocol). It lets users grant third-party apps limited access to their resources without sharing credentials.

Core Concepts

Roles

graph LR A[Resource Owner
User] -->|Grants permission| B[Client
Third-party app] B -->|Requests token| C[Authorization Server
Issuer] C -->|Issues token| B B -->|Accesses with token| D[Resource Server
API] D -->|Validates token| C style A fill:#4a4 style B fill:#4aa style C fill:#a4a style D fill:#aa4
  • Resource Owner: User who owns the data (e.g., you on GitHub)
  • Client: Third-party app requesting access (e.g., CI/CD tool)
  • Authorization Server: Issues tokens (e.g., GitHub OAuth server)
  • Resource Server: Hosts protected resources (e.g., GitHub API)

Scopes

Granular permissions. Client requests specific scopes, user consents to subset.

// GitHub scopes
repo              // Full repo access
repo:status       // Read commit status
public_repo       // Only public repos
user              // Read/write user profile
read:user         // Read-only user profile
gist              // Create gists

// Slack scopes
channels:read     // View channels
chat:write        // Post messages
files:read        // View files

// Request multiple scopes
scope=repo%20user%20gist

Grant Types (Authorization Flows)

4.1 Authorization Code Grant (Most Secure)

For web apps with backend. User redirected to authorization server, grants permission, server redirects back with code, app exchanges code for token.

sequenceDiagram participant User participant Browser participant Client Backend participant Auth Server participant API User->>Client Backend: Click "Connect GitHub" Client Backend-->>Browser: Redirect to:
github.com/login/oauth/authorize?
client_id=ABC&
redirect_uri=app.com/callback&
scope=repo&
state=random123 Browser->>Auth Server: Follow redirect Auth Server->>User: Show consent screen:
"App wants to access your repos" User->>Auth Server: Click "Authorize" Auth Server-->>Browser: Redirect to:
app.com/callback?
code=XYZ789&
state=random123 Browser->>Client Backend: Follow redirect (code in URL) Client Backend->>Client Backend: Verify state parameter Client Backend->>Auth Server: POST /token
code=XYZ789&
client_id=ABC&
client_secret=SECRET&
redirect_uri=app.com/callback Auth Server->>Auth Server: Validate code, client Auth Server-->>Client Backend: access_token + refresh_token Client Backend->>API: GET /user/repos
Authorization: Bearer access_token API-->>Client Backend: User's repos Client Backend-->>User: Show repos Note over User,API: Code never exposed to browser
Client secret stays on backend

Security: State Parameter

Prevents CSRF. Client generates random state, includes in authorization URL, validates it matches in callback.

// Step 1: Generate state
state = random_string(32)  // 'a3f7e9...'
store_in_session('oauth_state', state)
redirect_to('https://auth.com/authorize?state=' + state)

// Step 2: Validate state in callback
received_state = params['state']
expected_state = get_from_session('oauth_state')
if (received_state !== expected_state) {
  throw new Error('CSRF attack detected')
}

4.2 Authorization Code + PKCE (For Mobile/SPA)

PKCE (Proof Key for Code Exchange) protects public clients that can't store client secret. Uses code verifier and challenge.

sequenceDiagram participant App as Mobile App participant Browser participant Auth Server participant API App->>App: Generate code_verifier (random 43-128 chars) App->>App: code_challenge = SHA256(code_verifier) App->>Browser: Open:
auth.com/authorize?
client_id=ABC&
code_challenge=sha256_hash&
code_challenge_method=S256&
redirect_uri=app://callback Browser->>Auth Server: Authorization request Auth Server->>Browser: Show consent Browser->>Auth Server: User authorizes Auth Server->>Auth Server: Store code_challenge with code Auth Server-->>Browser: Redirect: app://callback?code=XYZ Browser->>App: Deep link with code App->>Auth Server: POST /token
code=XYZ&
client_id=ABC&
code_verifier=original_random_string Auth Server->>Auth Server: Verify: SHA256(code_verifier) == stored code_challenge Auth Server-->>App: access_token + refresh_token App->>API: API calls with access_token Note over App,API: Even if attacker intercepts code,
can't exchange without code_verifier

PKCE Prevents Code Interception

Without PKCE, attacker intercepts code in redirect, exchanges it for token (public clients have no client secret). With PKCE, attacker needs code_verifier (which never leaves app).

4.3 Client Credentials Grant (Service-to-Service)

No user involved. Service authenticates with client ID + secret, gets token for its own access.

sequenceDiagram participant Service A participant Auth Server participant Service B Service A->>Auth Server: POST /token
grant_type=client_credentials&
client_id=ABC&
client_secret=SECRET&
scope=read:data Auth Server->>Auth Server: Validate client credentials Auth Server-->>Service A: access_token (no refresh token) Service A->>Service B: GET /api/data
Authorization: Bearer access_token Service B->>Auth Server: Validate token (introspection or JWT verify) Auth Server-->>Service B: Valid (client_id: ABC, scopes: [read:data]) Service B-->>Service A: Data Note over Service A,Service B: No user context in token

When to Use

  • Backend service accessing its own resources
  • Microservice-to-microservice auth
  • Scheduled jobs, cron tasks
  • CLI tools (machine-to-machine)

4.4 Device Code Grant (IoT, CLI)

For devices without browser or limited input (smart TV, CLI). User authorizes on separate device.

sequenceDiagram participant Device as Smart TV participant Auth Server participant User participant Browser Device->>Auth Server: POST /device/code
client_id=ABC Auth Server-->>Device: device_code=D123
user_code=ABCD-1234
verification_uri=auth.com/device
interval=5 Device->>Device: Display:
"Go to auth.com/device
Enter code: ABCD-1234" User->>Browser: Visit auth.com/device Browser->>Auth Server: GET /device Auth Server-->>Browser: Show input form User->>Browser: Enter code: ABCD-1234 Browser->>Auth Server: POST /device/verify
user_code=ABCD-1234 Auth Server->>User: Show consent screen User->>Auth Server: Authorize loop Polling (every 5 seconds) Device->>Auth Server: POST /token
grant_type=device_code&
device_code=D123&
client_id=ABC Auth Server-->>Device: {"error": "authorization_pending"} end Note over User,Auth Server: User authorized on phone Device->>Auth Server: POST /token (poll) Auth Server-->>Device: access_token + refresh_token Device->>Device: βœ“ Authenticated Note over Device,Browser: Device never sees user password
User authenticated on separate device

Real-World Examples

  • AWS CLI: aws sso login
  • GitHub CLI: gh auth login
  • Google TV YouTube login

4.5 Implicit Grant (DEPRECATED)

Historically for SPAs. Auth server returns access token directly in redirect URL (no code exchange). Insecureβ€”token exposed in browser history, URL logs.

DO NOT USE Implicit Grant:
  • Token in URL β†’ leaked in logs, browser history, Referer headers
  • No refresh token β†’ user re-authenticates frequently
  • Superseded by Authorization Code + PKCE for SPAs
OAuth 2.1 (draft) removes implicit grant entirely.

OAuth vs OpenID Connect (OIDC)

Aspect OAuth 2.0 OpenID Connect (OIDC)
Purpose Authorization (what can you access) Authentication (who you are)
Use Case "App wants to access your photos" "Log in with Google"
Token Types access_token, refresh_token + id_token (JWT with user info)
User Info Not standardized Standard claims (sub, name, email)
Relationship Base protocol Authentication layer on top of OAuth 2.0

OIDC Flow (Authorization Code + OIDC)

sequenceDiagram participant User participant App participant Auth Server App->>Auth Server: /authorize?scope=openid%20profile%20email Auth Server->>User: Login & consent User->>Auth Server: Authorize Auth Server-->>App: code App->>Auth Server: /token (exchange code) Auth Server-->>App: access_token
+ id_token (JWT)
+ refresh_token App->>App: Decode id_token:
{
"sub": "user123",
"name": "Alice Smith",
"email": "alice@example.com",
"iss": "https://auth.com",
"aud": "client_abc",
"exp": 1234567890
} App->>Auth Server: GET /userinfo (with access_token) Auth Server-->>App: Full user profile Note over App: id_token for identity
access_token for API access

ID Token Structure (JWT)

// Header
{
  "alg": "RS256",
  "kid": "key-id-1"
}

// Payload (standard claims)
{
  "iss": "https://accounts.google.com",     // Issuer
  "sub": "1234567890",                       // Subject (user ID)
  "aud": "your-client-id.apps.googleusercontent.com", // Audience
  "exp": 1701234567,                         // Expiration
  "iat": 1701230967,                         // Issued at
  "name": "Alice Smith",
  "email": "alice@gmail.com",
  "email_verified": true,
  "picture": "https://lh3.googleusercontent.com/..."
}

// Signature (verify with public key)

OAuth Security Best Practices

1. Redirect URI Validation

Auth server MUST validate redirect_uri exactly matches pre-registered value. Prevents authorization code interception.

// ❌ BAD: Wildcard or partial match
allowed_redirects = ['https://app.com/*']  // Attacker uses app.com.evil.com

// βœ“ GOOD: Exact match
allowed_redirects = [
  'https://app.com/oauth/callback',
  'https://app.com/auth/google/callback'
]

// Validate
if (request.redirect_uri not in allowed_redirects) {
  return error('invalid_redirect_uri')
}

2. Always Use HTTPS

OAuth without HTTPS = attacker intercepts code/token. Never use http:// redirect URIs in production.

3. Short-Lived Access Tokens

Access tokens should expire in 5-60 minutes. Use refresh token for long-lived access. Limits damage if token leaks.

4. Token Storage (Frontend)

Storage Pros Cons Use When
Memory (JS variable) Most secure (lost on refresh) Lost on page refresh Short-lived access tokens
HttpOnly Cookie Not accessible to JS (XSS protection) Vulnerable to CSRF (use SameSite) Refresh tokens, session IDs
localStorage Persists across refresh Vulnerable to XSS AVOID for tokens (use for non-sensitive data)
sessionStorage Tab-scoped Vulnerable to XSS, lost on tab close AVOID for tokens
Recommended SPA Architecture:
  • Access token in memory (JS variable)
  • Refresh token in HttpOnly cookie (SameSite=Strict)
  • Silent refresh in background before expiry
  • BFF (Backend-For-Frontend) handles token exchange

5. Token Introspection vs JWT

Approach How It Works Pros Cons
Opaque Token
+ Introspection
Random string, API calls auth server to validate Can revoke instantly, small size Network call per validation (latency)
JWT
(Self-Contained)
Signed token with claims, validate locally No network call (fast), scales horizontally Can't revoke until expiry, larger size

Hybrid Approach

  • Access token: JWT (short-lived, fast validation)
  • Refresh token: Opaque (stored in DB, revocable)
Interview Question: "How would you implement 'Sign in with Google' for a web app?"

Answer: Use OpenID Connect (OIDC) Authorization Code flow:
  1. Register app with Google, get client_id + client_secret
  2. Redirect user to Google's /authorize endpoint with scope=openid profile email
  3. Google authenticates user, redirects back with code
  4. Backend exchanges code for tokens (access_token + id_token) at Google's /token endpoint
  5. Decode id_token (JWT) to get user's email, name, unique ID (sub claim)
  6. Create or update user in our database (match by Google's sub claim)
  7. Issue our own session token/JWT for subsequent requests
Don't store Google's access_token as sessionβ€”it's for accessing Google APIs, not our APIs.

5. JWT (JSON Web Tokens)

Structure

graph LR A[JWT Token] --> B[Header] A --> C[Payload] A --> D[Signature] B --> E[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9] C --> F[eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ] D --> G[SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c] style B fill:#4a4 style C fill:#4aa style D fill:#a4a
// Full JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└─────────── HEADER β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ └──────────────── PAYLOAD β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ └──────── SIGNATURE β”€β”€β”€β”€β”€β”€β”˜

// Decoded Header (Base64URL)
{
  "alg": "HS256",  // Algorithm: HMAC SHA-256
  "typ": "JWT"     // Type
}

// Decoded Payload (Base64URL)
{
  "sub": "1234567890",         // Subject (user ID)
  "name": "John Doe",           // Custom claim
  "iat": 1516239022             // Issued at (Unix timestamp)
}

// Signature (HMAC or RSA signature of header + payload)

Signing Algorithms

HS256 (HMAC + SHA-256) β€” Symmetric

Single secret key for signing AND verifying. Both auth server and API share the same secret.

graph LR A[Secret Key:
'my-secret-key'] --> B[Auth Server] A --> C[API Server] B -->|Sign JWT| D[JWT with signature] D --> C C -->|Verify with same key| E[βœ“ Valid] style A fill:#a4a style B fill:#4a4 style C fill:#4aa
// Signing (Auth Server)
header = base64url({"alg":"HS256","typ":"JWT"})
payload = base64url({"sub":"123","exp":1234567890})
signature = HMAC-SHA256(header + "." + payload, secret_key)
jwt = header + "." + payload + "." + signature

// Verification (API Server)
received_signature = jwt.split('.')[2]
computed_signature = HMAC-SHA256(jwt.split('.')[0] + "." + jwt.split('.')[1], secret_key)
if (received_signature === computed_signature) {
  // Valid
}

Pros: Fast, simple
Cons: Every service needs the secret (secrets proliferation), can't distinguish who signed it

RS256 (RSA + SHA-256) β€” Asymmetric

Private key for signing, public key for verifying. Auth server keeps private key, APIs get public key.

graph TB A[Private Key] --> B[Auth Server] C[Public Key] --> D[API Server 1] C --> E[API Server 2] C --> F[API Server 3] B -->|Sign with private key| G[JWT] G --> D G --> E G --> F D -->|Verify with public key| H[βœ“ Valid] E -->|Verify with public key| I[βœ“ Valid] F -->|Verify with public key| J[βœ“ Valid] style A fill:#a44 style C fill:#4a4
// Key Generation
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

// Signing (Auth Server)
signature = RSA-SHA256(header + "." + payload, private_key)

// Verification (API Server)
is_valid = RSA-Verify(header + "." + payload, signature, public_key)

Pros: APIs can't issue tokens (only verify), public key can be shared widely
Cons: Slower than HMAC, larger signatures

ES256 (ECDSA + SHA-256) β€” Asymmetric

Elliptic curve cryptography. Smaller keys and signatures than RSA, similar security.

// Key sizes for equivalent security
RSA-2048   β‰ˆ ECDSA-256  β‰ˆ 112-bit security
RSA-3072   β‰ˆ ECDSA-384  β‰ˆ 128-bit security

// Signature size
RS256: 256 bytes
ES256: 64 bytes  ← 4x smaller!

Algorithm Comparison

Algorithm Type Speed Key Distribution Signature Size Use When
HS256 Symmetric (HMAC) Fastest Secret must be shared 32 bytes Monolith or tightly-coupled services
RS256 Asymmetric (RSA) Slow Public key distributed 256 bytes Microservices, third-party validation
ES256 Asymmetric (ECDSA) Medium Public key distributed 64 bytes Mobile (smaller tokens), modern systems
Recommendation: Use RS256 or ES256 for production. Asymmetric crypto lets you distribute public keys safely. Only use HS256 if all services are in your control and share a secrets vault.

Standard Claims

Claim Name Purpose Example
iss Issuer Who issued the token "https://auth.example.com"
sub Subject Who the token is about (user ID) "user_12345"
aud Audience Who the token is for (API/service) "https://api.example.com"
exp Expiration When token expires (Unix timestamp) 1701234567
nbf Not Before Token not valid before this time 1701230967
iat Issued At When token was issued 1701230967
jti JWT ID Unique token identifier (prevent replay) "a3f7e9..."

Custom Claims

{
  // Standard claims
  "iss": "https://auth.example.com",
  "sub": "user_12345",
  "aud": "https://api.example.com",
  "exp": 1701234567,
  "iat": 1701230967,

  // Custom claims (namespaced to avoid conflicts)
  "https://example.com/roles": ["admin", "editor"],
  "https://example.com/tenant_id": "tenant_abc",
  "email": "alice@example.com",
  "email_verified": true,
  "plan": "premium"
}
Don't Store Permissions in JWT: Tokens can't be revoked until expiry. If user's role changes, old token still has old permissions. Store only immutable identity (sub, email), fetch permissions from AuthZ service.

JWT Validation

graph TB A[Receive JWT] --> B{Valid signature?} B -->|No| Z[❌ Reject] B -->|Yes| C{exp > now?} C -->|No| Z C -->|Yes| D{nbf <= now?} D -->|No| Z D -->|Yes| E{aud matches?} E -->|No| Z E -->|Yes| F{iss trusted?} F -->|No| Z F -->|Yes| G[βœ“ Valid] style G fill:#4a4 style Z fill:#a44
// JWT Validation Checklist
function validateJWT(token, expected_audience) {
  // 1. Decode header (don't trust yet)
  header = base64_decode(token.split('.')[0])

  // 2. Check algorithm (prevent algorithm confusion)
  if (header.alg !== 'RS256') {
    throw new Error('Invalid algorithm')
  }

  // 3. Verify signature
  public_key = get_public_key(header.kid)  // Key ID from header
  if (!verify_signature(token, public_key)) {
    throw new Error('Invalid signature')
  }

  // 4. Decode payload (now trusted)
  payload = json_decode(base64_decode(token.split('.')[1]))

  // 5. Check expiration
  if (payload.exp < current_time()) {
    throw new Error('Token expired')
  }

  // 6. Check not-before
  if (payload.nbf > current_time()) {
    throw new Error('Token not yet valid')
  }

  // 7. Check audience
  if (payload.aud !== expected_audience) {
    throw new Error('Invalid audience')
  }

  // 8. Check issuer (optional but recommended)
  if (payload.iss !== 'https://trusted-auth.com') {
    throw new Error('Untrusted issuer')
  }

  return payload  // Valid!
}

JWT Pitfalls

1. No Built-In Revocation

JWT is statelessβ€”once issued, valid until expiry. If compromised, can't revoke.

Workarounds:

  • Short expiry: 5-15 minutes (limits damage window)
  • Blacklist: Store revoked token IDs in Redis (check on every requestβ€”defeats stateless benefit)
  • Version claim: Include token_version in JWT, store current version in DB, increment on password change/logout
// Token includes version
{
  "sub": "user_123",
  "token_version": 5,
  "exp": 1701234567
}

// User table
user_id | current_token_version
user_123 | 5

// On password change or logout
UPDATE users SET current_token_version = 6 WHERE user_id = 'user_123'

// Validation
jwt_version = token.payload.token_version
db_version = db.query("SELECT current_token_version FROM users WHERE user_id = ?", token.sub)
if (jwt_version < db_version) {
  return error('Token revoked')
}

2. Token Size (HTTP Headers)

JWT can grow large (2-3KB) with many claims. Sent with every request in Authorization header. Impacts bandwidth, especially mobile.

// Small JWT (100 bytes)
{
  "sub": "123",
  "exp": 1701234567
}

// Large JWT (3KB+)
{
  "sub": "user_12345",
  "email": "alice@example.com",
  "name": "Alice Smith",
  "roles": ["admin", "editor", "viewer", "analyst"],
  "permissions": ["read:users", "write:users", "read:posts", ...],  // 100+ permissions
  "organization": {...},  // Nested objects
  "metadata": {...}
}

Solution: Keep JWT minimal (only sub, exp, iat). Fetch full user data from cache/DB.

3. Algorithm Confusion Attack

Attacker changes header from RS256 (asymmetric) to HS256 (symmetric), server verifies with public key as HMAC secret.

// 1. Original token (RS256)
Header: {"alg": "RS256", "typ": "JWT"}
Signature: RSA signature with private key

// 2. Attacker changes algorithm
Header: {"alg": "HS256", "typ": "JWT"}  ← Changed!
Payload: {... attacker payload ...}
Signature: HMAC(header + payload, public_key)  ← Uses public key as secret!

// 3. Vulnerable server
algorithm = jwt.header.alg  // HS256 (attacker-controlled)
if (algorithm === 'HS256') {
  verify_hmac(jwt, public_key)  // ❌ Uses public key as HMAC secret
} else if (algorithm === 'RS256') {
  verify_rsa(jwt, public_key)
}

Prevention: Never trust alg header. Enforce expected algorithm.

// βœ“ Secure validation
EXPECTED_ALGORITHM = 'RS256'

header = decode_header(jwt)
if (header.alg !== EXPECTED_ALGORITHM) {
  throw new Error('Invalid algorithm')
}

// Now verify with correct method
verify_rsa(jwt, public_key)

4. Sensitive Data Leakage

JWT payload is Base64-encoded, NOT encrypted. Anyone can decode it.

// ❌ BAD: Sensitive data in JWT
{
  "sub": "user_123",
  "ssn": "123-45-6789",  // ← Visible to anyone who sees token!
  "credit_card": "4111111111111111"
}

// βœ“ GOOD: Only non-sensitive identity
{
  "sub": "user_123",
  "email": "alice@example.com",  // OK (not secret)
  "exp": 1701234567
}

Rule: Only include data you'd put in a URL query parameter. If it's sensitive, don't put it in JWT.

Access Token vs Refresh Token vs ID Token

Token Type Purpose Lifetime Format Used With
Access Token API authorization Short (5-60 min) JWT or opaque Every API call (Authorization: Bearer)
Refresh Token Get new access token Long (7-90 days) Opaque (stored in DB) Auth server /token endpoint only
ID Token User identity (OIDC) Short (5-60 min) Always JWT Client app (not sent to APIs)
graph TB subgraph "Token Usage" A[Login] --> B[Auth Server] B -->|Issues| C[Access Token
15 min] B -->|Issues| D[Refresh Token
7 days] B -->|Issues| E[ID Token
OIDC only] C -->|Sent to| F[API] D -->|Sent to| B E -->|Used by| G[Client App] F -->|Validates| C G -->|Extract user info| E end style C fill:#4a4 style D fill:#4aa style E fill:#a4a
Interview Question: "Why use JWT for access tokens but opaque tokens for refresh tokens?"

Answer:
  • Access tokens: Used frequently (every API call), need fast validation. JWT allows stateless validation without DB lookup. Short expiry limits damage if leaked.
  • Refresh tokens: Used rarely (only when access token expires), must be revocable (logout, password change). Opaque + DB storage enables instant revocation. Long expiry requires stronger control.
Trade-off: access token = performance, refresh token = security.

JWT vs Session Comparison

Aspect JWT Server-Side Session
State Stateless (self-contained) Stateful (requires session store)
Validation Signature check (no DB) DB/Redis lookup
Revocation Hard (need blacklist) Easy (delete session)
Size Large (1-3KB) Small (~20 bytes ID)
Multi-Domain Easy (pass token in header) Hard (cookies domain-bound)
Mobile/SPA Natural fit Works but awkward
Best For APIs, microservices, SPAs Traditional web apps, admin panels
Hybrid Approach (Best of Both):
  • Use sessions for user-facing web app (easy revocation, smaller cookies)
  • Issue JWT for API access (mobile apps, third-party integrations)
  • Or: session ID in HttpOnly cookie (frontend) + JWT for backend-to-backend (microservices)

6. Authorization Models

6.1 RBAC (Role-Based Access Control)

Users assigned to roles, roles have permissions. Check user's roles to determine access.

Architecture

graph TB subgraph "Users" U1[Alice] U2[Bob] U3[Charlie] end subgraph "Roles" R1[Admin] R2[Editor] R3[Viewer] end subgraph "Permissions" P1[create:users] P2[delete:users] P3[edit:posts] P4[publish:posts] P5[read:posts] end U1 --> R1 U2 --> R2 U3 --> R3 R1 --> P1 R1 --> P2 R1 --> P3 R1 --> P4 R1 --> P5 R2 --> P3 R2 --> P4 R2 --> P5 R3 --> P5 style U1 fill:#4a4 style R1 fill:#a4a style P1 fill:#aa4

Database Schema

-- Users
users
  id | email
  1  | alice@example.com
  2  | bob@example.com

-- Roles
roles
  id | name
  1  | admin
  2  | editor
  3  | viewer

-- User-Role Assignment (many-to-many)
user_roles
  user_id | role_id
  1       | 1        -- Alice is admin
  2       | 2        -- Bob is editor

-- Permissions
permissions
  id | name
  1  | create:users
  2  | delete:users
  3  | edit:posts

-- Role-Permission Assignment
role_permissions
  role_id | permission_id
  1       | 1              -- Admin can create users
  1       | 2              -- Admin can delete users
  2       | 3              -- Editor can edit posts

Authorization Check

function canUserPerform(user_id, permission) {
  // 1. Get user's roles
  roles = db.query(`
    SELECT role_id FROM user_roles WHERE user_id = ?
  `, user_id)

  // 2. Get permissions for those roles
  permissions = db.query(`
    SELECT p.name FROM permissions p
    JOIN role_permissions rp ON p.id = rp.permission_id
    WHERE rp.role_id IN (?)
  `, roles)

  // 3. Check if permission exists
  return permissions.includes(permission)
}

// Usage
if (canUserPerform(user_id, 'delete:users')) {
  deleteUser(target_user_id)
} else {
  return 403 Forbidden
}

Role Hierarchy

Roles can inherit from other roles. Admin inherits all Editor permissions.

graph TB A[Admin] --> B[Editor] B --> C[Viewer] A -.->|Inherits all| D[Editor Permissions] A -.->|+ owns| E[Admin Permissions] B -.->|Inherits all| F[Viewer Permissions] B -.->|+ owns| G[Editor Permissions] style A fill:#a44 style B fill:#aa4 style C fill:#4a4
-- Role hierarchy table
role_hierarchy
  role_id | inherits_from_role_id
  1       | 2                      -- Admin inherits Editor
  2       | 3                      -- Editor inherits Viewer

-- Query with hierarchy
WITH RECURSIVE role_hierarchy_cte AS (
  -- Base: direct roles
  SELECT role_id FROM user_roles WHERE user_id = ?

  UNION

  -- Recursive: inherited roles
  SELECT rh.inherits_from_role_id
  FROM role_hierarchy rh
  JOIN role_hierarchy_cte rhc ON rh.role_id = rhc.role_id
)
SELECT DISTINCT p.name
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
JOIN role_hierarchy_cte rhc ON rp.role_id = rhc.role_id

RBAC Pros & Cons

Pros Cons
Simple to understand and implement Role explosion (need new role for each permission combo)
Easy to audit ("who has admin role?") Hard to model resource-specific permissions ("can edit THIS post")
Scales to thousands of users Difficult to handle context (time, location, resource state)
Mature tooling and standards Assignment can be manual and error-prone

6.2 ABAC (Attribute-Based Access Control)

Policy-driven. Evaluate attributes of user, resource, action, and environment to make decision.

Architecture

graph TB A[Authorization Request] --> B[Policy Engine] C[User Attributes
role, department, clearance] --> B D[Resource Attributes
owner, sensitivity, type] --> B E[Action
read, write, delete] --> B F[Environment Attributes
time, location, IP] --> B B --> G{Evaluate Policies} G -->|Match| H[βœ“ Allow] G -->|No Match| I[βœ— Deny] style B fill:#a4a style H fill:#4a4 style I fill:#a44

Policy Example (XACML-style)

// Policy: Employee can edit document if they own it OR are in same department
{
  "policy_id": "allow-edit-own-or-dept-doc",
  "effect": "Allow",
  "target": {
    "action": "edit",
    "resource_type": "document"
  },
  "condition": {
    "or": [
      {
        "attribute": "resource.owner_id",
        "operator": "equals",
        "value": "user.id"
      },
      {
        "attribute": "resource.department",
        "operator": "equals",
        "value": "user.department"
      }
    ]
  }
}

// Policy: Block access outside business hours
{
  "policy_id": "block-after-hours",
  "effect": "Deny",
  "target": {
    "action": "*",
    "resource_type": "*"
  },
  "condition": {
    "or": [
      {
        "attribute": "environment.hour",
        "operator": "less_than",
        "value": 9
      },
      {
        "attribute": "environment.hour",
        "operator": "greater_than",
        "value": 17
      }
    ]
  }
}

Evaluation Flow

sequenceDiagram participant User participant API participant PDP as Policy Decision Point participant PIP as Policy Info Point (Attribute Provider) participant PEP as Policy Enforcement Point User->>API: Request: Edit document_123 API->>PEP: Authorize(user, edit, document_123) PEP->>PDP: Can user_42 edit document_123? PDP->>PIP: Get user_42 attributes PIP-->>PDP: {role: employee, dept: engineering} PDP->>PIP: Get document_123 attributes PIP-->>PDP: {owner_id: 42, dept: engineering, sensitivity: low} PDP->>PIP: Get environment attributes PIP-->>PDP: {time: 10:30, location: office} PDP->>PDP: Evaluate policies against attributes PDP-->>PEP: Decision: ALLOW PEP-->>API: βœ“ Authorized API-->>User: 200 OK

Real-World Example (AWS IAM Policy)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "203.0.113.0/24"  ← Only from office IP
        },
        "DateGreaterThan": {
          "aws:CurrentTime": "2024-01-01T00:00:00Z"
        },
        "DateLessThan": {
          "aws:CurrentTime": "2024-12-31T23:59:59Z"
        }
      }
    }
  ]
}

ABAC Pros & Cons

Pros Cons
Extremely flexible (context-aware decisions) Complex to design and debug
No role explosion (policies instead of roles) Performance (attribute lookup + policy evaluation)
Fine-grained control (resource-level) Hard to audit ("who can access X?" requires simulation)
Dynamic (no manual role assignment) Requires mature policy management tooling

6.3 ReBAC (Relationship-Based Access Control)

Model permissions as relationships in a graph. Used by Google (Zanzibar), Airbnb, Dropbox.

Architecture (Google Zanzibar)

graph TB subgraph "Google Drive Example" U1[User: alice@gmail.com] U2[User: bob@gmail.com] G1[Group: eng-team] D1[Doc: financial-report] F1[Folder: 2024-reports] U1 -->|member| G1 U2 -->|member| G1 G1 -->|viewer| F1 U1 -->|owner| D1 D1 -->|parent| F1 end Q[Query: Can bob view financial-report?] --> E[Zanzibar Engine] E -->|Walk graph| A{bob β†’ member β†’ eng-team?} A -->|Yes| B{eng-team β†’ viewer β†’ folder?} B -->|Yes| C{doc β†’ parent β†’ folder?} C -->|Yes| R[βœ“ Allow] style E fill:#a4a style R fill:#4a4

Tuple Format

Relationships stored as tuples: (object, relation, subject)

// Tuple structure
(doc:financial-report, owner, user:alice)
(doc:financial-report, parent, folder:2024-reports)
(folder:2024-reports, viewer, group:eng-team)
(group:eng-team, member, user:bob)

// Query: Can bob view financial-report?
Check(doc:financial-report, viewer, user:bob)

// Zanzibar evaluates:
1. Is bob directly viewer of doc? β†’ No
2. Is doc's parent folder visible to bob? β†’ Check folder
3. Is eng-team viewer of folder? β†’ Yes
4. Is bob member of eng-team? β†’ Yes
Result: ALLOW

Real-World Schema (SpiceDB / AuthZed)

// Schema definition
definition user {}

definition group {
  relation member: user
}

definition folder {
  relation viewer: user | group#member
  relation editor: user | group#member
}

definition document {
  relation parent: folder
  relation owner: user
  relation viewer: user | group#member

  // Computed permission: can view if owner OR parent folder viewer
  permission view = owner + parent->viewer
  permission edit = owner
}

// Tuples (relationship instances)
document:financial-report#owner@user:alice
document:financial-report#parent@folder:2024-reports
folder:2024-reports#viewer@group:eng-team#member
group:eng-team#member@user:bob

// Query API
CheckPermission(document:financial-report, view, user:bob)
β†’ ALLOWED (via parent folder β†’ group membership)

Advantages Over RBAC

  • Resource ownership: "Can edit if you're the owner" naturally modeled
  • Hierarchies: Folder permissions inherit to documents
  • Group membership: Add user to group, instantly get all group's permissions
  • Delegation: Share document with someone, they can share with others

ReBAC Pros & Cons

Pros Cons
Natural model for sharing/collaboration (Google Docs, Notion) Complex to implement (graph traversal, caching)
Scales to billions of relationships (Google's Zanzibar) Slower than RBAC (graph walk vs table join)
Flexible inheritance (transitive permissions) Hard to reason about ("who can access X?" requires BFS)
Supports delegation and nested groups Immature ecosystem (compared to RBAC)

6.4 ACL (Access Control Lists)

Simple list attached to each resource specifying who can access it.

Structure

// File system ACL (Unix)
file: /home/alice/document.txt
owner: alice (rwx)
group: staff (r--)
other: (---)

// AWS S3 Bucket ACL
{
  "Owner": {"ID": "alice123"},
  "Grants": [
    {
      "Grantee": {"Type": "User", "ID": "bob456"},
      "Permission": "READ"
    },
    {
      "Grantee": {"Type": "Group", "URI": "http://acs.amazonaws.com/groups/global/AllUsers"},
      "Permission": "READ"  ← Public read
    }
  ]
}

// Database table ACL
resource_id | user_id | permission
document_1  | alice   | owner
document_1  | bob     | read
document_1  | charlie | write

Authorization Check

function canAccess(user_id, resource_id, required_permission) {
  acl = db.query(`
    SELECT permission FROM acl
    WHERE resource_id = ? AND user_id = ?
  `, resource_id, user_id)

  return acl.permission >= required_permission
}

// Usage
if (canAccess(user_id, 'document_1', 'write')) {
  updateDocument('document_1', new_content)
}

ACL Pros & Cons

Pros Cons
Simple to implement and understand Doesn't scale (ACL per resource)
Fast check (single DB lookup) Hard to manage (update 1000 files when user leaves?)
Fine-grained (per-resource control) No centralized policy (duplicated logic)
Works for small systems Doesn't support groups, roles, or inheritance well

Comparison Table: When to Use Each

Model Complexity Flexibility Scale Best For Examples
ACL ⭐ ⭐ ⭐ Small apps, file systems Unix file permissions, simple blogs
RBAC ⭐⭐ ⭐⭐ ⭐⭐⭐⭐ Enterprise apps, admin panels Jira, Salesforce, most SaaS
ABAC ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ Complex policies, compliance AWS IAM, healthcare systems
ReBAC ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Collaboration, sharing Google Drive, GitHub, Notion

Decision Tree

graph TB A[Need Authorization] --> B{Resource sharing/
collaboration?} B -->|Yes| C[ReBAC
Zanzibar/SpiceDB] B -->|No| D{Complex policies
with context?} D -->|Yes| E[ABAC
XACML/OPA] D -->|No| F{Large user base
10K+ users?} F -->|Yes| G[RBAC
Standard choice] F -->|No| H{Simple app
< 1K resources?} H -->|Yes| I[ACL
Keep it simple] H -->|No| G style C fill:#4a4 style E fill:#4aa style G fill:#aa4 style I fill:#a4a
Interview Question: "Design authorization for Google Docs."

Answer: Use ReBAC (Google Zanzibar):
  1. Model: Documents, folders, users, groups as objects with relationships (owner, editor, viewer, member)
  2. Tuples: Store relationships: (doc:123, owner, user:alice), (doc:123, parent, folder:work), (folder:work, viewer, group:team)
  3. Inheritance: Document inherits folder permissions, groups expand to members
  4. Check: Query "Can bob view doc:123?" β†’ Walk graph: bob β†’ team β†’ viewer of folder β†’ doc parent folder
  5. Scale: Distribute graph, cache hot paths, denormalize for common checks
Why not RBAC? Doesn't support "Alice shared doc with Bob" or nested folder permissions.

7. Encryption

7.1 Symmetric Encryption

Same key for encryption and decryption. Fast, but key distribution is the challenge.

graph LR A[Plaintext:
'Hello World'] --> B[Encrypt] K[Secret Key:
'k3yS3cr3t!'] --> B B --> C[Ciphertext:
'9f86d081...'] C --> D[Decrypt] K --> D D --> E[Plaintext:
'Hello World'] style K fill:#a4a style A fill:#4a4 style E fill:#4a4

AES (Advanced Encryption Standard)

Industry standard since 2001. Block cipher (encrypts 128-bit blocks).

Variant Key Size Security Use When
AES-128 128 bits Strong (2^128 brute force) General purpose
AES-192 192 bits Stronger Government (SECRET)
AES-256 256 bits Strongest Government (TOP SECRET), default choice

Modes of Operation

Mode How It Works Pros Cons Use When
ECB
(Electronic Codebook)
Each block encrypted independently Simple, parallel ⚠️ INSECURE: identical plaintext β†’ identical ciphertext NEVER use
CBC
(Cipher Block Chaining)
XOR each block with previous ciphertext Secure with IV Not authenticated (padding oracle attack) Legacy systems only
GCM
(Galois/Counter Mode)
Counter mode + GMAC authentication tag Fast, authenticated (AEAD), parallel Nonce reuse = catastrophic failure βœ“ DEFAULT CHOICE
CTR
(Counter)
Encrypt counter, XOR with plaintext Parallel, random access Not authenticated (need HMAC separately) When GCM unavailable
ECB is Broken: Famous exampleβ€”encrypted penguin image shows pattern:

Plaintext penguin β†’ ECB encrypted β†’ Same colors produce same ciphertext β†’ Penguin shape visible!
CBC/GCM β†’ No pattern visible.

AES-GCM Anatomy

// AES-256-GCM encryption
key = random_bytes(32)        // 256-bit key
nonce = random_bytes(12)      // 96-bit nonce (MUST be unique per message)
plaintext = "Sensitive data"
aad = "user_id:12345"         // Additional Authenticated Data (optional)

ciphertext, tag = AES_GCM_encrypt(key, nonce, plaintext, aad)

// Ciphertext: encrypted data
// Tag: authentication tag (128 bits) proves integrity

// Decryption
plaintext = AES_GCM_decrypt(key, nonce, ciphertext, tag, aad)
// If tag doesn't match β†’ decryption fails (tampered data)

AEAD (Authenticated Encryption with Associated Data): Provides both confidentiality (encryption) and integrity (authentication). AES-GCM is an AEAD cipher.

ChaCha20-Poly1305

Alternative to AES-GCM. Faster on mobile (no hardware AES), constant-time (resistant to timing attacks).

Aspect AES-256-GCM ChaCha20-Poly1305
Speed (hardware AES) Very fast (CPU acceleration) Slower
Speed (software) Slow Very fast
Mobile Slower (if no AES-NI) Faster
Constant-time Harder to implement Easier (designed for it)
Adoption Ubiquitous Growing (TLS 1.3, Wireguard)
Recommendation:
  • Use AES-256-GCM for servers (hardware acceleration)
  • Use ChaCha20-Poly1305 for mobile/IoT (software-only)
  • Never reuse nonces! Generate random 96-bit nonce per message or use counter (if you can guarantee uniqueness)

7.2 Asymmetric Encryption

Public key encrypts, private key decrypts. Solves key distribution but much slower.

graph TB subgraph "Alice" A1[Alice's Private Key] A2[Alice's Public Key] end subgraph "Bob" B1[Plaintext:
'Secret message'] B2[Encrypt with
Alice's Public Key] B3[Ciphertext] end A2 -.->|Shared publicly| B2 B1 --> B2 B2 --> B3 B3 -->|Send| C[Alice] C --> D[Decrypt with
Alice's Private Key] A1 --> D D --> E[Plaintext:
'Secret message'] style A1 fill:#a44 style A2 fill:#4a4 style E fill:#4a4

RSA (Rivest-Shamir-Adleman)

Key Size Security Speed Status
1024 bits BROKEN (factored in 2010s) Fast ❌ DO NOT USE
2048 bits Secure until ~2030 Medium βœ“ Minimum for now
3072 bits Secure until ~2050 Slow βœ“ Recommended
4096 bits Overkill (3072 sufficient) Very slow Only if required

RSA Usage: Hybrid Encryption

RSA is slow for large data. Use RSA to encrypt symmetric key, use symmetric key to encrypt data.

sequenceDiagram participant Alice participant Bob Note over Alice: Has large file (1GB) Alice->>Alice: Generate random AES key (symmetric) Alice->>Alice: Encrypt file with AES key (fast) Alice->>Alice: Encrypt AES key with Bob's RSA public key Alice->>Bob: Send: encrypted_file + encrypted_key Bob->>Bob: Decrypt AES key with RSA private key Bob->>Bob: Decrypt file with AES key Note over Bob: Both confidentiality (encryption)
and speed (AES for bulk)

ECDSA (Elliptic Curve Digital Signature Algorithm)

Smaller keys, faster than RSA for signatures. Used in TLS, Bitcoin, SSH.

Curve Key Size Equivalent RSA Status
P-256
(secp256r1)
256 bits RSA-3072 βœ“ Widely supported (NIST curve)
P-384 384 bits RSA-7680 High security, slower
Curve25519 256 bits RSA-3072 βœ“ Fast, constant-time (recommended for new systems)

Ed25519 (EdDSA with Curve25519)

Modern signing algorithm. Faster than ECDSA, simpler implementation, resistant to side-channel attacks.

// Key generation
private_key = random_bytes(32)  // 256 bits
public_key = derive_public_key(private_key)

// Signing
message = "Transfer $100 to Bob"
signature = Ed25519_sign(private_key, message)

// Verification
is_valid = Ed25519_verify(public_key, message, signature)

RSA vs ECDSA vs Ed25519

Algorithm Key Size Signature Size Sign Speed Verify Speed Use For
RSA-2048 2048 bits 256 bytes Slow Fast Legacy systems, broad compatibility
ECDSA P-256 256 bits 64 bytes Fast Fast TLS, JWT (ES256)
Ed25519 256 bits 64 bytes Very fast Very fast SSH keys, modern APIs, cryptocurrencies

7.3 Encryption at Rest

Disk Encryption (Full-Disk)

Encrypts entire disk. Protects if physical disk is stolen. Key stored in TPM or entered at boot.

graph TB A[Boot] --> B{Key Available?} B -->|No| C[Prompt Password/TPM] C --> D[Unlock Disk Key] B -->|Yes, TPM| D D --> E[Decrypt Disk Sectors On-The-Fly] E --> F[OS Reads/Writes Plaintext] style D fill:#a4a style F fill:#4a4

Technologies: BitLocker (Windows), FileVault (macOS), LUKS (Linux)

Pros: Transparent, protects all data
Cons: Doesn't protect if disk is mounted (attacker with shell access sees plaintext)

Database Encryption (Column-Level)

Encrypt specific columns (e.g., SSN, credit card). Allows indexing on non-encrypted columns.

-- Encrypted columns
users
  id  | email              | ssn_encrypted
  1   | alice@example.com  | 0x9f86d081...  ← AES-GCM(SSN)

-- Application layer encryption
plaintext_ssn = "123-45-6789"
encrypted_ssn = AES_encrypt(plaintext_ssn, data_encryption_key)
db.insert("INSERT INTO users (email, ssn_encrypted) VALUES (?, ?)", email, encrypted_ssn)

-- Reading
encrypted_ssn = db.query("SELECT ssn_encrypted FROM users WHERE id = 1")
plaintext_ssn = AES_decrypt(encrypted_ssn, data_encryption_key)

Application-Level Encryption

Encrypt before writing to storage. Most flexible, requires key management.

graph LR A[User Data] --> B[Application] B -->|Encrypt| C[Encrypted Data] C --> D[(Database)] C --> E[(S3)] C --> F[(Backup)] K[Data Encryption Key
from KMS] -.-> B style K fill:#a4a style C fill:#4a4

Encryption at Rest: Comparison

Method Granularity Performance Protects Against Key Management
Full-Disk All data Hardware-accelerated (fast) Physical theft TPM, BIOS password
Database (TDE) All tables Medium (DB handles it) Backup theft, file access DB master key
Column-Level Specific columns Slower (per-row decrypt) DB admin, SQL injection Application KMS
Application-Level Per-field, per-object Slowest (app handles it) Everyone except app Application KMS

7.4 Encryption in Transit (TLS)

TLS (Transport Layer Security) encrypts data between client and server. Prevents eavesdropping and tampering.

TLS Handshake (Simplified)

sequenceDiagram participant Client participant Server Client->>Server: ClientHello
(TLS versions, cipher suites, random) Server->>Client: ServerHello
(chosen TLS version, cipher, random) Server->>Client: Certificate (public key) Server->>Client: ServerHelloDone Client->>Client: Verify certificate
(signed by trusted CA?) Client->>Client: Generate pre-master secret Client->>Server: EncryptedPreMasterSecret
(encrypted with server's public key) Client->>Client: Derive session keys
(from pre-master + randoms) Server->>Server: Derive session keys Client->>Server: ChangeCipherSpec + Finished
(encrypted with session key) Server->>Client: ChangeCipherSpec + Finished Note over Client,Server: All subsequent data encrypted
with symmetric session key (AES)

Why Hybrid Crypto?

  • Asymmetric (RSA/ECDHE): Exchange session key securely (slow, only at handshake)
  • Symmetric (AES): Encrypt bulk data (fast, after handshake)

TLS 1.3 Improvements

Aspect TLS 1.2 TLS 1.3
Handshake 2 round-trips 1 round-trip (faster)
Cipher Suites 37 (many insecure) 5 (all AEAD)
Forward Secrecy Optional (ECDHE) Mandatory
RSA Key Exchange Supported Removed (not forward-secret)
0-RTT No Yes (resumption)

Forward Secrecy

Even if private key is compromised later, past sessions can't be decrypted. Achieved with ephemeral key exchange (ECDHE).

graph TB A[TLS 1.2 Static RSA] --> B[Session encrypted with
same server private key] B --> C[If key compromised,
ALL past traffic decrypted] D[TLS 1.3 ECDHE] --> E[Each session uses
ephemeral DH keys] E --> F[Keys destroyed after session] F --> G[If server key compromised,
past traffic STILL secure] style C fill:#a44 style G fill:#4a4

7.5 End-to-End Encryption (E2EE)

Only sender and recipient can decrypt. Server can't read content (zero-knowledge).

Signal Protocol (Double Ratchet)

Used by Signal, WhatsApp, Facebook Messenger (secret chats).

sequenceDiagram participant Alice participant Server participant Bob Note over Alice,Bob: Initial Key Exchange (X3DH) Alice->>Server: Upload identity key + signed pre-keys Bob->>Server: Upload identity key + signed pre-keys Alice->>Server: Fetch Bob's pre-keys Server-->>Alice: Bob's keys Alice->>Alice: Compute shared secret
(Diffie-Hellman) Note over Alice,Bob: Messaging Alice->>Alice: Encrypt message with shared secret Alice->>Server: Encrypted message Server->>Bob: Encrypted message (can't read) Bob->>Bob: Decrypt with shared secret Note over Alice,Bob: Each message uses new key (ratchet) Note over Alice,Bob: Server only sees ciphertext

E2EE Challenges

  • Key Exchange: How do Alice and Bob share keys securely? (Pre-keys uploaded to server)
  • Trust: How does Alice know she has Bob's real key? (Safety numbers, out-of-band verification)
  • Multi-Device: Alice has phone + laptop. How to decrypt on both? (Device-specific keys, or backup key)
  • Search: Server can't search encrypted messages. (Client-side search only)
  • Recovery: Lose key = lose all messages. (No password reset!)

7.6 Key Management

Key Hierarchy (Envelope Encryption)

Use master key to encrypt data keys, data keys to encrypt data. Rotating master key doesn't require re-encrypting all data.

graph TB A[Master Key
in HSM/KMS] -->|Encrypts| B[Data Encryption Key 1] A -->|Encrypts| C[Data Encryption Key 2] A -->|Encrypts| D[Data Encryption Key 3] B -->|Encrypts| E[User Data 1-1000] C -->|Encrypts| F[User Data 1001-2000] D -->|Encrypts| G[User Data 2001-3000] style A fill:#a44 style B fill:#a4a style C fill:#a4a style D fill:#a4a
// Encryption
data_key = random_bytes(32)  // Generate random AES-256 key
encrypted_data = AES_encrypt(plaintext, data_key)
encrypted_key = KMS.encrypt(data_key)  // Encrypt key with master key

store_in_db(encrypted_data, encrypted_key)

// Decryption
encrypted_key = db.get('encrypted_key')
data_key = KMS.decrypt(encrypted_key)  // Decrypt with master key
plaintext = AES_decrypt(encrypted_data, data_key)

Key Rotation

Regularly change keys to limit damage if compromised.

Key Type Rotation Frequency Strategy
Master Key Annually Re-encrypt data keys (not data itself)
Data Encryption Keys Monthly New key for new data, lazy re-encryption on access
TLS Certificates 90 days (Let's Encrypt) Automatic renewal
API Keys On compromise or quarterly Support multiple active keys (overlap period)

KMS (Key Management Service)

Provider Features Use When
AWS KMS Managed master keys, envelope encryption, integrated with AWS services AWS infrastructure
Azure Key Vault Keys + secrets + certs, HSM-backed, RBAC Azure infrastructure
GCP Cloud KMS Multi-region keys, automatic rotation, audit logs GCP infrastructure
HashiCorp Vault Multi-cloud, dynamic secrets, leasing, open-source Multi-cloud or on-prem

HSM (Hardware Security Module)

Tamper-resistant hardware for storing keys. Keys never leave HSM in plaintext.

  • FIPS 140-2 Level 3/4: Government-grade (physical tamper detection)
  • Use Cases: Root CA keys, payment processing (PCI DSS), signing certificates
  • Cloud HSM: AWS CloudHSM, Azure Dedicated HSM (you manage keys, cloud can't access)
Interview Question: "How would you encrypt 1 billion user records in S3?"

Answer: Use envelope encryption:
  1. Master Key: Store in AWS KMS (customer-managed key, HSM-backed)
  2. Data Keys: Generate unique AES-256 key per user (or per batch)
  3. Encrypt Data: Encrypt user record with data key
  4. Encrypt Key: Encrypt data key with KMS master key
  5. Store Both: Store encrypted data + encrypted key together in S3
  6. Rotation: Rotate KMS key annually (re-encrypt data keys, not data)
Why not encrypt all data with master key directly? KMS has rate limits, latency. Envelope encryption = one KMS call per object, not per byte.

8. Mutual TLS (mTLS)

Both client and server authenticate with certificates. Standard TLS = server proves identity, mTLS = both prove identity.

Standard TLS vs mTLS

graph TB subgraph "Standard TLS (HTTPS)" A1[Client] -->|Request| A2[Server] A2 -->|Certificate| A1 A1 -->|Verify server cert| A1 A1 -.->|Server authenticated| A2 end subgraph "Mutual TLS (mTLS)" B1[Client] -->|Request + Client Cert| B2[Server] B2 -->|Server Cert| B1 B1 -->|Verify server cert| B1 B2 -->|Verify client cert| B2 B1 -.->|Both authenticated| B2 end style A1 fill:#4a4 style B1 fill:#4aa style B2 fill:#4aa

mTLS Handshake

sequenceDiagram participant Client participant Server participant CA Note over Client,Server: Standard TLS Steps Client->>Server: ClientHello Server->>Client: ServerHello + Server Certificate Client->>Client: Verify server cert with CA public key Note over Client,Server: mTLS Additional Steps Server->>Client: CertificateRequest Client->>Server: Client Certificate Server->>Server: Verify client cert with CA public key Client->>Server: CertificateVerify
(sign handshake with client private key) Server->>Server: Verify signature with client public key Note over Client,Server: Both authenticated!
Establish encrypted session

Certificate Lifecycle

1. Certificate Generation (CSR)

// Generate private key
openssl genrsa -out client.key 2048

// Create Certificate Signing Request (CSR)
openssl req -new -key client.key -out client.csr \
  -subj "/CN=client.example.com/O=MyOrg/C=US"

// Submit CSR to CA
// CA signs CSR with its private key β†’ Client Certificate

2. Certificate Signing (CA)

// CA signs the CSR
openssl x509 -req -in client.csr \
  -CA ca.crt -CAkey ca.key \
  -out client.crt \
  -days 365 \
  -sha256

// Result: client.crt (signed certificate)
// Contains: client's public key + CA's signature

3. Certificate Verification (Server)

// Server verifies client certificate
1. Check signature (signed by trusted CA?)
   verify(client.crt.signature, ca.public_key) == true

2. Check validity period
   now >= client.crt.valid_from && now <= client.crt.valid_to

3. Check revocation (CRL or OCSP)
   is_revoked(client.crt.serial_number) == false

4. Check usage
   client.crt.ext_key_usage includes "TLS Client Authentication"

5. Verify client possesses private key
   Client signs handshake with private key
   Server verifies signature with client.crt.public_key

Certificate Revocation

CRL (Certificate Revocation List)

CA publishes list of revoked certificate serial numbers. Server downloads periodically.

graph LR C[Server] -->|Downloads daily| B D[Client] -->|Presents cert 456| C C -->|Check CRL| E{Is 456 in list?} E -->|Yes| F[❌ Reject] E -->|No| G[βœ“ Accept] style F fill:#a44 style G fill:#4a4

Pros: Simple
Cons: CRL can be huge, stale (updated daily/weekly), requires download

OCSP (Online Certificate Status Protocol)

Server queries CA in real-time: "Is cert 456 revoked?"

sequenceDiagram participant Client participant Server participant OCSP Responder Client->>Server: Present certificate Server->>OCSP Responder: Is cert 456 revoked? OCSP Responder-->>Server: Status: Good Server-->>Client: βœ“ Accepted Note over Server,OCSP Responder: Real-time check

Pros: Real-time, smaller payloads
Cons: Latency (extra network call), OCSP responder can be DDoS'd, privacy (CA sees who's checking what)

OCSP Stapling

Server fetches OCSP response, includes it in TLS handshake. No client→OCSP call.

// Server caches OCSP response
Server β†’ OCSP: Is my cert valid?
OCSP β†’ Server: Signed response (valid 7 days)

// Client connects
Client β†’ Server: ClientHello
Server β†’ Client: Certificate + OCSP Response (stapled)
Client: Verify OCSP response signature (CA signed it)
        βœ“ Cert is valid, no need to contact OCSP

Service Mesh mTLS

Automatic mTLS between all services. Sidecar proxy handles certificates.

Architecture (Istio / Linkerd)

graph TB subgraph "Service A Pod" A1[Service A] -->|localhost| A2[Envoy Sidecar] end subgraph "Service B Pod" B1[Service B] -->|localhost| B2[Envoy Sidecar] end A2 -->|mTLS| B2 C[Control Plane
Cert Authority] -.->|Issue certs
24h TTL| A2 C -.->|Issue certs| B2 A2 -.->|Auto-rotate| A2 B2 -.->|Auto-rotate| B2 style A2 fill:#4a4 style B2 fill:#4aa style C fill:#a4a

SPIFFE/SPIRE

Standard for service identity. SPIFFE = spec, SPIRE = implementation.

// SPIFFE ID (unique service identity)
spiffe://trust-domain/namespace/service-a

// SPIRE issues short-lived X.509 certs
- Workload requests cert from SPIRE agent
- Agent verifies workload identity (k8s service account)
- Agent requests cert from SPIRE server
- Server issues cert with SPIFFE ID (TTL: 1 hour)
- Workload uses cert for mTLS
- Cert auto-rotates before expiry

Benefits

  • Zero trust: Every service authenticates every call
  • Automatic rotation: Short-lived certs (hours), no manual renewal
  • No secrets in config: Certs issued at runtime
  • Observability: Service mesh logs all mTLS connections

When to Use mTLS

Use Case mTLS Appropriate? Why / Alternative
Service-to-service (internal) βœ“ YES Strong authentication, zero trust
IoT devices β†’ backend βœ“ YES Each device has unique cert
Browser β†’ web app βœ— NO Users don't have certs. Use OAuth/session cookies.
Mobile app β†’ API Partial Possible (embed cert in app) but hard to rotate. Prefer OAuth.
Third-party API clients βœ“ YES Better than API keys (harder to leak)
mTLS Best Practices:
  • Short-lived certs (1-24 hours) with automatic rotation
  • Use service mesh (Istio/Linkerd) for automatic sidecar injection
  • Monitor cert expiry (alert if rotation fails)
  • Store CA private key in HSM
  • Use separate CAs per environment (dev/staging/prod)

9. Zero Trust Architecture

"Never trust, always verify." Traditional perimeter security assumes inside = trusted. Zero trust assumes breach and verifies every request.

Traditional vs Zero Trust

graph TB subgraph "Traditional Perimeter Security" A1[Firewall] --> A2[Internal Network
TRUSTED] A2 --> A3[Service A] A2 --> A4[Service B] A2 --> A5[Database] A3 -.->|No auth| A4 A4 -.->|No auth| A5 end subgraph "Zero Trust" B1[Identity Provider] --> B2[Service A] B1 --> B3[Service B] B1 --> B4[Database] B2 -->|mTLS + token| B3 B3 -->|mTLS + token| B4 B2 -.->|Every request verified| B3 B3 -.->|Every request verified| B4 end style A2 fill:#a44 style B1 fill:#4a4

Core Principles

1. Verify Explicitly

Authenticate and authorize based on all available data: identity, location, device, risk.

// Every request evaluated
if (!authenticated(request)) return 401
if (!authorized(user, resource, action)) return 403
if (!device_trusted(request.device_id)) return 403
if (risk_score(request) > threshold) require_mfa()

2. Least Privilege Access

Grant minimum permissions needed. Just-in-time (JIT) access.

// Traditional: Always-on admin access
user.role = 'admin'  // Forever

// Zero Trust: Temporary elevated access
grant_access(user, 'admin', duration='1 hour', reason='incident-123')

// Access auto-expires
// Approval workflow for sensitive operations

3. Assume Breach

Design for inevitable compromise. Minimize blast radius, detect quickly, contain.

  • Microsegmentation: Firewall between every service (not just perimeter)
  • Encryption everywhere: TLS for all traffic (internal + external)
  • Audit everything: Log every access, detect anomalies
  • Immutable infrastructure: Rebuild, don't patch (no persistent compromise)

BeyondCorp (Google's Implementation)

graph TB subgraph "User Access" A[User Laptop
untrusted network] --> B[Access Proxy] end subgraph "Verification" B --> C[Identity Provider
User: alice@google.com] B --> D[Device Trust
Managed? Patched? Encrypted?] B --> E[Context Engine
Location, time, risk] end C --> F{All checks pass?} D --> F E --> F F -->|Yes| G[Internal Service] F -->|No| H[❌ Deny] G -.->|No VPN needed| A G -.->|Works from anywhere| A style F fill:#a4a style G fill:#4a4 style H fill:#a44

Key Components

Component Purpose Implementation
Access Proxy Single entry point, enforce policies Google IAP, Cloudflare Access, Zscaler
Identity Provider Who are you? (SSO) Google Workspace, Okta, Azure AD
Device Trust Is device managed, secure? MDM (Jamf, Intune), device certs
Context Engine Risk score based on behavior ML models (unusual location, time, access pattern)
Policy Engine Centralized authorization OPA, Google Zanzibar

Microsegmentation

Divide network into small zones. Firewall between each zone. Limits lateral movement.

graph TB subgraph "Traditional Flat Network" A1[Service A] -.->|No firewall| A2[Service B] A2 -.->|No firewall| A3[Service C] A3 -.->|No firewall| A4[Database] A5[Attacker] -.->|Compromise A
β†’ Access all| A1 end subgraph "Microsegmented" B1[Service A] -->|Firewall rule:
Allow B on port 8080| B2[Service B] B2 -->|Firewall rule:
Allow C on port 8080| B3[Service C] B3 -->|Firewall rule:
Allow C on port 5432| B4[Database] B5[Attacker] -->|Compromise A| B1 B1 -.->|❌ Can't reach C or DB| B3 end style A5 fill:#a44 style B5 fill:#a44

Implementation

// Kubernetes NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: service-b-allow-from-a
spec:
  podSelector:
    matchLabels:
      app: service-b
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: service-a
    ports:
    - protocol: TCP
      port: 8080

// Result: Only service-a can reach service-b
// All other traffic denied by default

Zero Trust for APIs

sequenceDiagram participant Client participant API Gateway participant Identity Provider participant Policy Engine participant API Service Client->>API Gateway: GET /api/resource
Authorization: Bearer token API Gateway->>Identity Provider: Validate token Identity Provider-->>API Gateway: βœ“ Valid (user: alice) API Gateway->>API Gateway: Check device trust
(extract device_id from cert) API Gateway->>Policy Engine: Can alice access /api/resource
from device_123 at this time? Policy Engine-->>API Gateway: βœ“ Allowed API Gateway->>API Service: Forward request (with user context) API Service-->>API Gateway: Data API Gateway-->>Client: Data Note over Client,API Service: Every request:
1. AuthN (who?)
2. Device check
3. AuthZ (can they?)
4. Audit log

Zero Trust Maturity Model

Stage Characteristics Implementation
1. Traditional Perimeter firewall, VPN for remote access Trusted internal network
2. Initial MFA, SSO, some network segmentation Identity layer added, but still VPN
3. Advanced mTLS, microsegmentation, context-aware access Service mesh, device trust, policy engine
4. Optimal Full zero trust, automated response, no VPN BeyondCorp-style, ML anomaly detection

Zero Trust: Production Implementation Challenges

Implementing zero trust in production is hard. Here are the real operational challenges and their solutions:

Challenge 1: Certificate Rotation Failures

Problem: If a certificate expires, service-to-service calls fail completely. In a microservices architecture with 100+ services using mTLS, a single cert expiry can cascade into a full outage.

graph TD A[Service A] -->|mTLS cert expires| B[Service B] B -->|All requests fail| A A -.->|Can't talk to| C[Service C] B -.->|Can't talk to| D[Database] style A fill:#e74c3c style B fill:#e74c3c style C fill:#f39c12 style D fill:#f39c12

Solution:

  • Auto-renewal 30 days before expiry: Use cert-manager (Kubernetes) or Vault PKI to automatically renew certs
  • Monitoring: Alert on certs expiring within 7 days (Prometheus: x509_cert_expiry < 7*24*3600)
  • Grace period: Accept certs up to 24 hours after expiry (not recommended for production, but helpful during incidents)
  • Canary rotation: Rotate 1% of services first, monitor error rates, then roll out
\ cert-manager Kubernetes config
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: service-a-tls
spec:
  secretName: service-a-tls
  duration: 720h  # 30 days
  renewBefore: 240h  # Renew 10 days before expiry
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer

Challenge 2: Policy Engine as Bottleneck

Problem: Every request needs an authorization check. At 100,000 requests/sec, the policy engine must handle 100,000 authorization decisions/sec. A centralized policy engine becomes a bottleneck and single point of failure.

Solution:

  • Cache decisions: Cache authZ results for 5-60 seconds per (user, resource, action) tuple
  • OPA as sidecar: Deploy Open Policy Agent as sidecar in each pod (local evaluation, no network hop)
  • Policy compilation: Pre-compile policies to WebAssembly for 10x faster evaluation
  • Tiered caching: L1 = in-memory (1ms), L2 = Redis (5ms), L3 = policy engine (20ms)
sequenceDiagram participant Service participant OPA Sidecar participant Redis Cache participant Central Policy Engine Service->>OPA Sidecar: Can alice access /api/resource? OPA Sidecar->>OPA Sidecar: Check L1 cache (hit) OPA Sidecar-->>Service: βœ“ Allowed (1ms) Note over Service,Central Policy Engine: On cache miss: Service->>OPA Sidecar: Can bob access /admin? OPA Sidecar->>Redis Cache: Check L2 (miss) OPA Sidecar->>Central Policy Engine: Evaluate policy Central Policy Engine-->>OPA Sidecar: βœ“ Allowed OPA Sidecar->>Redis Cache: Cache result (TTL=60s) OPA Sidecar-->>Service: βœ“ Allowed (25ms)
\ OPA sidecar with caching (Kubernetes pod)
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    image: myapp:v1
  - name: opa
    image: openpolicyagent/opa:latest
    args:
    - "run"
    - "--server"
    - "--addr=localhost:8181"
    - "--set=decision_logs.console=true"
    - "--set=caching.inter_query_builtin_cache.max_size_bytes=104857600"  // 100MB cache

Challenge 3: Device Trust Failure

Problem: If the MDM (Mobile Device Management) service goes down, you can't verify device posture. Do you deny all access (availability hit) or allow without device check (security risk)?

Solution:

  • Grace period: Use last-known-good device state for 4 hours if MDM is unreachable
  • Fallback to MFA: If device trust fails, require additional MFA challenge (FIDO2 key)
  • Tiered access: Allow read-only access without device trust, require full trust for write operations
  • Redundant MDM: Deploy MDM in multiple regions, failover on 503 errors
Scenario MDM Status Device Last Verified Action
Normal βœ… Healthy 5 min ago βœ… Allow (device trusted)
MDM down ❌ Unreachable 2 hours ago βœ… Allow (grace period, last-known-good)
MDM down (stale) ❌ Unreachable 6 hours ago ⚠️ Require MFA (device trust uncertain)
Device compromised βœ… Healthy 1 min ago (❌ jailbroken) ❌ Deny (device not trusted)

Challenge 4: Lateral Movement Detection

Problem: Even with zero trust, once an attacker compromises a service, they can move laterally between services. Traditional firewalls don't stop east-west traffic within a cluster.

Solution:

  • Microsegmentation: NetworkPolicies (Kubernetes) or security groups (AWS) to deny-by-default and allow-list only necessary service-to-service paths
  • Service identity (SPIFFE): Each service has a cryptographic identity, verified on every call
  • Anomaly detection: Alert on unusual service-to-service patterns (e.g., service-a calls service-z for the first time, service-b has 90% authZ failures)
  • Honeypot services: Deploy fake services that should never be called; if they receive traffic, it's an attack
graph TD subgraph "Normal Traffic" A1[Frontend] -->|βœ“ Allowed| B1[API Gateway] B1 -->|βœ“ Allowed| C1[Order Service] C1 -->|βœ“ Allowed| D1[Database] end subgraph "Lateral Movement Attack" A2[Compromised
Frontend] -.->|❌ Blocked| D2[Database] A2 -.->|❌ Blocked| E2[Admin Service] A2 -->|Honeypot triggered| F2[Fake Payment API] end style A2 fill:#e74c3c style D2 fill:#2ecc71 style E2 fill:#2ecc71 style F2 fill:#f39c12
\ Kubernetes NetworkPolicy: service-a can only talk to service-b
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: service-b-ingress
spec:
  podSelector:
    matchLabels:
      app: service-b
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: service-a
    ports:
    - protocol: TCP
      port: 8080

\ Result: service-c, service-d, etc. CANNOT reach service-b
\ Attacker compromising service-c cannot pivot to service-b

BeyondCorp Components Table

Google's BeyondCorp implementation (reference architecture):

Component Function Technology
Access Proxy Identity-aware proxy, enforces authN/authZ Envoy + OPA, Google IAP, Cloudflare Access
Device Inventory Trust database of known devices Jamf (macOS), Intune (Windows), custom DB
Access Control Engine Policy engine (who can access what) OPA, Google Zanzibar, Cedar (AWS)
Trust Pipeline Continuous security posture assessment CrowdStrike, Wiz, custom scripts
Identity Provider User authentication Okta, Azure AD, Google Workspace
Interview Question: "How would you secure a microservices architecture?"

Answer: Zero trust approach:
  1. mTLS everywhere: Service mesh (Istio/Linkerd) for automatic mTLS, SPIFFE/SPIRE for identity
  2. Microsegmentation: NetworkPolicies (allow-list, default deny)
  3. Authentication: JWT from identity provider (Okta/Auth0), short expiry (15 min)
  4. Authorization: Centralized policy engine (OPA/Zanzibar), checked per request
  5. Observability: Log every service-to-service call, detect anomalies (unusual caller, high failure rate)
  6. Secrets: Vault for dynamic secrets, rotate DB creds hourly
  7. Immutable infra: Container images signed, deploy-only (no SSH into pods)
Key: Assume any service can be compromised, limit blast radius.

10. API Security

10.1 Rate Limiting

Prevent abuse by limiting requests per time window. Multiple dimensions: per-user, per-IP, per-endpoint.

Algorithms

Token Bucket

Bucket holds tokens. Each request consumes token. Tokens refill at constant rate. Allows bursts.

graph TB A[Token Bucket
Capacity: 100
Refill: 10/sec] --> B{Tokens available?} B -->|Yes| C[Allow request
Remove 1 token] B -->|No| D[❌ Reject 429] C --> E[Bucket: 99] E -.->|Wait 1s| F[Bucket: 100
Refilled] style C fill:#4a4 style D fill:#a44
class TokenBucket:
  def __init__(self, capacity, refill_rate):
    self.capacity = capacity
    self.tokens = capacity
    self.refill_rate = refill_rate  // per second
    self.last_refill = now()

  def allow_request(self):
    // Refill tokens
    elapsed = now() - self.last_refill
    self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
    self.last_refill = now()

    // Check token
    if self.tokens >= 1:
      self.tokens -= 1
      return True
    return False  // Rate limited
Sliding Window

Count requests in rolling time window. More accurate than fixed window.

// Fixed Window (inaccurate)
Window 1 (0-60s):  100 requests at t=59s  βœ“ Allowed
Window 2 (60-120s): 100 requests at t=61s βœ“ Allowed
// 200 requests in 2 seconds! Burst at boundary.

// Sliding Window (accurate)
window_size = 60s
max_requests = 100

requests_in_last_60s = count_requests(now() - 60s, now())
if requests_in_last_60s < max_requests:
  allow()
else:
  reject()

Multi-Tier Rate Limiting

Tier Limit Purpose
Per-IP 1000 req/min Prevent DDoS from single source
Per-User 100 req/min Prevent abuse by authenticated users
Per-Endpoint POST /login: 5/min Protect expensive operations
Global 1M req/min Protect backend capacity

Implementation (Redis)

// Token bucket with Redis
key = f"rate_limit:{user_id}"

// Get current tokens
tokens = redis.get(key) or bucket_capacity
last_refill = redis.get(f"{key}:last_refill") or now()

// Refill
elapsed = now() - last_refill
tokens = min(bucket_capacity, tokens + elapsed * refill_rate)

// Check
if tokens >= 1:
  redis.set(key, tokens - 1)
  redis.set(f"{key}:last_refill", now())
  return allow()
else:
  return reject(429, headers={
    'X-RateLimit-Limit': bucket_capacity,
    'X-RateLimit-Remaining': 0,
    'X-RateLimit-Reset': next_refill_time
  })

10.2 Input Validation & Sanitization

Validation Rules

  • Type checking: email = string, age = int > 0
  • Length limits: username max 50 chars, bio max 500
  • Format validation: email regex, UUID format, phone number
  • Whitelist: Accept only known-good values (enum: ['admin', 'user'])
  • Blacklist (weak): Reject known-bad values (easily bypassed)
// ❌ BAD: No validation
function updateUser(data) {
  db.query(`UPDATE users SET name = '${data.name}' WHERE id = ${data.id}`)
}

// βœ“ GOOD: Validate + parameterized query
function updateUser(data) {
  // Validate
  if (typeof data.id !== 'number' || data.id <= 0) throw Error('Invalid ID')
  if (typeof data.name !== 'string' || data.name.length > 100) throw Error('Invalid name')
  if (!/^[a-zA-Z0-9\s]+$/.test(data.name)) throw Error('Name contains invalid characters')

  // Parameterized query (prevents SQL injection)
  db.query('UPDATE users SET name = ? WHERE id = ?', [data.name, data.id])
}

SQL Injection Prevention

// ❌ VULNERABLE
query = f"SELECT * FROM users WHERE username = '{username}'"
// Attacker input: username = "admin' OR '1'='1"
// Query: SELECT * FROM users WHERE username = 'admin' OR '1'='1'
// Result: Returns all users!

// βœ“ SAFE: Parameterized query
query = "SELECT * FROM users WHERE username = ?"
db.execute(query, [username])
// Library escapes input, treats as literal string

10.3 CORS, CSRF, XSS

CORS (Cross-Origin Resource Sharing)

Browser security: prevent site A from reading responses from site B (unless B allows it).

// Browser at https://example.com
fetch('https://api.bank.com/balance')

// Server response headers
Access-Control-Allow-Origin: https://example.com  // Allow this origin
Access-Control-Allow-Credentials: true            // Allow cookies
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Authorization

// Without CORS headers β†’ Browser blocks response
Dangerous CORS:
Access-Control-Allow-Origin: *  // Allow any origin!
Access-Control-Allow-Credentials: true  // + Send cookies!
// = Any website can read your bank balance if you're logged in
Never use * with credentials. Whitelist specific origins.

CSRF (Cross-Site Request Forgery)

See Session Management section. Prevention: SameSite cookies + CSRF tokens.

XSS (Cross-Site Scripting)

Attacker injects malicious JavaScript into your site.

// ❌ VULNERABLE
comment = request.body.comment
html = f"
{comment}
" // Directly inserted // Attacker input: comment = "" // Result: Your JavaScript runs on victim's browser, steals cookies // βœ“ SAFE: Escape HTML import html comment = html.escape(request.body.comment) // <script> instead of function toggleTheme() { const html = document.documentElement; const currentTheme = html.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', newTheme); mermaid.initialize({ startOnLoad: true, theme: newTheme }); location.reload(); }