feat(auth): OIDC SSO for admin users — phase 1 (#798)
Authorization-code + PKCE against a single configurable IdP via
openid-client v5, with JIT provisioning. Verified end-to-end against a
real Keycloak 26 (realm + confidential client + verified-email user):
settings → discovery test → login button → Keycloak → dashboard.
Backend:
- migration 162: admin_users.auth_provider ('local' default) +
external_subject, composite unique index
- oidcService: settings-driven config (client secret AES-256-GCM at
rest, mfaService pattern, OIDC_ENCRYPTION_KEY fallback JWT_SECRET),
cached discovery, sub-based identity binding — email linking of
existing admins only with email_verified=true; JIT behind
oidc_autoprovision with configurable default role and an unusable
random password hash
- GET /api/auth/admin/sso/login + /callback: state/nonce/PKCE verifier
cross the redirect in a 10-min signed httpOnly SameSite=Lax cookie;
the callback reuses the local login's session establishment
(completeAdminLogin split into establishAdminSession + JSON wrapper)
so SSO sessions are identical downstream; every failure lands on
/admin/login?sso_error=<key> as a translated toast
- dedicated /admin/settings/sso GET/PUT/test endpoints (secret
write-only, redacted to a set-flag; registered ABOVE the generic
/:type matcher which would shadow them); oidc_client_secret added to
the reserved keys stripped from generic settings upserts
- public settings expose only oidc_enabled + oidc_button_label for the
login page
Frontend:
- Settings → Single Sign-On (OIDC) tab: issuer/client/secret, scopes,
autoprovision + default role, button label, enable toggle, redirect
URI copy box, server-side discovery test
- login page: SSO button (custom label) when enabled; sso_error query
param surfaced as translated toasts; EN+DE i18n
Tests: 11 integration cases against an in-process mock IdP (real
discovery/JWKS/PKCE/ID-token validation) — JIT on/off, sub-vs-email
binding, unverified-email rejection, deactivated admin, missing/forged
state cookie, nonce tamper, secret encryption round-trip, disabled 404.
MFA is delegated to the IdP on the SSO path; local login stays
available as break-glass. Role-claim mapping and logout-to-IdP follow
in phase 2/3.
This commit is contained in:
@@ -2090,6 +2090,32 @@
|
||||
"enableFailed": "Zwei-Faktor-Authentifizierung konnte nicht aktiviert werden. Prüfe den Code und versuche es erneut.",
|
||||
"disableFailed": "Zwei-Faktor-Authentifizierung konnte nicht deaktiviert werden. Prüfe den Code und versuche es erneut.",
|
||||
"regenerateFailed": "Wiederherstellungscodes konnten nicht neu erzeugt werden. Prüfe den Code und versuche es erneut."
|
||||
},
|
||||
"sso": {
|
||||
"title": "Single Sign-On (OIDC)",
|
||||
"intro": "Admins melden sich über Ihren Identity Provider an (Keycloak, Authentik, Pocket ID oder jeder OIDC-konforme IdP). Die lokale Anmeldung mit E-Mail/Passwort bleibt als Fallback verfügbar.",
|
||||
"redirectUri": "Redirect-URI (beim IdP-Client registrieren)",
|
||||
"issuerUrl": "Issuer-URL",
|
||||
"issuerHint": "Die Basis-URL, die /.well-known/openid-configuration ausliefert.",
|
||||
"clientId": "Client-ID",
|
||||
"clientSecret": "Client-Secret",
|
||||
"secretSetPlaceholder": "•••••• (gespeichert — zum Ersetzen tippen)",
|
||||
"secretUnsetPlaceholder": "Client-Secret einfügen",
|
||||
"secretHint": "Verschlüsselt gespeichert; wird nie wieder angezeigt. Leer lassen, um das aktuelle zu behalten.",
|
||||
"scopes": "Scopes",
|
||||
"autoprovision": "Unbekannte Benutzer automatisch anlegen",
|
||||
"autoprovisionHint": "Erstellt beim ersten SSO-Login ein Admin-Konto. Aus: Nur bestehende/verknüpfte Admins können sich anmelden.",
|
||||
"defaultRole": "Rolle für neue Benutzer",
|
||||
"buttonLabel": "Beschriftung des Login-Buttons (optional)",
|
||||
"buttonLabelPlaceholder": "Mit SSO anmelden",
|
||||
"enabled": "SSO-Login aktivieren",
|
||||
"enabledHint": "Zeigt den SSO-Button auf der Admin-Anmeldeseite. Erfordert Issuer, Client-ID und Secret.",
|
||||
"test": "Verbindung testen",
|
||||
"testHint": "Der Test führt OIDC-Discovery gegen die gespeicherte Konfiguration aus — zuerst speichern.",
|
||||
"testOk": "Discovery erfolgreich — Issuer ist erreichbar: {{issuer}}",
|
||||
"testFailed": "Discovery fehlgeschlagen",
|
||||
"saved": "SSO-Einstellungen gespeichert",
|
||||
"saveError": "SSO-Einstellungen konnten nicht gespeichert werden"
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -3706,6 +3732,16 @@
|
||||
"sessionExpired": "Deine Bestätigungssitzung ist abgelaufen. Bitte melde dich erneut an.",
|
||||
"locked": "Konto wegen zu vieler Versuche vorübergehend gesperrt. Versuche es später erneut.",
|
||||
"lockedRetry": "Konto vorübergehend gesperrt. Versuche es in {{seconds}} Sekunden erneut."
|
||||
},
|
||||
"ssoDivider": "oder",
|
||||
"ssoSignIn": "Mit SSO anmelden",
|
||||
"ssoErrors": {
|
||||
"config": "SSO ist falsch konfiguriert — prüfen Sie die SSO-Einstellungen oder melden Sie sich mit E-Mail und Passwort an.",
|
||||
"state": "Die SSO-Anmeldung ist abgelaufen oder wurde manipuliert. Bitte erneut versuchen.",
|
||||
"idp": "Der Identity Provider hat die Anmeldung abgelehnt. Bitte erneut versuchen oder E-Mail und Passwort verwenden.",
|
||||
"inactive": "Ihr Admin-Konto ist deaktiviert.",
|
||||
"not_provisioned": "Kein Admin-Konto passt zu Ihrer SSO-Identität. Bitten Sie einen Administrator um eine Einladung.",
|
||||
"no_email": "Ihr Identity Provider hat keine E-Mail-Adresse geliefert — es kann kein Konto erstellt werden."
|
||||
}
|
||||
},
|
||||
"cssTemplates": {
|
||||
|
||||
@@ -1637,6 +1637,32 @@
|
||||
"enableFailed": "Could not enable two-factor authentication. Check the code and try again.",
|
||||
"disableFailed": "Could not disable two-factor authentication. Check the code and try again.",
|
||||
"regenerateFailed": "Could not regenerate recovery codes. Check the code and try again."
|
||||
},
|
||||
"sso": {
|
||||
"title": "Single Sign-On (OIDC)",
|
||||
"intro": "Let admins sign in through your identity provider (Keycloak, Authentik, Pocket ID, or any OIDC-compliant IdP). Local email/password login stays available as a fallback.",
|
||||
"redirectUri": "Redirect URI (register this on your IdP client)",
|
||||
"issuerUrl": "Issuer URL",
|
||||
"issuerHint": "The base URL that serves /.well-known/openid-configuration.",
|
||||
"clientId": "Client ID",
|
||||
"clientSecret": "Client Secret",
|
||||
"secretSetPlaceholder": "•••••• (saved — type to replace)",
|
||||
"secretUnsetPlaceholder": "Paste the client secret",
|
||||
"secretHint": "Stored encrypted; never shown again. Leave blank to keep the current one.",
|
||||
"scopes": "Scopes",
|
||||
"autoprovision": "Auto-provision unknown users",
|
||||
"autoprovisionHint": "Create an admin account on first SSO login. Off: only existing/linked admins can sign in.",
|
||||
"defaultRole": "Role for new users",
|
||||
"buttonLabel": "Login button label (optional)",
|
||||
"buttonLabelPlaceholder": "Sign in with SSO",
|
||||
"enabled": "Enable SSO login",
|
||||
"enabledHint": "Shows the SSO button on the admin login page. Requires issuer, client ID and secret.",
|
||||
"test": "Test connection",
|
||||
"testHint": "Test runs OIDC discovery against the saved configuration — save first.",
|
||||
"testOk": "Discovery succeeded — issuer is reachable: {{issuer}}",
|
||||
"testFailed": "Discovery failed",
|
||||
"saved": "SSO settings saved",
|
||||
"saveError": "Failed to save SSO settings"
|
||||
}
|
||||
},
|
||||
"analytics": {
|
||||
@@ -3595,6 +3621,16 @@
|
||||
"sessionExpired": "Your verification session expired. Please sign in again.",
|
||||
"locked": "Account temporarily locked due to too many attempts. Try again later.",
|
||||
"lockedRetry": "Account temporarily locked. Try again in {{seconds}} seconds."
|
||||
},
|
||||
"ssoDivider": "or",
|
||||
"ssoSignIn": "Sign in with SSO",
|
||||
"ssoErrors": {
|
||||
"config": "SSO is misconfigured — check the SSO settings or sign in with email and password.",
|
||||
"state": "The SSO sign-in expired or was tampered with. Please try again.",
|
||||
"idp": "The identity provider rejected the sign-in. Please try again or use email and password.",
|
||||
"inactive": "Your admin account is deactivated.",
|
||||
"not_provisioned": "No admin account matches your SSO identity. Ask an administrator to invite you.",
|
||||
"no_email": "Your identity provider supplied no email address — an account cannot be created."
|
||||
}
|
||||
},
|
||||
"slideshow": {
|
||||
|
||||
Reference in New Issue
Block a user