# Admin-Panel SSO via Authentik Phase 09 Task #82. Status: **code-complete, awaits Authentik deploy**. This document covers operating the admin-panel SSO flow once Phase 09 #81 has shipped the Authentik instance. ## Overview ```mermaid sequenceDiagram participant Browser participant Web as Web (admin host) participant Authentik participant DB Browser->>Web: GET /auth/admin/login Web->>Web: createAdminSsoChallenge() (PKCE+state+nonce) Web-->>Browser: 302 -> Authentik authorize, Set-Cookie: admin_sso_challenge Browser->>Authentik: GET /application/o/authorize/... Authentik-->>Browser: Login + MFA, then 302 /auth/admin/callback?code=...&state=... Browser->>Web: GET /auth/admin/callback (with cookie) Web->>Web: consumeAdminSsoChallenge() -> verify state+nonce+sig Web->>Authentik: POST /token (PKCE verifier + client_secret_post) Authentik-->>Web: id_token + access_token Web->>Web: extractAdminSsoClaims(idToken) + email_verified check Web->>DB: jitProvisionAdmin() -> upsert User + AdminUser Web->>DB: createAuditEvent('admin.login.sso.success') Web-->>Browser: 302 /admin/dashboard + Set-Cookie session + id_token cookie Note over Browser,Web: Logout (RP-initiated) Browser->>Web: POST /auth/admin/logout Web->>Web: readIdTokenFromRequest() + destroyAppSession() Web-->>Browser: 302 Authentik end_session_endpoint?id_token_hint=... Browser->>Authentik: end_session Authentik-->>Browser: 302 post_logout_redirect_uri -> /admin/login?logged_out=1 ``` ## Required environment See `.env.example` for the canonical commented block. Coolify deployment: | Key | Source | | ---------------------------------------------- | ------------------------------------------------- | | `AUTHENTIK_ISSUER_URL` | `https://auth./application/o/toolctrl-admin-panel/` | | `AUTHENTIK_ADMIN_CLIENT_ID` | Blueprint client_id (default `toolctrl-admin-panel`) | | `AUTHENTIK_ADMIN_CLIENT_SECRET` | Authentik admin UI (Coolify vault) | | `AUTHENTIK_ADMIN_REDIRECT_URI` | `https://admin./auth/admin/callback` | | `AUTHENTIK_ADMIN_POST_LOGOUT_REDIRECT_URI` | `https://admin./admin/login?logged_out=1` | | `ADMIN_FALLBACK_LOGIN_ENABLED` | `false` (default), flip to `true` during recovery | | `ADMIN_FALLBACK_LOGIN_EMAILS` | Comma-separated bootstrap emails | ## Authentik group → role mapping The OIDC `groups` claim drives the AdminUser role. Highest priority wins. | Authentik group | Internal AdminUser role | Notes | | ---------------- | ----------------------- | -------------------------------------- | | `toolctrl-admin` | `super_admin` | Full admin permissions; if user is in both `toolctrl-admin` and `toolctrl-staff`, this wins. | | `toolctrl-staff` | `staff` | Limited admin permissions. | | _other_ | _login rejected (403)_ | `admin.login.sso.failure` audit event with `reason=forbidden-group`. | Granular permission checks remain in `apps/web/app/utils/permissions/`; the role is the coarse switch. ## Onboarding a new admin 1. Create the user in Authentik (manual or via SCIM/LDAP if integrated). 2. Add the user to the `toolctrl-admin` or `toolctrl-staff` group. 3. Enforce MFA enrollment in Authentik (provider-side). 4. User clicks "Sign in with SSO" on `https://admin./admin/login`. 5. First login triggers JIT-provisioning — AdminUser row appears in DB with `ssoProvider='authentik'`, `ssoSubjectId=`, `ssoGroups=[...]`, `lastSsoLoginAt=`. ## Offboarding an admin 1. **Authentik** — remove the user from `toolctrl-admin`/`toolctrl-staff` or deactivate the user. 2. Next SSO login will be rejected (`admin.login.sso.failure` with `reason=forbidden-group`). 3. **Force-Logout** existing session: until the app session cookie expires, the admin can still load admin pages. Force-revoke via: - Operator CLI (planned, Phase 10): `pnpm cli admin:force-logout --userId=`. - Or: clear the user's session row directly in DB (search by `userId`). - Or: rotate `SESSION_SECRET` (invalidates every session — last resort). ## Fallback (emergency) login The email/password fallback bypasses Authentik. It is locked down by two env-gates plus the existing rate-limit on `/login`: - `ADMIN_FALLBACK_LOGIN_ENABLED=true` - `ADMIN_FALLBACK_LOGIN_EMAILS` contains the requesting email (case-insensitive) Every fallback attempt — success or failure — writes an `admin.login.fallback.*` audit event with `severity=warn` in the metadata (also visible as a Sentry breadcrumb when the reporter is configured). ### Disaster-recovery procedure 1. Verify Authentik is genuinely down. Sentry banner + `/auth/admin/login` redirect failing both confirm. 2. In Coolify, set: ``` ADMIN_FALLBACK_LOGIN_ENABLED=true ADMIN_FALLBACK_LOGIN_EMAILS=bootstrap@ ``` Redeploy the web service. 3. Visit `https://admin./admin/login` — expand the disclosure ("Use emergency password login") and sign in with the bootstrap admin. 4. Resolve the Authentik outage (see `docs/operations/authentik-setup.md`). 5. **Immediately** revert the env flag back to `ADMIN_FALLBACK_LOGIN_ENABLED=false` and redeploy. Verify the fallback toggle is no longer visible. 6. Resolve the Sentry alert / Slack-notification (when wired). The drill MUST be exercised end-to-end at least once on staging before production rollout. ## App-MFA gate (Phase 01 #26 TOTP) `requireAdminMfaForAction(request, action, userId)` enforces a fresh TOTP step-up challenge for the closed taxonomy: ```ts const SENSITIVE_ADMIN_ACTIONS = [ "impersonation.start", "tenant.delete", "permission.grant", "audit.export", "data.export", ] as const; ``` The cache window matches the existing step-up state (10 minutes — see `MFA_STEP_UP_MAX_AGE_MS`). Successful challenges write `admin.mfa.app.challenge.success`; redirects to enrollment/challenge write `admin.mfa.app.challenge.failure`. Enrollment + challenge UI is the existing `/admin/security/mfa-*` route from Phase 01 #26 — no new TOTP implementation here. ## Audit-event reference | Action key | Severity | Emitted by | | ---------------------------------------- | -------- | ---------------------------------------------- | | `admin.login.sso.success` | info | `/auth/admin/callback` after JIT success | | `admin.login.sso.failure` | warn | Any `/auth/admin/*` early-return path | | `admin.login.fallback.success` | warn | `/login` action (admin host, fallback path) | | `admin.login.fallback.failure` | warn | `/login` action when gate rejects | | `admin.logout.sso` | info | `/auth/admin/logout` | | `admin.mfa.app.challenge.success` | info | `requireAdminMfaForAction` happy path | | `admin.mfa.app.challenge.failure` | warn | `requireAdminMfaForAction` redirect/deny path | All events carry `ip`, `userAgent`, `actorAdminUserId` (when resolvable) and an action-specific `meta` JSON blob. The audit wrapper redacts any suspicious key (authorization/cookie/password/secret/token) before persisting to `AuditEvent`. ## Troubleshooting | Symptom | Cause / Fix | | ------------------------------------------------------- | ------------------------------------------------------ | | `/auth/admin/login` redirects to `/admin/login?error=sso_unavailable` | env vars missing or Authentik unreachable. Check Sentry for `AdminSsoConfigError` (`discovery-failed`). | | `error=sso_state_invalid` after Authentik | Challenge cookie expired, signed under stale secret, or tampered. Have user retry; check `CRYPTO_SECRET` rotation. | | `error=sso_forbidden_group` | User not in `toolctrl-admin`/`toolctrl-staff`. Add them in Authentik then retry. | | `error=sso_email_not_verified` | Authentik user has not verified their email; enforce verification in Authentik provider settings. | | `error=sso_token_exchange_failed` | Client secret mismatch, clock-skew, or redirect-URI drift. Compare `.env` vs Authentik UI. | | JWKS-stale after key rotation | openid-client refreshes JWKS lazily; first failure forces a refetch. If repeated, restart the web container. | | Group claim empty in ID-token | Authentik provider must include `groups` scope mapping — see blueprint `03-application-toolctrl-admin.yaml`. | ## Rollback / kill-switch - **Disable SSO entry-point only**: unset the Authentik env vars and enable the fallback. `/auth/admin/login` will redirect with `?error=sso_unavailable`; the UI shows only the password form. - **Disable the whole flow**: revert the commits tagged `09/admin-oidc-login: ...`. The Prisma migration is additive (`AdminUser` SSO columns are nullable) so no data-loss. - **Compromise (client_secret leak)**: rotate via Authentik admin UI → update `AUTHENTIK_ADMIN_CLIENT_SECRET` in Coolify vault → redeploy. Existing app sessions remain valid (they are independent of the Authentik secret).