| name | authentication |
| description | Choose and implement auth correctly — JWT vs session vs OAuth decision, pin allowed algorithms server-side, rotate refresh tokens with reuse detection, avoid the classic JWT pitfalls. Use when adding login, integrating OAuth, or when token handling looks risky. Not for access control / permissions (use authorization) or a broader OWASP audit (use backend-security-audit). |
| license | MIT |
Authentication
Purpose
Establish who the caller is, securely. Pick the right mechanism for the use case and avoid the well-known token pitfalls (algorithm confusion, non-rotating refresh tokens, client-trusted claims).
Universal — the JWT-vs-session-vs-OAuth decision, algorithm pinning, and refresh-rotation-with-reuse-detection are auth principles independent of language; the library differs.
Procedure
- Choose the mechanism by use case
- Session (server-side, cookie) — classic web apps, easy revocation, default for first-party
- JWT (stateless) — APIs, microservices, mobile; harder to revoke (keep access tokens short)
- OAuth 2.0 / OIDC — delegated auth (login with Google), third-party access
- Passkeys / WebAuthn — passwordless, phishing-resistant. Treat as the primary path for new consumer products; passwords as a fallback. The provider (Supabase Auth / Auth.js / similar) handles the protocol — your job is the UX
- Don't default to JWT for a simple web app — sessions are simpler and revocable
1b. Require MFA / step-up for sensitive operations
- At least a second factor (TOTP, passkey, push) for sign-in to anything with money, PII at scale, or admin scope
- Step-up (re-auth before a high-stakes action like withdrawal / password change) is more usable than "always-MFA-on-everything"
- Recovery flow is part of the design — account-recovery is the typical bypass attackers use, not the front door
-
Pin allowed algorithms server-side — never trust the token alg header
- Classic attack class: algorithm confusion (RS256→HS256, using the public key as an HMAC secret) and the
none algorithm bypass
- Explicitly configure the verifier to accept ONLY your chosen algorithm; reject everything else
- (Encode the vulnerability class, not specific CVE numbers — those churn)
-
Short access tokens + rotating refresh tokens
- Access token: short TTL (minutes) so a leaked token expires fast
- Refresh token: rotate on every use; detect reuse (a used-twice refresh token = theft → revoke the whole token family)
-
Store tokens safely (coordinate with frontend)
- Refresh/session tokens in HttpOnly + Secure + SameSite cookies — never localStorage (XSS-readable)
- Rotate the session id on every login (and privilege change) — pinning a pre-auth session id post-login = session fixation
- This pairs with the frontend-toolkit
security-audit token-storage rule
-
Validate token claims as untrusted input
- Verify
exp, iss, aud, signature — all of them
- Treat claims as untrusted until verified (see
data-validation)
5b. Defend the login endpoint itself
- Rate-limit by IP and by account (credential-stuffing attacks rotate IPs; per-account limits stop them)
- Account lockout / exponential delay after N failures; expose only generic "invalid credentials" (no user-enumeration leak between "no such user" and "wrong password")
- On OAuth flows:
state (CSRF) + PKCE for public clients; verify the redirect URI is on the allowlist exactly (not a prefix)
- Validate (validation loop)
- Attempt an
alg: none and an RS256→HS256 confusion token → verify both rejected
- Use a refresh token twice → verify reuse detection revokes the family
- If any forged/replayed token is accepted → verifier misconfigured; fix and re-test
Anti-patterns
| ❌ Anti-pattern | ✅ Correct |
|---|
Trusting the token's alg header | Pin allowed algorithms server-side |
| Long-lived access tokens, no rotation | Short access + rotating refresh w/ reuse detection |
| Tokens in localStorage | HttpOnly + Secure + SameSite cookies |
| JWT for a simple revocable web session | Server-side session (easy revocation) |
Skipping iss/aud/exp verification | Verify all standard claims |
| Reusing the pre-login session id post-login | Rotate session id on login (session fixation defense) |
| No login rate-limit / generic 401 only by IP | Rate-limit by IP and per account; lockout after N failures |
| User-enumeration leak ("no such user" vs "wrong password") | Generic "invalid credentials" for both |
Severity tiers
| Tier | Examples | Action SLA |
|---|
| Critical | alg: none or RS256→HS256 accepted; refresh token never rotates; tokens in localStorage; no MFA on admin / money-moving actions | Block release; fix immediately |
| Major | Long access-token TTL with no revocation; missing aud/iss checks; no login rate-limit (credential stuffing open); session id not rotated on login (fixation) | Fix this sprint |
| Minor | Session timeout untuned; OAuth state param missing on a low-risk flow; user-enumeration leak in error messages | Schedule within 2 sprints |
Stop & Ask (AI must pause for user approval)
- Before rotating a signing key in production — confirm a multi-key verifier window so in-flight tokens validate during cutover
- Before changing the allowed
algorithms list on the verifier — confirm every live issuer is already on the new algorithm
- Before mass-revoking refresh-token families — this logs users out everywhere; coordinate the user-comms
Completion Criteria
Output
- Auth implementation: chosen mechanism + token lifecycle
- Security notes: algorithm pinning + rotation policy documented
- Commit format:
feat(auth): add <mechanism> login / fix(auth): pin allowed JWT algorithms
Implementation
TypeScript + Supabase Auth / NestJS (default)
- Supabase Auth (default): handles JWT issuance, refresh rotation, OAuth providers, RLS integration — prefer it over hand-rolling
- NestJS:
@nestjs/passport + passport-jwt; configure algorithms: ['RS256'] explicitly (pin)
- Refresh rotation + reuse detection: Supabase built-in, or implement token-family tracking
- Lucia is deprecated (sunset Jul 2025) — read it as a reference, don't adopt; use Supabase Auth / Auth.js
Other stacks
- Python / FastAPI:
fastapi-users; PyJWT with explicit algorithms=[...]
- Go:
golang-jwt with explicit alg pinning; avoid the historical alg-confusion footgun
- Universal: algorithm pinning + refresh rotation + reuse detection are protocol-level; the
none-alg and RS256→HS256 classes apply to every JWT library
Related skills
authorization — authn (who you are) precedes authz (what you can do)
backend-security-audit — token handling is a top OWASP audit area
data-validation — token claims are untrusted input — validate them
Reference
- Key insight encoded: Always pin allowed algorithms server-side (never trust the token header
alg); rotate refresh tokens with reuse detection that revokes the whole family on replay. Modern auth is more than tokens: passkeys for consumer products, MFA / step-up on sensitive actions, login-endpoint rate-limit by IP and per account (credential-stuffing rotates IPs), and session-id rotation on login (fixation).
- Caveats: Lucia is deprecated — recommend Supabase Auth / Auth.js. Encode the vulnerability class (none-alg, RS256→HS256) not specific CVE IDs (search-returned CVE numbers were unreliable).