When to Use
- Use when: designing login / registration flow for a new service
- Use when: JWT tokens are leaking, expired unexpectedly, or over-broad
- Use when: designing a permissions model (who can do what)
- Use when: adding OAuth / "Login with Google/GitHub" to an app
- Do NOT use for: cryptographic implementation details — use a library
JWT Lifecycle
Structure
header.payload.signature
Payload contains claims — never put sensitive data (passwords, PII) in JWT payload. It is base64-encoded, not encrypted — anyone can decode it.
Access token + refresh token pattern
Access token: short-lived (15 min – 1 hour)
stored in memory (JS variable) — NOT localStorage
sent as: Authorization: Bearer <token>
Refresh token: long-lived (7–30 days)
stored in httpOnly, Secure, SameSite=Strict cookie
used only to get a new access token
rotated on every use (rotation invalidates old token)
Token storage rules
| Storage | XSS risk | CSRF risk | Use for |
|---|---|---|---|
| Memory (JS var) | Low | None | Access token (lost on refresh) |
| httpOnly cookie | None (JS can't read) | Yes (mitigate with SameSite) | Refresh token |
| localStorage | High (XSS steals it) | None | Never for tokens |
| sessionStorage | High | None | Never for tokens |
Revocation
JWTs are stateless — can't be revoked until expiry without a denylist.
For immediate revocation (logout, compromise): maintain a denylist in Redis keyed by jti (JWT ID). Check on every request.
OAuth 2.0 Flows
Authorization Code + PKCE (recommended for all clients)
1. App generates code_verifier (random 43–128 chars) + code_challenge = SHA256(verifier)
2. Redirect user → /authorize?response_type=code&code_challenge=...&code_challenge_method=S256
3. User authenticates → provider redirects back with ?code=AUTH_CODE
4. App exchanges: POST /token { code, code_verifier, client_id, redirect_uri }
5. Provider returns: { access_token, refresh_token, id_token }
PKCE prevents authorization code interception attacks — required for SPAs and mobile.
Which flow to use
| Client type | Flow |
|---|---|
| Web app (server-side) | Authorization Code + PKCE |
| SPA | Authorization Code + PKCE (no client_secret) |
| Mobile | Authorization Code + PKCE |
| Server-to-server | Client Credentials |
| Never use | Implicit flow (deprecated), Password Grant (except legacy migration) |
OIDC (OpenID Connect)
OIDC = OAuth 2.0 + identity layer. Add openid scope to get an id_token (JWT with user identity).
Use id_token to confirm who the user is. Use access_token to call APIs.
Authorization Models
RBAC (Role-Based Access Control)
User → has Roles → Roles have Permissions → Permissions allow Actions on Resources
Example:
Role: admin → permissions: [users:read, users:write, billing:read, billing:write]
Role: editor → permissions: [content:read, content:write]
Role: viewer → permissions: [content:read]
Good for: clear org hierarchies, most SaaS apps. Weakness: role explosion when exceptions accumulate.
ABAC (Attribute-Based Access Control)
Decision = f(subject_attrs, resource_attrs, environment_attrs, action)
Example policy:
ALLOW if user.department == resource.department AND user.clearance >= resource.classification
Good for: complex, context-dependent rules (time-of-day, IP range, data classification). Weakness: harder to audit than RBAC — "why can Alice do X?" requires policy tracing.
Hybrid (RBAC + resource ownership)
Most apps need: RBAC for broad roles + ownership check for row-level access:
canEdit(user, document):
return user.role == 'admin' OR document.owner_id == user.id
Common Auth Mistakes
| Mistake | Fix |
|---|---|
| Long-lived access tokens (days) | Max 1 hour; use refresh token rotation |
| JWT secret hardcoded in code | Env var; rotate on compromise |
| No token expiry check on backend | Always validate exp claim server-side |
| Permissions checked only on frontend | Always enforce on backend — UI is advisory only |
user_id in URL without ownership check |
Verify resource.owner_id == authenticated_user_id |
| Refresh token in localStorage | httpOnly cookie only |
Anti-Fake-Pass Rules
Before claiming auth is done, you MUST show:
- Access token: short-lived (≤ 1 hour), stored in memory not localStorage
- Refresh token: httpOnly cookie, rotated on use
- Backend validates token signature and
expon every protected request - Authorization: every endpoint checks both authentication AND permission
- RBAC/ABAC: permissions enforced server-side, not just hidden in UI
Reference: gates/anti-fake-pass-gate.md