From 48d4dd07337128b48b24dc69eb781e4299077a9b Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Tue, 21 Apr 2026 20:35:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(blocks):=204=20auth=20blocks=20=E2=80=94?= =?UTF-8?q?=20oauth2-oidc/passkeys/sessions/authorization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _blocks/auth-authorization.md | 27 +++++++++++++++++++++++++++ _blocks/auth-oauth2-oidc.md | 26 ++++++++++++++++++++++++++ _blocks/auth-passkeys.md | 27 +++++++++++++++++++++++++++ _blocks/auth-sessions.md | 29 +++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 _blocks/auth-authorization.md create mode 100644 _blocks/auth-oauth2-oidc.md create mode 100644 _blocks/auth-passkeys.md create mode 100644 _blocks/auth-sessions.md diff --git a/_blocks/auth-authorization.md b/_blocks/auth-authorization.md new file mode 100644 index 0000000..0b515f3 --- /dev/null +++ b/_blocks/auth-authorization.md @@ -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` 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. diff --git a/_blocks/auth-oauth2-oidc.md b/_blocks/auth-oauth2-oidc.md new file mode 100644 index 0000000..e89c017 --- /dev/null +++ b/_blocks/auth-oauth2-oidc.md @@ -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` 43–128 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. diff --git a/_blocks/auth-passkeys.md b/_blocks/auth-passkeys.md new file mode 100644 index 0000000..9b62688 --- /dev/null +++ b/_blocks/auth-passkeys.md @@ -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 2024–2026. + +## 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 2023–2024; SimpleWebAuthn 10.x stable. diff --git a/_blocks/auth-sessions.md b/_blocks/auth-sessions.md new file mode 100644 index 0000000..3d9d011 --- /dev/null +++ b/_blocks/auth-sessions.md @@ -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: 7–30 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.