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.
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
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
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.
'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) |
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.
Server accepts Β±1 window (90s total)
FIDO2/WebAuthn (Hardware Keys)
Public key cryptography. Private key never leaves hardware device. Resistant to phishing (domain-bound).
(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.
- 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
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.
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
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) |
- 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
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)
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
3. Session Management
Server-Side Sessions
How It Works
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
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.
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.
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
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.
- 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
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.
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.
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.
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.
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.
- 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 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)
+ 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 |
- 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)
Answer: Use OpenID Connect (OIDC) Authorization Code flow:
- Register app with Google, get client_id + client_secret
- Redirect user to Google's /authorize endpoint with scope=openid profile email
- Google authenticates user, redirects back with code
- Backend exchanges code for tokens (access_token + id_token) at Google's /token endpoint
- Decode id_token (JWT) to get user's email, name, unique ID (sub claim)
- Create or update user in our database (match by Google's sub claim)
- Issue our own session token/JWT for subsequent requests
5. JWT (JSON Web Tokens)
Structure
// 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.
'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.
// 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 |
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"
}
JWT Validation
// 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) |
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
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.
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 |
- 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
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.
-- 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
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
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)
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
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
Answer: Use ReBAC (Google Zanzibar):
- Model: Documents, folders, users, groups as objects with relationships (owner, editor, viewer, member)
- Tuples: Store relationships:
(doc:123, owner, user:alice),(doc:123, parent, folder:work),(folder:work, viewer, group:team) - Inheritance: Document inherits folder permissions, groups expand to members
- Check: Query "Can bob view doc:123?" β Walk graph: bob β team β viewer of folder β doc parent folder
- Scale: Distribute graph, cache hot paths, denormalize for common checks
7. Encryption
7.1 Symmetric Encryption
Same key for encryption and decryption. Fast, but key distribution is the challenge.
'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 |
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) |
- 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.
'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.
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.
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.
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)
(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).
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).
(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.
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)
Answer: Use envelope encryption:
- Master Key: Store in AWS KMS (customer-managed key, HSM-backed)
- Data Keys: Generate unique AES-256 key per user (or per batch)
- Encrypt Data: Encrypt user record with data key
- Encrypt Key: Encrypt data key with KMS master key
- Store Both: Store encrypted data + encrypted key together in S3
- Rotation: Rotate KMS key annually (re-encrypt data keys, not data)
8. Mutual TLS (mTLS)
Both client and server authenticate with certificates. Standard TLS = server proves identity, mTLS = both prove identity.
Standard TLS vs mTLS
mTLS Handshake
(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.
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?"
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)
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) |
- 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
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)
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.
β 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
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.
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)
\ 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
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 |
Answer: Zero trust approach:
- mTLS everywhere: Service mesh (Istio/Linkerd) for automatic mTLS, SPIFFE/SPIRE for identity
- Microsegmentation: NetworkPolicies (allow-list, default deny)
- Authentication: JWT from identity provider (Okta/Auth0), short expiry (15 min)
- Authorization: Centralized policy engine (OPA/Zanzibar), checked per request
- Observability: Log every service-to-service call, detect anomalies (unusual caller, high failure rate)
- Secrets: Vault for dynamic secrets, rotate DB creds hourly
- Immutable infra: Container images signed, deploy-only (no SSH into pods)
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.
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
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