Auth Operations
Comprehensive authentication and authorization patterns for secure application development across languages and frameworks.
Authentication Method Decision Tree
Use this tree to select the right authentication strategy for your use case.
What are you building?
│
├─ Traditional web application (server-rendered)?
│ └─ Session-based authentication
│ ├─ Server stores session data (Redis/DB)
│ ├─ Session ID in httpOnly cookie
│ └─ Best for: monoliths, SSR apps, admin panels
│
├─ API consumed by multiple clients?
│ └─ JWT (JSON Web Tokens)
│ ├─ Stateless, self-contained tokens
│ ├─ Access token (short-lived) + refresh token (long-lived)
│ └─ Best for: microservices, mobile apps, SPAs via BFF
│
├─ Service-to-service communication?
│ └─ API keys or Client Credentials (OAuth2)
│ ├─ API keys: simple, scoped, rotatable
│ ├─ Client Credentials: OAuth2 standard, token-based
│ └─ Best for: internal services, third-party integrations
│
├─ Third-party login (Google, GitHub, etc.)?
│ └─ OAuth2 / OpenID Connect
│ ├─ Authorization Code + PKCE for web/mobile
│ ├─ Delegate identity to trusted providers
│ └─ Best for: consumer apps, social login
│
├─ Passwordless authentication?
│ └─ Passkeys (WebAuthn) or Magic Links
│ ├─ Passkeys: phishing-resistant, biometric/hardware
│ ├─ Magic links: email-based, time-limited
│ └─ Best for: high-security, modern UX
│
└─ Internal tool / staff app with an existing IdP?
└─ Identity-aware proxy (Cloudflare Access)
├─ Authn enforced at the edge, before your origin
├─ Origin verifies the proxy's signed JWT (never a bare header)
└─ Best for: admin panels, partner portals, not consumer signup
JWT Quick Reference
Structure
Header.Payload.Signature
Header: { "alg": "RS256", "typ": "JWT" }
Payload: { "iss": "auth.example.com", "sub": "user_123", ... }
Signature: RSASHA256(base64(header) + "." + base64(payload), privateKey)
Common Claims
| Claim |
Name |
Purpose |
Example |
iss |
Issuer |
Who issued the token |
"auth.example.com" |
sub |
Subject |
Who the token represents |
"user_123" |
exp |
Expiration |
When the token expires |
1700000000 (Unix timestamp) |
iat |
Issued At |
When the token was created |
1699999100 |
aud |
Audience |
Intended recipient(s) |
"api.example.com" |
jti |
JWT ID |
Unique token identifier |
"a1b2c3d4" (for revocation) |
nbf |
Not Before |
Token not valid before this time |
1699999100 |
Signing Algorithms
| Algorithm |
Type |
Key |
Use When |
| RS256 |
Asymmetric (RSA) |
Public/private key pair |
Distributed systems, multiple verifiers |
| ES256 |
Asymmetric (ECDSA) |
Public/private key pair |
Same as RS256, smaller keys/signatures |
| HS256 |
Symmetric (HMAC) |
Shared secret |
Single service, simple setups |
Rule of thumb: Use asymmetric (RS256/ES256) when the token issuer and verifier are different services. Use HS256 only when a single service both creates and verifies tokens.
Access + Refresh Token Pattern
┌──────────┐ ┌──────────┐
│ Client │─── login ────────>│ Auth │
│ │<── access (15m) ──│ Server │
│ │<── refresh (7d) ──│ │
│ │ └──────────┘
│ │─── API call ─────>┌──────────┐
│ │ (access token) │ Resource │
│ │<── response ──────│ Server │
│ │ └──────────┘
│ │─── access expired │ │
│ │─── refresh ──────>│ Auth │
│ │<── new access ────│ Server │
│ │<── new refresh ───│ (rotate)│
└──────────┘ └──────────┘
- Access token: Short-lived (5-15 minutes), used for API calls
- Refresh token: Long-lived (7-30 days), used to get new access tokens
- Rotation: Issue a new refresh token with each use, invalidate the old one
- Family detection: Track refresh token lineage; if a revoked token is reused, invalidate the entire family
OAuth2 Flow Decision Tree
What type of client?
│
├─ Web app with backend (Next.js, Rails, Django)?
│ └─ Authorization Code + PKCE
│ ├─ Redirect user to authorization server
│ ├─ Receive code at callback URL
│ ├─ Exchange code for tokens server-side
│ └─ PKCE prevents code interception attacks
│
├─ SPA (React, Vue) without backend?
│ └─ Authorization Code + PKCE (via BFF)
│ ├─ Use a Backend-for-Frontend to handle tokens
│ ├─ Never store tokens in browser-accessible storage
│ └─ BFF proxies API calls with token attached
│
├─ Mobile app (iOS, Android)?
│ └─ Authorization Code + PKCE
│ ├─ Use custom URI scheme or universal links for redirect
│ ├─ PKCE is mandatory (public client)
│ └─ Store tokens in secure enclave/keystore
│
├─ Server-to-server (no user)?
│ └─ Client Credentials
│ ├─ Authenticate with client_id + client_secret
│ ├─ No user context, service-level access
│ └─ Token cached until expiry
│
├─ CLI tool or smart TV?
│ └─ Device Code
│ ├─ Display code and URL to user
│ ├─ User authenticates on another device
│ ├─ CLI/TV polls for completion
│ └─ Good UX for input-constrained devices
│
└─ Microservice acting on behalf of a user?
└─ Token Exchange (RFC 8693)
├─ Exchange user's token for a scoped downstream token
├─ Maintains user context across services
└─ Use `act` claim for delegation chain
Authorization Model Decision Tree
How complex are your access control needs?
│
├─ Simple: just "can user X do action Y"?
│ └─ Permission-based (direct)
│ ├─ user_permissions table
│ ├─ Simple to implement, hard to scale
│ └─ Good for: small apps, prototypes
│
├─ Users grouped into roles with fixed permissions?
│ └─ RBAC (Role-Based Access Control)
│ ├─ Roles: admin, editor, viewer
│ ├─ Each role has a set of permissions
│ ├─ Users assigned one or more roles
│ └─ Good for: most apps, admin panels, team tools
│
├─ Decisions depend on attributes (time, location, resource owner)?
│ └─ ABAC (Attribute-Based Access Control)
│ ├─ Policies evaluate subject + resource + environment attributes
│ ├─ "Allow if user.department == resource.department AND time < 17:00"
│ ├─ Flexible but complex
│ └─ Good for: enterprise, compliance-heavy, context-dependent access
│
└─ Access based on relationships (owner, parent, shared with)?
└─ ReBAC (Relationship-Based Access Control)
├─ Google Zanzibar model
├─ Tuples: user:alice#viewer@document:report
├─ Supports inheritance: folder viewer → document viewer
├─ Tools: OpenFGA, SpiceDB, Ory Keto
└─ Good for: file sharing, nested resources, social features
Session Management Quick Reference
Cookie Security Settings
| Setting |
Value |
Purpose |
SameSite |
Strict |
Cookie sent only for same-site requests (best CSRF protection) |
SameSite |
Lax |
Cookie sent for top-level navigations (good default) |
SameSite |
None |
Cookie sent for cross-site requests (requires Secure) |
Secure |
true |
Cookie only sent over HTTPS |
HttpOnly |
true |
Cookie not accessible via JavaScript (prevents XSS theft) |
__Host- prefix |
N/A |
Requires Secure, no Domain, Path=/ (strictest) |
__Secure- prefix |
N/A |
Requires Secure flag |
Max-Age |
seconds |
Cookie lifetime (prefer over Expires) |
Path |
/ |
Scope cookie to path (usually /) |
Recommended Cookie Configuration
Set-Cookie: __Host-session=abc123;
Secure;
HttpOnly;
SameSite=Lax;
Max-Age=86400;
Path=/
Session Expiry Strategies
| Strategy |
Typical Value |
Notes |
| Idle timeout |
15-30 minutes |
Reset on each request |
| Absolute timeout |
8-24 hours |
Force re-authentication |
| Sliding window |
30 min idle, 8h max |
Best balance |
| Remember me |
30 days |
Extended session, reduced privileges |
Password Handling Quick Reference
Hashing Algorithms
| Algorithm |
Verdict |
Notes |
| argon2id |
BEST |
Memory-hard, resists GPU attacks, recommended by OWASP |
| bcrypt |
GOOD |
Battle-tested, cost factor 12+, 72-byte input limit |
| scrypt |
GOOD |
Memory-hard, less common library support |
| PBKDF2 |
ACCEPTABLE |
FIPS compliant, use 600k+ iterations with SHA-256 |
| SHA-256/512 |
BAD |
Too fast, no salt built-in, easily brute-forced |
| MD5 |
NEVER |
Broken, rainbow tables widely available |
Password Rules (NIST 800-63B)
| Rule |
Guidance |
| Minimum length |
8 characters (12+ recommended) |
| Maximum length |
At least 64 characters |
| Complexity rules |
Do NOT require special chars/uppercase/numbers |
| Breached password check |
Check against known breached passwords (HaveIBeenPwned API) |
| Password hints |
Do NOT allow |
| Forced rotation |
Do NOT force periodic changes (only on breach) |
| Paste into password field |
ALLOW (supports password managers) |
Rate Limiting Login Attempts
| Attempt |
Response |
| 1-5 |
Normal login |
| 6-10 |
CAPTCHA required |
| 11-20 |
Progressive delays (2s, 4s, 8s...) |
| 20+ |
Temporary account lockout (15-30 min) |
Important: Use consistent response times for both success and failure to prevent timing-based username enumeration.
MFA Quick Reference
Methods Ranked by Security
| Method |
Security |
UX |
Notes |
| WebAuthn/Passkeys |
Highest |
Good |
Phishing-resistant, hardware-backed |
| TOTP (Authenticator) |
High |
Medium |
App-based (Google/Microsoft Authenticator) |
| Push notifications |
High |
Good |
Requires mobile app |
| Email OTP |
Medium |
Medium |
Depends on email security |
| SMS OTP |
Low |
Easy |
SIM swap vulnerable, use as fallback only |
TOTP Implementation Checklist
Passkey/WebAuthn Checklist
Identity-Aware Proxy Quick Reference
When authn is delegated to a proxy edge (Cloudflare Access, Google IAP, oauth2-proxy), two invariants carry the whole model:
- Verify the assertion. The proxy's identity header is a signed JWT — verify signature + issuer + per-application audience against the proxy's JWKS on every request. Never trust the plain email convenience headers.
- Close every path around the proxy. The header is only meaningful if the proxy is the only way to reach the origin (
workers_dev = false, firewalled origin, or tunnel). An open origin makes any header forgeable.
Proxy edge (authn) ──JWT header──> Origin verifies JWT ──> app user lookup ──> role/scope binding
│ │ 403 on any failure │ 403 if no row (server-side)
└ IdP / OTP login, sessions └ cached JWKS, └ proxy admits ≠ app authorizes
rate limits, bot defense refetch on unknown kid
Machine routes (webhooks, ingest) get Service-Auth/Bypass at the edge + bearer keys at the origin, mounted outside the human-auth middleware. Full treatment: references/cloudflare-access.md.
Common Gotchas
| Gotcha |
Why It's Dangerous |
Fix |
| JWT stored in localStorage |
XSS can steal tokens, no expiry enforcement by browser |
Use httpOnly cookies or BFF pattern |
| Missing PKCE in OAuth2 |
Authorization code interception attacks possible |
Always use PKCE, even for confidential clients |
| Role explosion in RBAC |
Hundreds of roles become unmanageable |
Move to ABAC or ReBAC for complex scenarios |
| String comparison for tokens |
Timing attacks reveal token value character by character |
Use constant-time comparison (crypto.timingSafeEqual) |
| No token revocation strategy |
Cannot invalidate compromised JWTs before expiry |
Short expiry + refresh tokens, or maintain a blocklist |
CORS with credentials: true |
Access-Control-Allow-Origin: * does not work with credentials |
Specify exact origin, set Access-Control-Allow-Credentials: true |
SameSite=None without Secure |
Browser silently rejects the cookie |
Always pair SameSite=None with Secure flag |
| Refresh token reuse without detection |
Stolen refresh tokens grant indefinite access |
Rotate refresh tokens, detect reuse (token families) |
| Using OAuth2 Implicit grant |
Tokens exposed in URL fragment, no refresh tokens |
Use Authorization Code + PKCE instead (Implicit is deprecated) |
| Password in URL or logs |
URLs are logged by proxies, browsers, and servers |
Always send credentials in request body or headers |
| Missing CSRF protection with cookies |
Cookie-based auth is vulnerable to cross-site request forgery |
Use SameSite cookies + CSRF tokens for state-changing ops |
| Long-lived access tokens (hours/days) |
Large attack window if token is compromised |
Keep access tokens to 5-15 minutes, use refresh tokens |
| Storing API keys in plaintext |
Database breach exposes all keys |
Hash stored keys (SHA-256 of key), store prefix for lookup |
Not validating JWT aud claim |
Token meant for Service A accepted by Service B |
Always validate aud matches your service identifier |
| Session fixation |
Attacker sets session ID before login, then hijacks it |
Regenerate session ID after authentication |
| Hardcoded secrets in code |
Secrets leak via source control |
Use environment variables or secret managers (Vault, AWS SSM) |
| Trusting an identity-aware proxy's plain email header |
Headers are attacker-settable on any unproxied path |
Verify the proxy's signed JWT (sig + issuer + audience); close every path around the proxy |
| Auth-library middleware as the only session check |
Framework middleware can be bypassed (Next.js CVE-2025-29927 class) |
Re-check the session in the data-access layer / route handlers |
Reference Files
| File |
Contents |
Lines |
references/jwt-sessions.md |
JWT structure, signing, sessions, cookies, CSRF, storage |
~650 |
references/oauth2-oidc.md |
OAuth2 flows, OIDC, provider integration, social login |
~700 |
references/authorization.md |
RBAC, ABAC, ReBAC, RLS, multi-tenant, audit logging |
~600 |
references/implementation.md |
Password hashing, MFA, rate limiting, API keys, reset flows |
~550 |
references/cloudflare-access.md |
Identity-aware proxies via Cloudflare Access: app/policy anatomy, token claims, JWT verification, closed-origin precondition, service auth, sessions/logout/SPA, local dev |
~330 |
references/better-auth.md |
Better Auth library: server/client setup, adapters, session model, social login, plugin catalog (passkey/2FA/org/SSO), Hono integration, migration |
~240 |
See Also
- security-ops - Broader security patterns: OWASP, headers, input validation, encryption
- api-design-ops - API design including authentication endpoints, rate limiting
- postgres-ops - Row-level security (RLS) policies for database authorization
- cloudflare-ops - Workers runtime, wrangler config, secrets, deploy mechanics behind an Access-fronted origin
1---2name: auth-ops3description: Authentication and authorization patterns - JWT, OAuth2, sessions, RBAC, ABAC, passkeys, MFA, identity-aware proxies, and Better Auth. Use for: authentication, jwt, oauth2, session, login, rbac, abac, passkey, mfa, totp, api key, token, cookie, csrf, bearer token, refresh token, oidc, cloudflare access, zero trust, Cf-Access-Jwt-Assertion, AUD tag, service auth, better auth.4license: MIT5---67# Auth Operations89Comprehensive authentication and authorization patterns for secure application development across languages and frameworks.1011## Authentication Method Decision Tree1213Use this tree to select the right authentication strategy for your use case.1415```16What are you building?17│18├─ Traditional web application (server-rendered)?19│ └─ Session-based authentication20│ ├─ Server stores session data (Redis/DB)21│ ├─ Session ID in httpOnly cookie22│ └─ Best for: monoliths, SSR apps, admin panels23│24├─ API consumed by multiple clients?25│ └─ JWT (JSON Web Tokens)26│ ├─ Stateless, self-contained tokens27│ ├─ Access token (short-lived) + refresh token (long-lived)28│ └─ Best for: microservices, mobile apps, SPAs via BFF29│30├─ Service-to-service communication?31│ └─ API keys or Client Credentials (OAuth2)32│ ├─ API keys: simple, scoped, rotatable33│ ├─ Client Credentials: OAuth2 standard, token-based34│ └─ Best for: internal services, third-party integrations35│36├─ Third-party login (Google, GitHub, etc.)?37│ └─ OAuth2 / OpenID Connect38│ ├─ Authorization Code + PKCE for web/mobile39│ ├─ Delegate identity to trusted providers40│ └─ Best for: consumer apps, social login41│42├─ Passwordless authentication?43│ └─ Passkeys (WebAuthn) or Magic Links44│ ├─ Passkeys: phishing-resistant, biometric/hardware45│ ├─ Magic links: email-based, time-limited46│ └─ Best for: high-security, modern UX47│48└─ Internal tool / staff app with an existing IdP?49 └─ Identity-aware proxy (Cloudflare Access)50 ├─ Authn enforced at the edge, before your origin51 ├─ Origin verifies the proxy's signed JWT (never a bare header)52 └─ Best for: admin panels, partner portals, not consumer signup53```5455## JWT Quick Reference5657### Structure5859```60Header.Payload.Signature6162Header: { "alg": "RS256", "typ": "JWT" }63Payload: { "iss": "auth.example.com", "sub": "user_123", ... }64Signature: RSASHA256(base64(header) + "." + base64(payload), privateKey)65```6667### Common Claims6869| Claim | Name | Purpose | Example |70|-------|------|---------|---------|71| `iss` | Issuer | Who issued the token | `"auth.example.com"` |72| `sub` | Subject | Who the token represents | `"user_123"` |73| `exp` | Expiration | When the token expires | `1700000000` (Unix timestamp) |74| `iat` | Issued At | When the token was created | `1699999100` |75| `aud` | Audience | Intended recipient(s) | `"api.example.com"` |76| `jti` | JWT ID | Unique token identifier | `"a1b2c3d4"` (for revocation) |77| `nbf` | Not Before | Token not valid before this time | `1699999100` |7879### Signing Algorithms8081| Algorithm | Type | Key | Use When |82|-----------|------|-----|----------|83| **RS256** | Asymmetric (RSA) | Public/private key pair | Distributed systems, multiple verifiers |84| **ES256** | Asymmetric (ECDSA) | Public/private key pair | Same as RS256, smaller keys/signatures |85| **HS256** | Symmetric (HMAC) | Shared secret | Single service, simple setups |8687**Rule of thumb:** Use asymmetric (RS256/ES256) when the token issuer and verifier are different services. Use HS256 only when a single service both creates and verifies tokens.8889### Access + Refresh Token Pattern9091```92┌──────────┐ ┌──────────┐93│ Client │─── login ────────>│ Auth │94│ │<── access (15m) ──│ Server │95│ │<── refresh (7d) ──│ │96│ │ └──────────┘97│ │─── API call ─────>┌──────────┐98│ │ (access token) │ Resource │99│ │<── response ──────│ Server │100│ │ └──────────┘101│ │─── access expired │ │102│ │─── refresh ──────>│ Auth │103│ │<── new access ────│ Server │104│ │<── new refresh ───│ (rotate)│105└──────────┘ └──────────┘106```107108- **Access token:** Short-lived (5-15 minutes), used for API calls109- **Refresh token:** Long-lived (7-30 days), used to get new access tokens110- **Rotation:** Issue a new refresh token with each use, invalidate the old one111- **Family detection:** Track refresh token lineage; if a revoked token is reused, invalidate the entire family112113## OAuth2 Flow Decision Tree114115```116What type of client?117│118├─ Web app with backend (Next.js, Rails, Django)?119│ └─ Authorization Code + PKCE120│ ├─ Redirect user to authorization server121│ ├─ Receive code at callback URL122│ ├─ Exchange code for tokens server-side123│ └─ PKCE prevents code interception attacks124│125├─ SPA (React, Vue) without backend?126│ └─ Authorization Code + PKCE (via BFF)127│ ├─ Use a Backend-for-Frontend to handle tokens128│ ├─ Never store tokens in browser-accessible storage129│ └─ BFF proxies API calls with token attached130│131├─ Mobile app (iOS, Android)?132│ └─ Authorization Code + PKCE133│ ├─ Use custom URI scheme or universal links for redirect134│ ├─ PKCE is mandatory (public client)135│ └─ Store tokens in secure enclave/keystore136│137├─ Server-to-server (no user)?138│ └─ Client Credentials139│ ├─ Authenticate with client_id + client_secret140│ ├─ No user context, service-level access141│ └─ Token cached until expiry142│143├─ CLI tool or smart TV?144│ └─ Device Code145│ ├─ Display code and URL to user146│ ├─ User authenticates on another device147│ ├─ CLI/TV polls for completion148│ └─ Good UX for input-constrained devices149│150└─ Microservice acting on behalf of a user?151 └─ Token Exchange (RFC 8693)152 ├─ Exchange user's token for a scoped downstream token153 ├─ Maintains user context across services154 └─ Use `act` claim for delegation chain155```156157## Authorization Model Decision Tree158159```160How complex are your access control needs?161│162├─ Simple: just "can user X do action Y"?163│ └─ Permission-based (direct)164│ ├─ user_permissions table165│ ├─ Simple to implement, hard to scale166│ └─ Good for: small apps, prototypes167│168├─ Users grouped into roles with fixed permissions?169│ └─ RBAC (Role-Based Access Control)170│ ├─ Roles: admin, editor, viewer171│ ├─ Each role has a set of permissions172│ ├─ Users assigned one or more roles173│ └─ Good for: most apps, admin panels, team tools174│175├─ Decisions depend on attributes (time, location, resource owner)?176│ └─ ABAC (Attribute-Based Access Control)177│ ├─ Policies evaluate subject + resource + environment attributes178│ ├─ "Allow if user.department == resource.department AND time < 17:00"179│ ├─ Flexible but complex180│ └─ Good for: enterprise, compliance-heavy, context-dependent access181│182└─ Access based on relationships (owner, parent, shared with)?183 └─ ReBAC (Relationship-Based Access Control)184 ├─ Google Zanzibar model185 ├─ Tuples: user:alice#viewer@document:report186 ├─ Supports inheritance: folder viewer → document viewer187 ├─ Tools: OpenFGA, SpiceDB, Ory Keto188 └─ Good for: file sharing, nested resources, social features189```190191## Session Management Quick Reference192193### Cookie Security Settings194195| Setting | Value | Purpose |196|---------|-------|---------|197| `SameSite` | `Strict` | Cookie sent only for same-site requests (best CSRF protection) |198| `SameSite` | `Lax` | Cookie sent for top-level navigations (good default) |199| `SameSite` | `None` | Cookie sent for cross-site requests (requires `Secure`) |200| `Secure` | `true` | Cookie only sent over HTTPS |201| `HttpOnly` | `true` | Cookie not accessible via JavaScript (prevents XSS theft) |202| `__Host-` prefix | N/A | Requires Secure, no Domain, Path=/ (strictest) |203| `__Secure-` prefix | N/A | Requires Secure flag |204| `Max-Age` | seconds | Cookie lifetime (prefer over `Expires`) |205| `Path` | `/` | Scope cookie to path (usually `/`) |206207### Recommended Cookie Configuration208209```210Set-Cookie: __Host-session=abc123;211 Secure;212 HttpOnly;213 SameSite=Lax;214 Max-Age=86400;215 Path=/216```217218### Session Expiry Strategies219220| Strategy | Typical Value | Notes |221|----------|---------------|-------|222| **Idle timeout** | 15-30 minutes | Reset on each request |223| **Absolute timeout** | 8-24 hours | Force re-authentication |224| **Sliding window** | 30 min idle, 8h max | Best balance |225| **Remember me** | 30 days | Extended session, reduced privileges |226227## Password Handling Quick Reference228229### Hashing Algorithms230231| Algorithm | Verdict | Notes |232|-----------|---------|-------|233| **argon2id** | BEST | Memory-hard, resists GPU attacks, recommended by OWASP |234| **bcrypt** | GOOD | Battle-tested, cost factor 12+, 72-byte input limit |235| **scrypt** | GOOD | Memory-hard, less common library support |236| **PBKDF2** | ACCEPTABLE | FIPS compliant, use 600k+ iterations with SHA-256 |237| **SHA-256/512** | BAD | Too fast, no salt built-in, easily brute-forced |238| **MD5** | NEVER | Broken, rainbow tables widely available |239240### Password Rules (NIST 800-63B)241242| Rule | Guidance |243|------|----------|244| Minimum length | 8 characters (12+ recommended) |245| Maximum length | At least 64 characters |246| Complexity rules | Do NOT require special chars/uppercase/numbers |247| Breached password check | Check against known breached passwords (HaveIBeenPwned API) |248| Password hints | Do NOT allow |249| Forced rotation | Do NOT force periodic changes (only on breach) |250| Paste into password field | ALLOW (supports password managers) |251252### Rate Limiting Login Attempts253254| Attempt | Response |255|---------|----------|256| 1-5 | Normal login |257| 6-10 | CAPTCHA required |258| 11-20 | Progressive delays (2s, 4s, 8s...) |259| 20+ | Temporary account lockout (15-30 min) |260261**Important:** Use consistent response times for both success and failure to prevent timing-based username enumeration.262263## MFA Quick Reference264265### Methods Ranked by Security266267| Method | Security | UX | Notes |268|--------|----------|----|-------|269| **WebAuthn/Passkeys** | Highest | Good | Phishing-resistant, hardware-backed |270| **TOTP (Authenticator)** | High | Medium | App-based (Google/Microsoft Authenticator) |271| **Push notifications** | High | Good | Requires mobile app |272| **Email OTP** | Medium | Medium | Depends on email security |273| **SMS OTP** | Low | Easy | SIM swap vulnerable, use as fallback only |274275### TOTP Implementation Checklist276277- [ ] Generate 160-bit secret (base32 encoded)278- [ ] Build otpauth:// URI with issuer and account279- [ ] Display QR code for authenticator scanning280- [ ] Require verification of first code before enabling281- [ ] Accept current window +/- 1 (30-second steps)282- [ ] Generate 8-10 single-use backup codes283- [ ] Hash backup codes before storing284- [ ] Allow recovery via verified identity285286### Passkey/WebAuthn Checklist287288- [ ] Generate cryptographic challenge on server289- [ ] Set relying party ID (your domain)290- [ ] Store credential public key and ID291- [ ] Verify signature on authentication292- [ ] Support multiple credentials per user293- [ ] Handle platform vs cross-platform authenticators294- [ ] Provide fallback auth method295296## Identity-Aware Proxy Quick Reference297298When authn is delegated to a proxy edge (Cloudflare Access, Google IAP, oauth2-proxy), two invariants carry the whole model:2993001. **Verify the assertion.** The proxy's identity header is a signed JWT — verify signature + issuer + per-application audience against the proxy's JWKS on every request. Never trust the plain email convenience headers.3012. **Close every path around the proxy.** The header is only meaningful if the proxy is the *only* way to reach the origin (`workers_dev = false`, firewalled origin, or tunnel). An open origin makes any header forgeable.302303```304Proxy edge (authn) ──JWT header──> Origin verifies JWT ──> app user lookup ──> role/scope binding305 │ │ 403 on any failure │ 403 if no row (server-side)306 └ IdP / OTP login, sessions └ cached JWKS, └ proxy admits ≠ app authorizes307 rate limits, bot defense refetch on unknown kid308```309310Machine routes (webhooks, ingest) get Service-Auth/Bypass at the edge + bearer keys at the origin, mounted outside the human-auth middleware. Full treatment: `references/cloudflare-access.md`.311312## Common Gotchas313314| Gotcha | Why It's Dangerous | Fix |315|--------|--------------------|-----|316| JWT stored in localStorage | XSS can steal tokens, no expiry enforcement by browser | Use httpOnly cookies or BFF pattern |317| Missing PKCE in OAuth2 | Authorization code interception attacks possible | Always use PKCE, even for confidential clients |318| Role explosion in RBAC | Hundreds of roles become unmanageable | Move to ABAC or ReBAC for complex scenarios |319| String comparison for tokens | Timing attacks reveal token value character by character | Use constant-time comparison (`crypto.timingSafeEqual`) |320| No token revocation strategy | Cannot invalidate compromised JWTs before expiry | Short expiry + refresh tokens, or maintain a blocklist |321| CORS with `credentials: true` | `Access-Control-Allow-Origin: *` does not work with credentials | Specify exact origin, set `Access-Control-Allow-Credentials: true` |322| `SameSite=None` without `Secure` | Browser silently rejects the cookie | Always pair `SameSite=None` with `Secure` flag |323| Refresh token reuse without detection | Stolen refresh tokens grant indefinite access | Rotate refresh tokens, detect reuse (token families) |324| Using OAuth2 Implicit grant | Tokens exposed in URL fragment, no refresh tokens | Use Authorization Code + PKCE instead (Implicit is deprecated) |325| Password in URL or logs | URLs are logged by proxies, browsers, and servers | Always send credentials in request body or headers |326| Missing CSRF protection with cookies | Cookie-based auth is vulnerable to cross-site request forgery | Use SameSite cookies + CSRF tokens for state-changing ops |327| Long-lived access tokens (hours/days) | Large attack window if token is compromised | Keep access tokens to 5-15 minutes, use refresh tokens |328| Storing API keys in plaintext | Database breach exposes all keys | Hash stored keys (SHA-256 of key), store prefix for lookup |329| Not validating JWT `aud` claim | Token meant for Service A accepted by Service B | Always validate `aud` matches your service identifier |330| Session fixation | Attacker sets session ID before login, then hijacks it | Regenerate session ID after authentication |331| Hardcoded secrets in code | Secrets leak via source control | Use environment variables or secret managers (Vault, AWS SSM) |332| Trusting an identity-aware proxy's plain email header | Headers are attacker-settable on any unproxied path | Verify the proxy's signed JWT (sig + issuer + audience); close every path around the proxy |333| Auth-library middleware as the only session check | Framework middleware can be bypassed (Next.js CVE-2025-29927 class) | Re-check the session in the data-access layer / route handlers |334335## Reference Files336337| File | Contents | Lines |338|------|----------|-------|339| `references/jwt-sessions.md` | JWT structure, signing, sessions, cookies, CSRF, storage | ~650 |340| `references/oauth2-oidc.md` | OAuth2 flows, OIDC, provider integration, social login | ~700 |341| `references/authorization.md` | RBAC, ABAC, ReBAC, RLS, multi-tenant, audit logging | ~600 |342| `references/implementation.md` | Password hashing, MFA, rate limiting, API keys, reset flows | ~550 |343| `references/cloudflare-access.md` | Identity-aware proxies via Cloudflare Access: app/policy anatomy, token claims, JWT verification, closed-origin precondition, service auth, sessions/logout/SPA, local dev | ~330 |344| `references/better-auth.md` | Better Auth library: server/client setup, adapters, session model, social login, plugin catalog (passkey/2FA/org/SSO), Hono integration, migration | ~240 |345346## See Also347348- **security-ops** - Broader security patterns: OWASP, headers, input validation, encryption349- **api-design-ops** - API design including authentication endpoints, rate limiting350- **postgres-ops** - Row-level security (RLS) policies for database authorization351- **cloudflare-ops** - Workers runtime, wrangler config, secrets, deploy mechanics behind an Access-fronted origin