Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/docs/operations/admin-sso.md
T

225 lines
13 KiB
Markdown

# Admin-Panel SSO via Authentik
Phase 09 Task #82. Status: **operational**.
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: __Host-hivegrid_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 -> /login?logged_out=1
```
## Admin cookie scope
Production and every HTTPS deployment use host-only
`__Host-hivegrid_admin_sso_challenge` and
`__Host-hivegrid_admin_sso_id_token` cookies. Both are `Secure`, `HttpOnly`,
`SameSite=Lax`, `Path=/`, and never carry a `Domain` attribute. `Path=/` is
required by the browser-enforced `__Host-` contract; because the admin UI has
its own hostname, that dedicated host is the narrowest safe compatible scope.
The application does not accept the former unprefixed names on HTTPS, so a
parent-domain or sibling-host cookie cannot shadow an admin SSO cookie.
Local HTTP development uses the distinct unprefixed names
`hivegrid_admin_sso_challenge` and `hivegrid_admin_sso_id_token`, without
`Secure`. They are compatibility fallbacks only and are never read by an
HTTPS deployment.
## Required environment
See `.env.example` for the canonical commented block. Coolify deployment:
| Key | Source |
| ------------------------------------------ | ---------------------------------------------------------------- |
| `AUTHENTIK_ISSUER_URL` | `https://auth.<base-domain>/application/o/hivegrid-admin-panel/` |
| `AUTHENTIK_ADMIN_CLIENT_ID` | Blueprint client_id (default `hivegrid-admin-panel`) |
| `AUTHENTIK_ADMIN_CLIENT_SECRET` | Authentik admin UI (Coolify vault) |
| `AUTHENTIK_ADMIN_JIT_DATABASE_URL` | PgBouncer URL for the dedicated `hivegrid_authentik_jit` login |
| `AUTHENTIK_ADMIN_REDIRECT_URI` | `https://admin.<base-domain>/auth/admin/callback` |
| `AUTHENTIK_ADMIN_POST_LOGOUT_REDIRECT_URI` | `https://admin.<base-domain>/login?logged_out=1` |
| `ADMIN_FALLBACK_LOGIN_ENABLED` | `false` (default), flip to `true` during recovery |
| `ADMIN_FALLBACK_LOGIN_EMAILS` | Comma-separated bootstrap emails |
The regular web `DATABASE_URL` remains authenticated as `hivegrid_app` and
cannot execute the Admin-JIT function. Only the verified
`/auth/admin/callback` path opens the separate
`AUTHENTIK_ADMIN_JIT_DATABASE_URL`; its `hivegrid_authentik_jit` role owns no
table or sequence privileges and receives `EXECUTE` only for
`hivegrid_provision_authentik_admin(...)`. PostgreSQL also checks
`session_user`, so switching roles or forwarding freely constructed claims
through the application connection fails closed.
Before deploying the RLS cutover into an existing environment, provision the
new login externally with the other managed roles. The non-superuser migrator
never creates or alters roles:
```sh
HIVEGRID_RLS_PROVISION_DATABASE_URL=<SUPERUSER_DATABASE_URL> \
HIVEGRID_RLS_MIGRATOR_PASSWORD=<MIGRATOR_PASSWORD> \
HIVEGRID_RLS_APP_PASSWORD=<APP_PASSWORD> \
HIVEGRID_RLS_WORKER_PASSWORD=<WORKER_PASSWORD> \
HIVEGRID_RLS_AUTHENTIK_JIT_PASSWORD=<AUTHENTIK_JIT_PASSWORD> \
pnpm run db:roles:provision
```
Then add the matching, PgBouncer-fronted
`AUTHENTIK_ADMIN_JIT_DATABASE_URL` only to the web resource. Run role
provisioning before `prisma migrate deploy`; the migration deliberately stops
with a prerequisite error when any managed role is absent or unsafe.
## Authentik group → role mapping
The OIDC `groups` claim drives the AdminUser role. Highest priority wins.
| Authentik group | Internal AdminUser role | Notes |
| ---------------- | ----------------------- | -------------------------------------------------------------------------------------------- |
| `hivegrid-admin` | `super_admin` | Full admin permissions; if user is in both `hivegrid-admin` and `hivegrid-staff`, this wins. |
| `hivegrid-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 `hivegrid-admin` or `hivegrid-staff` group.
3. The provider-specific authentication flow enrolls or validates TOTP.
4. User clicks "Sign in with SSO" on `https://admin.<domain>/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 `hivegrid-admin`/`hivegrid-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>/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 `/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 `hivegrid-admin`/`hivegrid-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_mfa_required` | The ID token has no `mfa` AMR. Assign the dedicated HiveGrid authentication flow to the provider, verify that its TOTP validation stage uses `configure` for users without a device, and keep the authorization request at `max_age=1` so a prior Authentik session cannot bypass validation. A first enrollment creates the device; the following fresh authentication proves possession. Do not enable the password fallback. |
| `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-hivegrid-admin.yaml`. |
## Rollback / kill-switch
- **MFA-flow rollback**: restore the provider's previous `authentication_flow`
from the pre-change Authentik snapshot. Keep the application-side MFA claim
check fail-closed; do not use fallback login as a substitute for MFA.
- **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).