diff --git a/docs/superpowers/specs/2026-05-05-p4-05-oidc-design.md b/docs/superpowers/specs/2026-05-05-p4-05-oidc-design.md index eb9f9b7..f0cac64 100644 --- a/docs/superpowers/specs/2026-05-05-p4-05-oidc-design.md +++ b/docs/superpowers/specs/2026-05-05-p4-05-oidc-design.md @@ -19,7 +19,7 @@ The Authorization Code flow (with PKCE) is implemented against the discovered we | Decision | Pick | |---|---| | User lifecycle | **B** — JIT-provision local rows on first OIDC login (`auth_source='oidc'`, `oidc_subject`) | -| Role mapping config | **A** — YAML/env, claim name `roles`, default = deny on no-match | +| Role mapping config | **A** — YAML/env, claim name configurable (default `groups`, matching Authelia / Keycloak / Authentik), default = deny on no-match | | Username source | `preferred_username`, fallback to `email` | | Username collision with existing local user | **Refuse** with clear remediation message | | Provider config | **Single provider** — `providers:` array can come later | @@ -51,8 +51,9 @@ oidc: issuer: https://auth.example.com # well-known config discovered from this client_id: restic-manager client_secret: ${RM_OIDC_CLIENT_SECRET} # or via _FILE - scopes: [openid, profile, email, roles] # 'roles' usually means a custom scope - role_claim: roles # default if absent + display_name: Authelia # button label "Sign in with "; default "SSO" + scopes: [openid, profile, email, groups] + role_claim: groups # default if absent (matches Authelia / Keycloak / Authentik) role_mapping: rm-admins: admin rm-operators: operator @@ -196,6 +197,8 @@ The IdP is the hard part to test cleanly. Two layers: - **End-session 404 / not advertised** — degrade gracefully; just drop the session and 303 to `/login`. Don't 500 the logout because the IdP doesn't implement RP-initiated logout. - **Username changes at the IdP** — silently keep the local username (matches our locked decision: subject is the stable key, username is display-only). Document. - **Role claim is sometimes a string, sometimes an array, sometimes a comma-separated string** depending on IdP — normalise into `[]string` before mapping. Authelia/Keycloak emit arrays; some custom setups emit strings; handle both. +- **Authelia `sub` is an opaque UUID, not the username** (Authelia 4.39+ default for new clients). Don't assume `sub` is human-readable; it's stable but display value is `preferred_username` or `email`. The locked design already keys lookups on `sub` and uses `preferred_username` for the display username, so this is just a correctness note. +- **`end_session_endpoint` may not be published** (Authelia doesn't advertise it for many configs). The locked logout flow already degrades to "drop session + redirect to /login" when the discovery doc lacks it; no extra config needed. - **Password-form bypass for OIDC users via /api/auth/login (JSON)** — same rejection rule applies, not just the HTML form. ## Acceptance