From 505e727dcf2f9157fc3a9074a743b5a308242e84 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Tue, 21 Apr 2026 20:47:21 +0800 Subject: [PATCH] feat(skills): /auth-setup 5-phase pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hub-and-spoke skill that converts "I need auth for app X" into a reviewable plan across 5 phases: intake (flows/stack/storage/MFA), identity-provider pick + env scaffold, session strategy + cookies, authorization model + permission matrix, and threats + mitigations. - 8 AskUserQuestion calls total (≥6 hub-and-spoke contract; 4 in Phase 1 + 1 each in Phases 2–5). - Reads all four _blocks/auth-*.md; never writes production code or secret values. - RULE 0.8 (Secrets SSoT): emits env VARIABLE NAMES only; storage path is secrets/auth.env per domain-has-secrets.md. - Constructor Pattern: 6 files, largest 115 LOC (<200 limit). - Fail-closed default + NO DOWNGRADE on unsafe combinations (passkey-only without recovery → return recovery-path options, not "not supported"). Evidence grade [E2] — pipeline mirrors OWASP ASVS v4.0.3 chapters 2–4. --- skills/auth-setup/SKILL.md | 102 ++++++++++++++++ skills/auth-setup/phase-1-intake.md | 115 ++++++++++++++++++ .../auth-setup/phase-2-identity-provider.md | 79 ++++++++++++ skills/auth-setup/phase-3-session-strategy.md | 79 ++++++++++++ skills/auth-setup/phase-4-authorization.md | 84 +++++++++++++ skills/auth-setup/phase-5-threats.md | 84 +++++++++++++ 6 files changed, 543 insertions(+) create mode 100644 skills/auth-setup/SKILL.md create mode 100644 skills/auth-setup/phase-1-intake.md create mode 100644 skills/auth-setup/phase-2-identity-provider.md create mode 100644 skills/auth-setup/phase-3-session-strategy.md create mode 100644 skills/auth-setup/phase-4-authorization.md create mode 100644 skills/auth-setup/phase-5-threats.md diff --git a/skills/auth-setup/SKILL.md b/skills/auth-setup/SKILL.md new file mode 100644 index 0000000..4c0a45a --- /dev/null +++ b/skills/auth-setup/SKILL.md @@ -0,0 +1,102 @@ +--- +name: auth-setup +description: Hub-and-spoke pipeline that produces a production-grade auth/IAM plan for a new or existing app — user flows, identity providers, session strategy, authorization model, and threat mitigations — via pure-click decisions across five phases. Emits a scaffolded env-var list, library picks, and a per-threat checklist; never writes secrets. +argument-hint: +--- + +# Auth-Setup — Identity, Session & Authorization Pipeline (index) + +You are converting "I need auth for app X" into a concrete, reviewable plan: +which identity methods to ship, which providers to register, which session +strategy to pick, which authorization model to enforce, and which threats to +mitigate up front. Every decision is a click; the only typed input is the +one-line app description in Phase 1. + +This skill does NOT write production code. It emits a plan, the env-var +scaffold, the library picks, and a per-threat checklist. Code scaffolding +is a separate task owned by `new-agent` or the project's own +code-implementer. + +The skill reads the four companion blocks heavily — every phase references +at least one of them: + +- `_blocks/auth-oauth2-oidc.md` — OAuth2 / OIDC flows, PKCE, providers. +- `_blocks/auth-passkeys.md` — WebAuthn registration + assertion, RP ID. +- `_blocks/auth-sessions.md` — server sessions vs JWT tradeoff, cookies. +- `_blocks/auth-authorization.md` — RBAC / ABAC / ReBAC, policy engines. + +--- + +## Pipeline overview (5 phases, ≥6 AskUserQuestion calls) + +| Phase | File | Purpose | AskUserQuestion | +|---|---|---|---| +| 1 | [phase-1-intake.md](phase-1-intake.md) | App flows, stack, storage, MFA | 4× | +| 2 | [phase-2-identity-provider.md](phase-2-identity-provider.md) | Pick + configure IdPs; env scaffold | 1× | +| 3 | [phase-3-session-strategy.md](phase-3-session-strategy.md) | Server-session vs JWT; cookie flags | 1× | +| 4 | [phase-4-authorization.md](phase-4-authorization.md) | RBAC / ABAC / ReBAC; permission matrix | 1× | +| 5 | [phase-5-threats.md](phase-5-threats.md) | CSRF / XSS / timing / enumeration | 1× | + +Minimum AskUserQuestion count across a full session: **8** (4 in Phase 1 + +1 each in Phases 2–5). Exceeds the ≥6 hub-and-spoke contract. + +--- + +## Variables the pipeline produces + +| Name | Set in | Meaning | +|---|---|---| +| `FLOWS` | Phase 1 | subset of {email+password, magic-link, OAuth, passkey, SSO} | +| `STACK` | Phase 1 | Next / Remix / SvelteKit / Astro / Rust axum / FastAPI / other | +| `STORAGE` | Phase 1 | Postgres / SQLite / MySQL / Supabase / managed-auth | +| `MFA` | Phase 1 | none / TOTP / passkey / WebAuthn-as-2FA | +| `PROVIDERS` | Phase 2 | list of OAuth/OIDC providers with env-var names | +| `SESSION` | Phase 3 | server-session OR JWT + cookie config | +| `AUTHZ` | Phase 4 | RBAC / ABAC / ReBAC + policy engine (or none) | +| `THREATS` | Phase 5 | mitigation checklist, per threat class | + +--- + +## Final report (emit after Phase 5) + +``` +=== AUTH-SETUP REPORT === +App: ... +Stack: + +Flows: MFA: +Providers: +Session: +Authz: +Threats: +Libraries: +Env vars: new entries for secrets/.env +Next: run `compose-solution` or hand off to project code-implementer +``` + +--- + +## Rules (apply throughout) + +- **Pure-click contract.** Only the Phase 1 intake paragraph is typed. + Every other decision is `AskUserQuestion`. +- **RULE 0.8 Secrets SSoT.** The skill emits env VARIABLE NAMES only + (`GOOGLE_CLIENT_ID`, `APPLE_TEAM_ID`, ...). It NEVER echoes a token + value, never writes to `secrets/*.env`, never suggests hard-coding. + Storage path is `/secrets/auth.env` per `domain-has-secrets.md`. +- **NO DOWNGRADE.** If the chosen combination is unsafe (e.g. + passkey-only without a recovery flow) the skill returns 2–3 constructive + alternatives, never "not supported". +- **Fail-closed default.** Every authz / session / threat decision + defaults to the safer option when the user is unsure. +- **Surgical scope.** Reads the four auth blocks; writes nothing outside + its own phase files. Production scaffolding is delegated. + +--- + +## References + +- `_blocks/auth-oauth2-oidc.md`, `_blocks/auth-passkeys.md`, + `_blocks/auth-sessions.md`, `_blocks/auth-authorization.md`. +- `_blocks/domain-has-secrets.md` — storage path + loading convention. +- `_blocks/rule-pre-dev-gate.md` — analogue check before inventing. +- Evidence grade [E2] — pipeline mirrors OWASP ASVS v4.0.3 chapters 2–4. diff --git a/skills/auth-setup/phase-1-intake.md b/skills/auth-setup/phase-1-intake.md new file mode 100644 index 0000000..b064942 --- /dev/null +++ b/skills/auth-setup/phase-1-intake.md @@ -0,0 +1,115 @@ +# Phase 1 — Intake (flows, stack, storage, MFA) + +One free-text paragraph, then four click batches. This is the only phase +that accepts typed input. + +## 1a — Ask for the app description + +Emit a regular message (NOT AskUserQuestion): + +> Describe the app in one paragraph: what is it, who signs in, and any +> constraint I should know (regulated industry, existing user table, +> multi-tenant, mobile-first, etc.). Reply in one message. + +Store the reply verbatim as `INTAKE`. + +## 1b — User-flow click (AskUserQuestion, multi-select) + +```json +{ + "questions": [ + { + "question": "Which login flows should the app support?", + "header": "Flows", + "multiSelect": true, + "options": [ + {"label": "Email + password", "description": "Classic — requires password hash (argon2id), breach-check (HIBP), reset email flow"}, + {"label": "Magic link", "description": "Email-only, one-time link; removes password surface, depends on email deliverability"}, + {"label": "OAuth / social", "description": "Google / GitHub / Apple / Microsoft — see _blocks/auth-oauth2-oidc.md"}, + {"label": "Passkey (WebAuthn)", "description": "Phishing-resistant, passwordless — see _blocks/auth-passkeys.md"}, + {"label": "Enterprise SSO", "description": "SAML / OIDC to Okta / Entra ID / Keycloak — B2B multi-tenant"} + ] + } + ] +} +``` + +Store the multi-selection as `FLOWS`. Empty selection → re-ask. + +## 1c — Stack click (AskUserQuestion, single-select) + +```json +{ + "questions": [ + { + "question": "Primary framework / runtime?", + "header": "Stack", + "multiSelect": false, + "options": [ + {"label": "Next.js (App Router)", "description": "Server Components + Server Actions; Better-Auth or Auth.js"}, + {"label": "Remix / React Router", "description": "Loader/action model; remix-auth + Better-Auth"}, + {"label": "SvelteKit", "description": "Hooks + form actions; Lucia replacement = Better-Auth or custom"}, + {"label": "Astro", "description": "Islands + middleware; Better-Auth or external auth"}, + {"label": "Rust (axum / actix)", "description": "axum-login + tower-sessions + webauthn-rs + openidconnect-rs"}, + {"label": "Python (FastAPI)", "description": "authlib + starlette SessionMiddleware + py_webauthn"}, + {"label": "Other / specify", "description": "Add a free-text note; skill will pick libraries in Phase 2"} + ] + } + ] +} +``` + +Store as `STACK`. + +## 1d — Storage click (AskUserQuestion, single-select) + +```json +{ + "questions": [ + { + "question": "Where will user + session rows live?", + "header": "Storage", + "multiSelect": false, + "options": [ + {"label": "Postgres", "description": "Recommended default — ACID, row-level security, good for multi-tenant"}, + {"label": "SQLite", "description": "Single-node, edge-friendly; fine up to ~100k users"}, + {"label": "MySQL / MariaDB", "description": "Existing stack compatibility"}, + {"label": "Managed (Supabase / Clerk / Auth0 / WorkOS)", "description": "Off-load auth entirely — skill emits integration plan, not self-hosted tables"}, + {"label": "Redis (sessions only)", "description": "Paired with a primary DB for users"} + ] + } + ] +} +``` + +Store as `STORAGE`. + +## 1e — MFA click (AskUserQuestion, single-select) + +```json +{ + "questions": [ + { + "question": "Multi-factor requirement?", + "header": "MFA", + "multiSelect": false, + "options": [ + {"label": "None", "description": "Consumer app, low risk surface"}, + {"label": "TOTP (authenticator app)", "description": "RFC 6238; pair with 10 one-time recovery codes"}, + {"label": "Passkey as 2FA", "description": "WebAuthn with user-verification=required, after password or magic link"}, + {"label": "Required for admins only", "description": "RBAC rule: role=admin → MFA gate before privileged actions"} + ] + } + ] +} +``` + +Store as `MFA`. + +## Verify-criterion + +- `INTAKE` non-empty. +- `FLOWS` has ≥1 entry. +- `STACK`, `STORAGE`, `MFA` each exactly one label. +- If `FLOWS = {Passkey}` ONLY and `MFA = None` → warn "passkey-only requires a + recovery path" and return to 1b (NO DOWNGRADE: present recovery options). diff --git a/skills/auth-setup/phase-2-identity-provider.md b/skills/auth-setup/phase-2-identity-provider.md new file mode 100644 index 0000000..a562932 --- /dev/null +++ b/skills/auth-setup/phase-2-identity-provider.md @@ -0,0 +1,79 @@ +# Phase 2 — Identity provider selection + env-var scaffold + +Only runs if `FLOWS` contains `OAuth / social` or `Enterprise SSO`. If +neither is selected, skip to Phase 3 (passkey-only or magic-link apps +don't need external IdPs). + +## 2a — Provider click (AskUserQuestion, multi-select) + +Reference: `_blocks/auth-oauth2-oidc.md`. + +```json +{ + "questions": [ + { + "question": "Which identity providers to register?", + "header": "Providers", + "multiSelect": true, + "options": [ + {"label": "Google", "description": "OIDC; discovery at accounts.google.com/.well-known/openid-configuration"}, + {"label": "GitHub", "description": "OAuth2 only (no OIDC discovery); hard-code endpoints"}, + {"label": "Apple", "description": "OIDC; name/email returned ONLY on first consent — persist immediately"}, + {"label": "Microsoft", "description": "OIDC multi-tenant via login.microsoftonline.com/common/v2.0"}, + {"label": "Enterprise OIDC (Okta / Auth0 / Keycloak / Entra)", "description": "B2B SSO; per-tenant discovery URL"}, + {"label": "SAML 2.0 (legacy enterprise)", "description": "Use a library like samlify (TS) or python3-saml; NOT OAuth"} + ] + } + ] +} +``` + +Store as `PROVIDERS`. Empty → skip Phase 2. + +## 2b — Emit env-var scaffold (no AskUserQuestion) + +For EACH provider in `PROVIDERS`, emit the env-var rows the user must add +to `secrets/auth.env`. NEVER emit values — names only. Example for Google: + +```bash +# secrets/auth.env — add these, then `chmod 600` the file +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI=https:///auth/google/callback +GOOGLE_OIDC_DISCOVERY=https://accounts.google.com/.well-known/openid-configuration +``` + +Per-provider scaffold rules: + +- **Google / Microsoft / Apple / Enterprise OIDC:** `*_CLIENT_ID`, + `*_CLIENT_SECRET` (confidential) OR `*_CLIENT_ID` + PKCE only (public + SPA/mobile), `*_REDIRECT_URI`, `*_OIDC_DISCOVERY`. +- **Apple** adds `APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_PRIVATE_KEY_PATH` + (path to the `.p8` file — stored inside `secrets/`, never inline). +- **GitHub:** `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, + `GITHUB_REDIRECT_URI`. No discovery URL. +- **SAML:** `SAML_IDP_METADATA_URL`, `SAML_SP_ENTITY_ID`, + `SAML_SP_ACS_URL`, `SAML_SP_PRIVATE_KEY_PATH`, + `SAML_SP_CERT_PATH`. + +Emit the snippet as a fenced code block in chat. Remind the user once: +"File `secrets/auth.env` must be `chmod 600` and listed in `.gitignore` +BEFORE the first write. See `_blocks/domain-has-secrets.md`." + +## 2c — Library pick (emitted inline, no AskUserQuestion) + +Driven by `STACK` from Phase 1: + +- **Next.js / Remix / SvelteKit / Astro:** Better-Auth (preferred 2025–2026) + OR NextAuth/Auth.js (Next-only, mature). Both support PKCE by default. +- **Rust (axum):** `openidconnect-rs` (OIDC) or `oauth2-rs` (OAuth2 bare). +- **Python (FastAPI):** `authlib` (covers both OAuth2 and OIDC). +- **Managed (Clerk / Supabase / WorkOS):** provider SDK only; this phase + just records the SDK name. + +## Verify-criterion + +- Every provider in `PROVIDERS` has its env-var scaffold printed. +- No literal token value appears anywhere in the emitted text + (RULE 0.8 / `auth-oauth2-oidc.md` enforcement). +- Library pick is one line, matches `STACK`. diff --git a/skills/auth-setup/phase-3-session-strategy.md b/skills/auth-setup/phase-3-session-strategy.md new file mode 100644 index 0000000..f2583c3 --- /dev/null +++ b/skills/auth-setup/phase-3-session-strategy.md @@ -0,0 +1,79 @@ +# Phase 3 — Session strategy + cookie configuration + +Decides how the authenticated principal is carried across requests. Reads +`_blocks/auth-sessions.md` heavily. Default bias: server-side opaque +sessions (revocable, simple) unless the user needs horizontal stateless +scale. + +## 3a — Strategy click (AskUserQuestion, single-select) + +```json +{ + "questions": [ + { + "question": "Session carrier?", + "header": "Session", + "multiSelect": false, + "options": [ + {"label": "Server-side session + cookie (DEFAULT)", + "description": "Opaque 256-bit id in HttpOnly Secure cookie; row in DB/Redis. Instant revoke. Recommended for >95% of apps."}, + {"label": "JWT access + opaque refresh", + "description": "ES256 access ≤15 min in HttpOnly cookie; refresh rotated server-side. Use ONLY when you have stateless edge workers that can't reach the session DB."}, + {"label": "JWT access + refresh in native secure storage", + "description": "Mobile app; refresh in Keychain / Keystore. Same rotation rules; cookie flags N/A."}, + {"label": "Managed (Clerk / Supabase / Auth0)", + "description": "Provider owns the session primitive; skill records the SDK integration points only."} + ] + } + ] +} +``` + +Store as `SESSION`. + +## 3b — Emit cookie-flag checklist (inline, no AskUserQuestion) + +Apply ONLY when `SESSION` involves a browser cookie. For every cookie the +app sets (session, CSRF, anti-re-use nonce): + +``` +[ ] HttpOnly — blocks JS read; XSS-resistant +[ ] Secure — HTTPS only; reject on cleartext +[ ] SameSite=Lax — default; use Strict for cross-site-hostile apps +[ ] __Host- prefix — no Domain, Path=/, Secure — session cookie only +[ ] Max-Age tuned — 7–30 d sliding (consumer) / 24 h hard (regulated) +[ ] Rotation on login, — new session_id issued, old row deleted or revoked_at set + logout-all, passkey/password change, privilege elevation +[ ] Logout deletes BOTH — server row AND cookie (Max-Age=0, same flags) +``` + +## 3c — Emit JWT-specific checklist (inline) — only if JWT chosen + +``` +[ ] Algorithm = ES256 — asymmetric; NOT HS256 for cross-service +[ ] access_token ≤15 min — minimises revocation-gap window +[ ] refresh_token OPAQUE — stored server-side, rotated on every use +[ ] refresh-reuse detection — family revocation on stolen refresh +[ ] JWKS rotation + kid — key rollover without service restart +[ ] Claims validated — iss, aud, exp, nbf, iat, sub, nonce (if OIDC) +[ ] NEVER in localStorage — HttpOnly cookie (web) / secure storage (native) +[ ] Logout policy stated — "revoke refresh only; access valid until exp" + and accepted by the product (or escalate to server-session strategy) +``` + +## 3d — CSRF strategy (inline, driven by SESSION) + +- Cookie session + same-origin forms → `SameSite=Lax` is enough; plus a + CSRF token (cookie+header double-submit) for cross-origin POSTs. +- Cookie session + third-party embed (iframes, extensions) → `SameSite=None; + Secure` + mandatory CSRF token, reject missing/mismatched. +- Bearer-token API (no cookie) → no CSRF (no ambient credential); enforce + `Origin` header check as defence-in-depth. + +## Verify-criterion + +- `SESSION` set to exactly one strategy. +- At least one of the three checklists (3b / 3c / 3d) applies and was + emitted. +- If JWT chosen, 3c is printed in full AND the logout gap was explicitly + acknowledged in the report. diff --git a/skills/auth-setup/phase-4-authorization.md b/skills/auth-setup/phase-4-authorization.md new file mode 100644 index 0000000..070f8be --- /dev/null +++ b/skills/auth-setup/phase-4-authorization.md @@ -0,0 +1,84 @@ +# Phase 4 — Authorization model + permission matrix + +Decides who-can-do-what after authentication. Reads +`_blocks/auth-authorization.md`. Fail-closed by default. + +## 4a — Model click (AskUserQuestion, single-select) + +```json +{ + "questions": [ + { + "question": "Authorization model?", + "header": "Authz", + "multiSelect": false, + "options": [ + {"label": "RBAC (roles → permissions)", + "description": "Static roles (admin / editor / viewer). Simplest, enough for most apps with <5 roles."}, + {"label": "RBAC + ownership", + "description": "Roles + per-row owner_id check. The sweet spot for multi-tenant SaaS."}, + {"label": "ABAC (policy engine)", + "description": "Attributes + context (time, IP, resource tier). Use Cerbos or OPA. Adopt when rule count >~20."}, + {"label": "ReBAC (Google Zanzibar)", + "description": "Graph-shaped sharing (folders/teams/orgs). SpiceDB or OpenFGA. Pick only if your domain is inherently graph-shaped."}, + {"label": "None — single-user app", + "description": "No authz layer beyond authentication. Record explicitly."} + ] + } + ] +} +``` + +Store as `AUTHZ`. + +## 4b — Emit permission matrix skeleton (inline) + +For `RBAC` / `RBAC + ownership`, emit a table stub the user must fill in +before coding. Example for a notes app: + +```markdown +| Role | notes:read | notes:write | notes:delete | users:manage | +|--------|:---------:|:-----------:|:------------:|:------------:| +| admin | all | all | all | yes | +| editor | owned | owned | owned | no | +| viewer | owned | no | no | no | +``` + +- Columns = `resource:action` tokens — these become the `Permission` enum + variants in code. +- Cells = `all` / `owned` / `no` / `shared:` — NEVER free-text. +- Save as `docs/permissions.md` in the target repo; treat it as code + (tested, reviewed, versioned). + +## 4c — Enforcement-point rule (inline) + +- Middleware, not handlers. Every authenticated request runs an authz + decision BEFORE the handler sees it. Handler receives a typed + `AuthorizedRequest` or the request 403s earlier in the + stack. +- Ownership checks enforced in BOTH the middleware AND the data layer + (`WHERE tenant_id=$1 AND owner_id=$2`). Double layer defeats IDOR. +- Fail-closed: unknown action, missing role, policy-engine error → 403. + Log every denial with subject + action + resource + reason. +- Audit log append-only row on every privilege change, role assignment, + and denied action. Required for SOC2 / HIPAA / ISO 27001. + +## 4d — Policy-engine pick (inline, driven by AUTHZ) + +- `RBAC` / `RBAC + ownership` → in-code match on `Permission` enum; no + engine. +- `ABAC` → Cerbos (YAML rules, stateless decision service) OR OPA/Rego + (general-purpose, steeper curve). Keep `.cerbos.yaml` / `.rego` files + in the repo, unit-tested like code. +- `ReBAC` → SpiceDB (Zanzibar reference) OR OpenFGA (Auth0-backed). + Define the schema, seed relationships, use the client SDK. +- `None` → emit a single line "authz skipped — no multi-user model". + +## Verify-criterion + +- `AUTHZ` is exactly one choice. +- If RBAC chosen, permission-matrix skeleton with ≥1 row + ≥1 column is + printed. +- Enforcement-point rule (4c) is emitted verbatim — non-negotiable. +- If a policy engine is implied by `AUTHZ`, the pick is named (Cerbos / + OPA / SpiceDB / OpenFGA). diff --git a/skills/auth-setup/phase-5-threats.md b/skills/auth-setup/phase-5-threats.md new file mode 100644 index 0000000..79e0352 --- /dev/null +++ b/skills/auth-setup/phase-5-threats.md @@ -0,0 +1,84 @@ +# Phase 5 — Threats & mitigations + +Close the pipeline with a per-threat checklist. The user picks which +mitigations to commit to; the skill emits them into the final report so +they get tracked as acceptance criteria. + +## 5a — Threat-class click (AskUserQuestion, multi-select, pre-checked) + +Pre-select every item by default — opting OUT requires a click, opting IN +is the cheap path (fail-closed bias). + +```json +{ + "questions": [ + { + "question": "Confirm the threat mitigations to enforce (pre-checked; deselect only if you have a compensating control)?", + "header": "Threats", + "multiSelect": true, + "options": [ + {"label": "CSRF — SameSite + token", + "description": "SameSite=Lax default; double-submit token for cross-origin POSTs; reject on mismatch"}, + {"label": "XSS — HttpOnly + CSP", + "description": "HttpOnly on every auth cookie; strict CSP (no inline script); sanitise every rendered string; NEVER put session or JWT in localStorage"}, + {"label": "Session fixation — rotate on login", + "description": "New session_id issued at every privilege change (login, logout-all, password/passkey change, MFA step-up)"}, + {"label": "Account enumeration — uniform responses", + "description": "Same timing + wording for 'user not found' and 'wrong password'; signup and reset flows respond identically regardless of address existence"}, + {"label": "Timing attacks — constant-time compare", + "description": "Use subtle.timingSafeEqual / crypto.constant_time_compare on password hash, token, session_id lookups"}, + {"label": "Password policy — argon2id + HIBP", + "description": "argon2id hashing (memory≥64MB, t≥3); reject passwords found in HaveIBeenPwned k-anonymity API; min length 12, no max"}, + {"label": "Brute-force — rate limit + lockout", + "description": "Per-account exponential backoff; per-IP sliding window; CAPTCHA after N failures; unlock via email or time"}, + {"label": "Email-link security", + "description": "One-time tokens (random 32B, SHA-256 in DB); ≤15 min TTL; single-use; bound to email address at issue time"}, + {"label": "OAuth state + nonce", + "description": "state (CSRF) + nonce (replay) on every authorize request; reject on mismatch; see _blocks/auth-oauth2-oidc.md"}, + {"label": "Passkey recovery path", + "description": "Backup codes OR email magic-link OR OAuth fallback; user opts out only after explicit warning"}, + {"label": "Logging without leakage", + "description": "Never log raw password, TOTP secret, session_id, or access_token; log userID + action + result only"}, + {"label": "Dependency hygiene", + "description": "Auth library at latest patched version; CVE scan in CI; pin via lock file"} + ] + } + ] +} +``` + +Store the confirmed subset as `THREATS`. Any item the user deselects must +have a one-line justification recorded in the final report. + +## 5b — Emit threat-by-threat implementation hints (inline) + +For each item in `THREATS`, print ONE implementation line. Examples: + +- `CSRF` → "middleware double-submit: cookie `__Host-csrf` + `X-CSRF-Token` + header; `subtle.timingSafeEqual` on compare." +- `Timing attacks` → "Rust `subtle::ConstantTimeEq`; Node + `crypto.timingSafeEqual`; Python `hmac.compare_digest`." +- `Passkey recovery` → "register 10 single-use codes at passkey creation; + store argon2id hashes; mark consumed on use." + +Keep each line short — full guidance lives in the upstream blocks, not in +this phase. + +## 5c — Final report assembly + +After Phase 5 completes, emit the final report template from SKILL.md with +all variables filled. Add at the bottom: + +``` +Deselected threats (with justification): +- : +(or "none" if THREATS covers every default) +``` + +## Verify-criterion + +- `THREATS` has ≥8 of the 12 defaults selected; each deselection carries a + justification line. +- Every selected threat has an implementation hint printed (5b). +- Final report (5c) emits the full `=== AUTH-SETUP REPORT ===` block from + SKILL.md.