feat(blocks): 4 auth blocks — oauth2-oidc/passkeys/sessions/authorization

This commit is contained in:
Parfii-bot 2026-04-21 20:35:12 +08:00
parent ae8dd3fd37
commit 48d4dd0733
4 changed files with 109 additions and 0 deletions

View file

@ -0,0 +1,27 @@
# AUTH — Authorization (RBAC / ABAC / ReBAC)
Who is allowed to do what, AFTER authentication (`auth-sessions.md`) has identified the principal. Decides on every request; fail-closed.
## When to include
- App has more than one user role OR owner-vs-member resource semantics.
- App exposes admin endpoints, multi-tenant data, or per-resource sharing.
- Regulated domain (health / finance / legal) where permission decisions must be logged and auditable.
## What it declares
- **RBAC (Role-Based)** — static roles (`admin`, `editor`, `viewer`) mapped to permission sets (`posts:write`, `posts:read`, `billing:read`). Simple, O(1) check, enough for most small apps. Roles live in DB; assignment is an admin action, not a code change.
- **ABAC (Attribute-Based)** — decision = f(subject attrs, resource attrs, action, context). Example: "user can edit doc IF `doc.owner_id == user.id` OR `user.role == admin AND doc.tenant_id == user.tenant_id`". Use when RBAC explodes into per-resource special cases.
- **ReBAC (Relationship-Based, Google Zanzibar style)** — graph of `(subject, relation, object)` tuples; check = "does path `user:A` → ... → `doc:X#editor` exist?". Use for hierarchical sharing (folders, orgs, teams). Implementations: SpiceDB, OpenFGA.
- **Permission matrix — always DECLARED, never implicit:** a table `roles × resource_types × actions` in the repo (`docs/permissions.md` or a DB seed). Every new endpoint picks a cell from the matrix. No ad-hoc `if user.is_admin` scattered through handlers.
- **Enforcement point: middleware, not handlers.** Decision computed once per request against a typed `Permission` enum. Handler receives `AuthorizedRequest<Action>` or 403s before it runs. Prevents "forgot the check on the new endpoint" — the dominant authz bug.
- **Fail-closed:** missing role, unknown action, or policy engine error → DENY. Log the denial with subject + action + resource. Never default-allow on error.
- **Policy engines — use when authz logic grows > ~20 rules:** Cerbos (YAML rules, decision-as-a-service, stateless), OPA / Rego (general-purpose, steeper curve), Oso Cloud, SpiceDB (ReBAC). Keep policy files in the repo; treat them as code (tested, reviewed, versioned).
- **Ownership checks scope every query:** `SELECT ... WHERE tenant_id = $1 AND owner_id = $2` — enforced in the data layer, not just the middleware. Double layer defeats IDOR (Insecure Direct Object Reference).
- **Admin + audit:** every permission change, role assignment, and deny-event written to an append-only audit log (`tenant_id`, `actor_id`, `action`, `target`, `timestamp`, `result`). Required for SOC2 / ISO 27001 / HIPAA.
## References
- NIST SP 800-162 (ABAC), Google Zanzibar paper (2019), Cerbos docs, OPA/Rego docs [E1].
- `auth-sessions.md` — source of the authenticated principal; this block decides what that principal can do.
- Evidence grade [E2] — RBAC/ABAC widely deployed; ReBAC via Zanzibar-clones production since ~2022.

View file

