Files
HiveGrid-Wireframes/hivegrid-cloud/docs/operations/admin-sso.md
T

183 lines
9.4 KiB
Markdown

# 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.<base-domain>/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.<base-domain>/auth/admin/callback` |
| `AUTHENTIK_ADMIN_POST_LOGOUT_REDIRECT_URI` | `https://admin.<base-domain>/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.<domain>/admin/login`.
5. First login triggers JIT-provisioning — AdminUser row appears in DB
with `ssoProvider='authentik'`, `ssoSubjectId=<sub>`, `ssoGroups=[...]`,
`lastSsoLoginAt=<now>`.
## 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=<id>`.
- 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@<base-domain>
```
Redeploy the web service.
3. Visit `https://admin.<base-domain>/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).