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

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

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

  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:

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).