@ -0,0 +1,26 @@
# AUTH — OAuth2 + OIDC (Authorization Code + PKCE)
Identity delegation to external providers (Google / GitHub / Apple / Microsoft / any OIDC-compliant IdP). For first-party login see `auth-passkeys.md` / `auth-sessions.md`; for post-login permissions see `auth-authorization.md`.
## When to include
- App supports "Sign in with Google / GitHub / Apple / Microsoft" or federates to an enterprise OIDC IdP (Okta, Auth0, Keycloak, Entra ID).
- App needs a short-lived API access token for the user (Gmail, Calendar, GitHub API).
- Regulated context where the IdP — not the app — is the system of record for identity.
## What it declares
- **Flow: Authorization Code + PKCE for EVERY client** (public SPA, mobile, confidential server). PKCE is mandatory in OAuth 2.1 and removes the implicit flow entirely.
- **PKCE params:** `code_verifier` 43128 chars random, `code_challenge = BASE64URL(SHA256(verifier))`, `code_challenge_method=S256`. Never `plain`.
- **State + nonce:** `state` (CSRF, 32+ bytes random, bound to session) on every auth request; `nonce` (replay, in ID token claim) for OIDC. Reject response if either mismatches.
- **Redirect URIs:** exact-match, pre-registered at the IdP. No wildcards. `localhost` and custom schemes OK for native; HTTPS required for web.
- **Providers: Google** (`accounts.google.com/.well-known/openid-configuration`), **GitHub** (OAuth2 only, no OIDC discovery — hard-code `https://github.com/login/oauth/authorize`, `token`, `https://api.github.com/user`), **Apple** (OIDC, but only returns user name/email on FIRST consent — persist on first login or lose it), **Microsoft** (`login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration`).
- **Token handling:** `access_token` short-lived (≤1 h), kept server-side only. `refresh_token` rotated on every use (RFC 6749 §6 + OAuth 2.1), stored encrypted at rest, NEVER sent to the browser. `id_token` validated (JWKS signature + `iss` + `aud` + `exp` + `nonce`) and discarded — do NOT re-use as a session token.
- **Secrets:** `CLIENT_ID` + `CLIENT_SECRET` per provider in `secrets/*.env`; referenced by env var name only. Public clients (SPA/mobile) use PKCE WITHOUT a secret.
- **Libraries:** prefer Better-Auth (TS), NextAuth/Auth.js (Next.js), authlib (Python), openidconnect-rs or oauth2-rs (Rust). Avoid rolling your own — every major CVE in this space is custom code.
## References
- RFC 6749 (OAuth 2.0), RFC 7636 (PKCE), RFC 9700 (OAuth 2.0 Security BCP, 2024), OAuth 2.1 draft, OpenID Connect Core 1.0 [E1 — standards-track RFCs].
- `auth-sessions.md` for what to do AFTER the IdP handshake returns.
- Evidence grade [E2] — implementation widely deployed, spec stable since 2024.

27
_blocks/auth-passkeys.md Normal file
View file

@ -0,0 +1,27 @@
# AUTH — Passkeys (WebAuthn / FIDO2)
Phishing-resistant, passwordless authentication via public-key credentials bound to the Relying Party. For federated login see `auth-oauth2-oidc.md`; for session issuance after passkey assertion see `auth-sessions.md`.
## When to include
- Greenfield auth: passkeys as PRIMARY login (password-optional or password-less).
- Existing password login: passkeys as stronger step-up or second factor that also replaces the password.
- Any consumer product — Apple, Google, Microsoft all ship platform authenticators (Touch ID / Face ID / Windows Hello) and sync passkeys across devices via iCloud Keychain / Google Password Manager / Microsoft Authenticator as of 20242026.
## What it declares
- **Two ceremonies:**
- **Registration** — server sends `PublicKeyCredentialCreationOptions` (random `challenge`, `rp.id`, `rp.name`, `user.id` opaque, `pubKeyCredParams` prefer ES256=-7 and RS256=-257, `authenticatorSelection`, `attestation: "none"` unless regulated). Client returns `attestationObject` + `clientDataJSON`. Server verifies and stores `credentialID`, `publicKey`, `signCount`, `transports`, `backupEligible`, `backupState`.
- **Assertion (login)** — server sends `PublicKeyCredentialRequestOptions` (fresh random `challenge`, `rpId`, `allowCredentials` list or empty for discoverable). Client returns `signature` + `authenticatorData` + `clientDataJSON`. Server verifies signature with stored `publicKey`, checks `signCount` strictly > stored, origin, `rpId` hash.
- **RP ID** = eTLD+1 or a subdomain of it (`example.com` covers `app.example.com`; a passkey for `app.example.com` does NOT work on `example.com`). Pick RP ID carefully at launch — changing it invalidates every existing credential.
- **Resident / discoverable credentials** (`residentKey: "required"` + `userVerification: "required"`) enable username-less login ("Sign in" button with no email field). Requires passkey-capable authenticator.
- **Platform vs cross-platform:** `authenticatorAttachment: "platform"` = Touch ID / Face ID / Windows Hello (synced, convenient). `"cross-platform"` = roaming security keys (YubiKey, Titan). Leave unset to accept both.
- **Challenge**: 16+ random bytes per ceremony, single-use, time-boxed (≤5 min), bound to server session, rejected on replay.
- **Libraries:** SimpleWebAuthn (TS — reference implementation, covers both server + browser), webauthn-rs (Rust, `Webauthn` builder + `passkey` feature), fido2-rs (low-level), py_webauthn (Python). NEVER roll CBOR / COSE parsing by hand.
- **Recovery path REQUIRED** before enabling passkey-only — lose device, lose account. Ship at least one of: email magic-link fallback, passkey backup codes, OAuth federation as recovery. User opts out of recovery only after explicit warning.
## References
- W3C WebAuthn Level 3 (2024-ready), FIDO2 CTAP 2.1, passkeys.dev [E1 — W3C/FIDO specs].
- `auth-sessions.md` for cookie issuance after `verifyAuthenticationResponse` succeeds.
- Evidence grade [E2] — Apple/Google/Microsoft production since 20232024; SimpleWebAuthn 10.x stable.

