Merge branch 'feat/v0.7-auth-iam' — 4 blocks + /auth-setup
This commit is contained in:
commit
0081bc8f46
6 changed files with 543 additions and 0 deletions
102
skills/auth-setup/SKILL.md
Normal file
102
skills/auth-setup/SKILL.md
Normal file
|
|
@ -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: <one-line app description, e.g. "B2C SaaS, Next.js + Postgres, needs Google + passkeys">
|
||||
---
|
||||
|
||||
# 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: <first 80 chars of intake>...
|
||||
Stack: <STACK> + <STORAGE>
|
||||
Flows: <FLOWS list> MFA: <MFA>
|
||||
Providers: <PROVIDERS with env var names>
|
||||
Session: <SESSION summary line>
|
||||
Authz: <AUTHZ model + engine if any>
|
||||
Threats: <N mitigations selected>
|
||||
Libraries: <pick per language, one line>
|
||||
Env vars: <count> new entries for secrets/<file>.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 `<repo>/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.
|
||||
115
skills/auth-setup/phase-1-intake.md
Normal file
115
skills/auth-setup/phase-1-intake.md
Normal file
|
|
@ -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).
|
||||
79
skills/auth-setup/phase-2-identity-provider.md
Normal file
79
skills/auth-setup/phase-2-identity-provider.md
Normal file
|
|
@ -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://<app>/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`.
|
||||
79
skills/auth-setup/phase-3-session-strategy.md
Normal file
79
skills/auth-setup/phase-3-session-strategy.md
Normal file
|
|
@ -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.
|
||||
84
skills/auth-setup/phase-4-authorization.md
Normal file
84
skills/auth-setup/phase-4-authorization.md
Normal file
|
|
@ -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:<relation>` — 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<Action, Resource>` 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).
|
||||
84
skills/auth-setup/phase-5-threats.md
Normal file
84
skills/auth-setup/phase-5-threats.md
Normal file
|
|
@ -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):
|
||||
- <threat name>: <one-line 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.
|
||||
Loading…
Reference in a new issue