# Pathrule Pattern: Auth (Sessions, JWT, OAuth) (1.0.0)
# ::pathrule:package:auth-sessions-jwt-oauth

### [RULE] Never store auth tokens in localStorage  (path: /src/auth)
<!-- scope: folder | priority: high | strict -->

Any token that authenticates a request must be set as an httpOnly cookie so JavaScript cannot read it and XSS cannot exfiltrate it.

- Set the session or refresh cookie with `httpOnly`, `secure`, `sameSite: 'lax'` (or `strict` for high-value actions), and the `__Host-` name prefix.
- Never write access tokens, refresh tokens, or session IDs to `localStorage`, `sessionStorage`, or non-httpOnly cookies.
- Keep access tokens short-lived (15 to 60 minutes) and refresh tokens long-lived (7 to 14 days) so a stolen access token expires quickly.
- If a short-lived access token must reach the browser for direct API calls, hold it in memory only — never in persistent storage.

---

### [RULE] Default to server sessions; use JWT only when statelessness is required  (path: /src/auth)
<!-- scope: folder | priority: high | strict -->

Choosing the wrong token model is an architectural mistake that is expensive to reverse. The safer default is always an opaque session.

- Default to opaque server sessions stored in Redis or Postgres, returned to the browser as an httpOnly cookie. They are instantly revocable, carry no payload to leak, and are the simplest model to reason about.
- Reach for JWTs only when you genuinely need stateless verification across services or edge runtimes. Accept that revocation requires a denylist or very short TTLs.
- A signed JWT cannot be invalidated before expiry. Keep access token TTL at 15 to 60 minutes and pair with a rotating refresh token to limit the blast radius of a leaked token.
- Never put secrets, passwords, PII, or sensitive authorization claims in a JWT payload; it is base64, not encrypted, and is readable by anyone who holds it.

---

### [RULE] Hash passwords with Argon2id, never fast hashes  (path: /src/auth)
<!-- scope: folder | priority: high | strict -->

Passwords must be hashed with a memory-hard algorithm so offline cracking remains expensive even with modern hardware.

- Default to Argon2id via the `argon2` package using OWASP 2026 minimums: 19 MiB memory, 2 iterations, parallelism 1. Tune upward if your hardware allows without exceeding p95 login latency.
- Use bcrypt at cost factor 12 or higher only when Argon2 is unavailable in the target runtime.
- Never use `md5`, `sha1`, `sha256`, or any unsalted or single-round hash for passwords.
- Compare passwords using the library's built-in `verify` so the work factor and salt are read from the stored hash. Never write your own comparison.

---

### [RULE] Protect cookie-based auth from CSRF  (path: /src/middleware)
<!-- scope: folder | priority: high | strict -->

The browser sends auth cookies automatically on every matching request, including cross-site ones that SameSite alone does not fully cover in all scenarios. Every state-changing endpoint needs an explicit CSRF defense.

- Set `sameSite: 'lax'` as the baseline; upgrade to `strict` on sensitive write flows.
- Add a double-submit cookie token (for example `csrf-csrf`) validated on every unsafe method (POST, PUT, PATCH, DELETE). Pure stateless JWT-in-Authorization-header APIs that never use cookies do not need this.
- Verify `Origin` or `Referer` on state-changing requests as defense in depth.
- Treat GET, HEAD, and OPTIONS as safe; never mutate state inside them.

---

### [MEMORY] OAuth/OIDC: PKCE, token rotation, and replay detection  (path: /src/auth)

Third-party login and delegated access must follow RFC 9700 (OAuth 2.0 Security BCP). Older tutorials recommend flows that are now deprecated or insecure.

- Always use the Authorization Code flow with PKCE, even for confidential clients. The implicit and resource-owner password grants are deprecated and absent from RFC 9700. Use `openid-client` or `oauth4webapi` instead of hand-rolling the flow.
- Validate the `state` parameter on callback to prevent CSRF. Before trusting any OIDC claim, verify the `id_token` signature, `iss`, `aud`, and `nonce`.
- Rotate refresh tokens on every use: issue a new refresh token and immediately invalidate the consumed one. If a consumed token is replayed, revoke the entire token family for that user.
- Make rotation atomic with a database transaction or a compare-and-swap lock so concurrent refresh requests cannot mint two valid tokens for one client session.
- Store OAuth tokens server-side under the same httpOnly cookie session, never in localStorage. The browser only needs the session cookie; the token exchange and storage live on the server.

---

### [SKILL] auth-sessions-jwt-oauth-review  (path: /)

---
name: auth-sessions-jwt-oauth-review
description: Use before merging any authentication change covering sessions, JWTs, OAuth/OIDC, password storage, cookies, CSRF, and token rotation. Run every item against the diff.
---

# Auth (Sessions, JWT, OAuth) review

- [ ] Tokens and session IDs are stored in httpOnly, secure, SameSite cookies with a `__Host-` prefix, never in localStorage or sessionStorage.
- [ ] Session vs JWT choice is justified: opaque server sessions by default, JWT only when stateless cross-service verification is required.
- [ ] Access tokens are short-lived (15 to 60 min); no secrets, passwords, or PII are in a JWT payload.
- [ ] Passwords are hashed with Argon2id at OWASP 2026 parameters (19 MiB, 2 iterations, parallelism 1), or bcrypt cost 12+ only as a documented runtime fallback.
- [ ] No fast or unsalted hash (md5, sha1, sha256) is used for passwords anywhere.
- [ ] Cookie-based endpoints enforce CSRF protection (SameSite + double-submit token) on all state-changing methods.
- [ ] OAuth uses Authorization Code + PKCE; implicit and password grants are absent.
- [ ] OIDC `id_token` signature, `iss`, `aud`, and `nonce` are verified before trusting any claim. `state` is validated against CSRF.
- [ ] Refresh tokens rotate on every use with replay detection that revokes the token family; rotation is atomic.
- [ ] Auth failures return generic messages and do not leak whether the user or password was wrong.