29
_blocks/auth-sessions.md Normal file
View file

@ -0,0 +1,29 @@
# AUTH — Sessions & Cookies (+JWT tradeoff)
What happens AFTER identity is proven (password / OAuth / passkey / magic-link). Issues a session, enforces it on every request, and kills it on logout. Upstream of `auth-authorization.md`.
## When to include
- Any web or mobile app that needs an authenticated request state beyond a single round-trip.
- Any app that exposes logout, session revocation, or step-up auth.
- API-only backend (mobile/SPA): choose cookie-based session OR short-lived JWT — decision recorded per project.
## What it declares
- **Default: server-side opaque sessions** stored in Postgres / Redis / SQLite, keyed by a 256-bit random `session_id`. Row columns: `id`, `user_id`, `created_at`, `last_seen_at`, `expires_at`, `ip`, `user_agent`, `revoked_at`. Session data NEVER encoded in the cookie itself.
- **Cookie flags — all mandatory:** `HttpOnly` (blocks JS read → XSS-resistant), `Secure` (HTTPS only), `SameSite=Lax` for top-level nav auth / `Strict` for cross-site-hostile apps, `Path=/`, `__Host-` prefix for session cookie (forbids `Domain`, requires `Secure` + `Path=/`). Max-Age tuned to app: 730 days sliding, 24 h hard for regulated.
- **Session rotation:** issue a NEW `session_id` on login, logout-everywhere, password/passkey change, privilege elevation. Old row deleted or `revoked_at` set. Rotation defeats session fixation.
- **Logout:** delete the server row AND clear the cookie (`Max-Age=0`, same flags). Logout-everywhere = delete all rows for `user_id`. Client-only logout (cookie clear, server row kept) is a bug, not a feature.
- **CSRF:** `SameSite=Lax` covers most flows. For cross-origin POSTs keep a double-submit CSRF token (cookie + header/form field, server compares). API-only backend with Bearer token → no CSRF (no ambient credential).
- **JWT alternative — use ONLY when stateless horizontal scale matters more than revocation:**
- `access_token` ≤15 min, signed ES256 (NOT HS256 with shared secret across services), `iat`/`exp`/`aud`/`iss`/`sub` all validated, `kid` header + JWKS rotation.
- `refresh_token` opaque (NOT a JWT), stored server-side, rotated on every use (detect reuse → revoke family).
- Logout revokes refresh token ONLY; access token is trusted until `exp`. If you need instant revoke → use server sessions instead.
- Never store JWT in `localStorage` — use `HttpOnly` cookie or native secure storage. `localStorage` + XSS = total account takeover.
- **Libraries:** axum-login + tower-sessions (Rust), express-session / Better-Auth (Node), iron-session (edge), starlette SessionMiddleware + authlib (Python), SvelteKit `event.cookies`. JWT: jose (TS), jsonwebtoken (Rust), PyJWT.
## References
- OWASP Session Management Cheat Sheet, RFC 6265bis (cookies), RFC 7519 (JWT), RFC 8725 (JWT BCP) [E1].
- `auth-oauth2-oidc.md` / `auth-passkeys.md` — upstream identity proof; `auth-authorization.md` — downstream permission check.
- Evidence grade [E2] — session-cookie pattern stable since 2000s; JWT revocation gap is a well-known tradeoff.