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).
1---2name: authentication3description: 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).4license: MIT5---67# Authentication89## Purpose10Establish 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).1112**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.1314## Procedure15161. **Choose the mechanism by use case**17 - **Session (server-side, cookie)** — classic web apps, easy revocation, default for first-party18 - **JWT (stateless)** — APIs, microservices, mobile; harder to revoke (keep access tokens short)19 - **OAuth 2.0 / OIDC** — delegated auth (login with Google), third-party access20 - **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 UX21 - Don't default to JWT for a simple web app — sessions are simpler and revocable22231b. **Require MFA / step-up for sensitive operations**24 - At least a second factor (TOTP, passkey, push) for sign-in to anything with money, PII at scale, or admin scope25 - **Step-up** (re-auth before a high-stakes action like withdrawal / password change) is more usable than "always-MFA-on-everything"26 - Recovery flow is part of the design — account-recovery is the typical bypass attackers use, not the front door27282. **Pin allowed algorithms server-side — never trust the token `alg` header**29 - Classic attack class: **algorithm confusion** (RS256→HS256, using the public key as an HMAC secret) and the **`none` algorithm** bypass30 - Explicitly configure the verifier to accept ONLY your chosen algorithm; reject everything else31 - (Encode the *vulnerability class*, not specific CVE numbers — those churn)32333. **Short access tokens + rotating refresh tokens**34 - Access token: short TTL (minutes) so a leaked token expires fast35 - Refresh token: rotate on every use; **detect reuse** (a used-twice refresh token = theft → revoke the whole token family)36374. **Store tokens safely (coordinate with frontend)**38 - Refresh/session tokens in HttpOnly + Secure + SameSite cookies — never localStorage (XSS-readable)39 - **Rotate the session id on every login (and privilege change)** — pinning a pre-auth session id post-login = session fixation40 - This pairs with the frontend-toolkit `security-audit` token-storage rule41425. **Validate token claims as untrusted input**43 - Verify `exp`, `iss`, `aud`, signature — all of them44 - Treat claims as untrusted until verified (see `data-validation`)45465b. **Defend the login endpoint itself**47 - Rate-limit by IP **and** by account (credential-stuffing attacks rotate IPs; per-account limits stop them)48 - Account lockout / exponential delay after N failures; expose only generic "invalid credentials" (no user-enumeration leak between "no such user" and "wrong password")49 - On OAuth flows: `state` (CSRF) + PKCE for public clients; verify the redirect URI is on the allowlist exactly (not a prefix)50516. **Validate (validation loop)**52 - Attempt an `alg: none` and an RS256→HS256 confusion token → verify both rejected53 - Use a refresh token twice → verify reuse detection revokes the family54 - If any forged/replayed token is accepted → verifier misconfigured; fix and re-test5556## Anti-patterns5758| ❌ Anti-pattern | ✅ Correct |59|---|---|60| Trusting the token's `alg` header | Pin allowed algorithms server-side |61| Long-lived access tokens, no rotation | Short access + rotating refresh w/ reuse detection |62| Tokens in localStorage | HttpOnly + Secure + SameSite cookies |63| JWT for a simple revocable web session | Server-side session (easy revocation) |64| Skipping `iss`/`aud`/`exp` verification | Verify all standard claims |65| Reusing the pre-login session id post-login | Rotate session id on login (session fixation defense) |66| No login rate-limit / generic 401 only by IP | Rate-limit by IP **and** per account; lockout after N failures |67| User-enumeration leak ("no such user" vs "wrong password") | Generic "invalid credentials" for both |6869## Severity tiers7071| Tier | Examples | Action SLA |72|---|---|---|73| **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 |74| **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 |75| **Minor** | Session timeout untuned; OAuth state param missing on a low-risk flow; user-enumeration leak in error messages | Schedule within 2 sprints |7677## Stop & Ask (AI must pause for user approval)7879- **Before rotating a signing key in production** — confirm a multi-key verifier window so in-flight tokens validate during cutover80- **Before changing the allowed `algorithms` list on the verifier** — confirm every live issuer is already on the new algorithm81- **Before mass-revoking refresh-token families** — this logs users out everywhere; coordinate the user-comms8283## Completion Criteria84- [ ] Mechanism choice justified by use case (passkeys considered for consumer products)85- [ ] MFA / step-up required on admin and money-moving actions; recovery flow explicitly designed86- [ ] Allowed algorithms pinned (forged-token rejection verified)87- [ ] Refresh rotation + reuse detection in place; session id rotates on login88- [ ] Login endpoint: rate-limited by IP **and** per account; generic error message (no user enumeration)89- [ ] Tokens stored in HttpOnly cookies (not localStorage)90- [ ] All standard claims verified9192## Output93- **Auth implementation**: chosen mechanism + token lifecycle94- **Security notes**: algorithm pinning + rotation policy documented95- **Commit format**: `feat(auth): add <mechanism> login` / `fix(auth): pin allowed JWT algorithms`9697## Implementation9899### TypeScript + Supabase Auth / NestJS (default)100- **Supabase Auth** (default): handles JWT issuance, refresh rotation, OAuth providers, RLS integration — prefer it over hand-rolling101- NestJS: `@nestjs/passport` + `passport-jwt`; configure `algorithms: ['RS256']` explicitly (pin)102- Refresh rotation + reuse detection: Supabase built-in, or implement token-family tracking103- **Lucia is deprecated** (sunset Jul 2025) — read it as a reference, don't adopt; use Supabase Auth / Auth.js104105### Other stacks106- **Python / FastAPI**: `fastapi-users`; PyJWT with explicit `algorithms=[...]`107- **Go**: `golang-jwt` with explicit alg pinning; avoid the historical alg-confusion footgun108- **Universal**: algorithm pinning + refresh rotation + reuse detection are protocol-level; the `none`-alg and RS256→HS256 classes apply to every JWT library109110## Related skills111- `authorization` — authn (who you are) precedes authz (what you can do)112- `backend-security-audit` — token handling is a top OWASP audit area113- `data-validation` — token claims are untrusted input — validate them114115## Reference116- **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).117- **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).