13 KiB
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
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:
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
- Create the user in Authentik (manual or via SCIM/LDAP if integrated).
- Add the user to the
hivegrid-adminorhivegrid-staffgroup. - The provider-specific authentication flow enrolls or validates TOTP.
- User clicks "Sign in with SSO" on
https://admin.<domain>/login. - First login triggers JIT-provisioning — AdminUser row appears in DB
with
ssoProvider='authentik',ssoSubjectId=<sub>,ssoGroups=[...],lastSsoLoginAt=<now>.
Offboarding an admin
- Authentik — remove the user from
hivegrid-admin/hivegrid-staffor deactivate the user. - Next SSO login will be rejected (
admin.login.sso.failurewithreason=forbidden-group). - 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).
- Operator CLI (planned, Phase 10):
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=trueADMIN_FALLBACK_LOGIN_EMAILScontains 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
- Verify Authentik is genuinely down. Sentry banner +
/auth/admin/loginredirect failing both confirm. - In Coolify, set:
Redeploy the web service.
ADMIN_FALLBACK_LOGIN_ENABLED=true ADMIN_FALLBACK_LOGIN_EMAILS=bootstrap@<base-domain> - Visit
https://admin.<base-domain>/login— expand the disclosure ("Use emergency password login") and sign in with the bootstrap admin. - Resolve the Authentik outage (see
docs/operations/authentik-setup.md). - Immediately revert the env flag back to
ADMIN_FALLBACK_LOGIN_ENABLED=falseand redeploy. Verify the fallback toggle is no longer visible. - 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:
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_flowfrom 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/loginwill 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 (AdminUserSSO columns are nullable) so no data-loss. - Compromise (client_secret leak): rotate via Authentik admin UI →
update
AUTHENTIK_ADMIN_CLIENT_SECRETin Coolify vault → redeploy. Existing app sessions remain valid (they are independent of the Authentik secret).