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

9.4 KiB

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

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:

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