feat(dashboard-auth): generic self-hosted OIDC provider (+ multi-provider verify fix) - #38917
Merged
Conversation
Adds a bundled dashboard-auth provider plugin that authenticates the
web dashboard against any conformant self-hosted OpenID Connect server
(Authentik, Keycloak, Zitadel, Authelia, Auth0, Okta, Google, …) using
standard OIDC — no per-IDP code.
It's a pure drop-in plugin implementing the DashboardAuthProvider
protocol; it touches no core auth/runtime/login paths. Mechanics:
- OIDC discovery from {issuer}/.well-known/openid-configuration
(cached; issuer pinned; endpoints required HTTPS, loopback http
allowed for local-dev IDPs)
- authorization-code + PKCE (S256), public client
- verifies the OIDC ID token (RS256/ES256) against the discovered
jwks_uri with iss/aud pinned to the configured issuer/client_id, and
maps standard claims (sub/email/name/preferred_username, groups→org)
onto a Session
- standard refresh_token grant for silent re-auth; RFC 7009 revocation
on logout when advertised
Verifies the ID token (not the access token) because OIDC guarantees the
ID token is a signed JWT carrying identity, while access-token format is
opaque to the client per spec — the only universally-correct choice
across self-hosted IDPs.
Config via dashboard.oauth.self_hosted.{issuer,client_id,scopes} in
config.yaml or HERMES_DASHBOARD_OIDC_{ISSUER,CLIENT_ID,SCOPES} env vars
(env-wins-config, empty-is-unset — same convention as the nous plugin).
Confidential clients (client_secret) left as a documented TODO seam.
Docs: adds a Self-hosted OIDC section to the web-dashboard guide,
including a copy-paste Keycloak worked example (realm import + docker
run + dashboard wiring + login walkthrough).
Tests: 65 cases covering construction, discovery (incl. issuer
mismatch + https enforcement), start_login/PKCE, complete_login, ID
token verification, refresh/revoke, and env/config precedence.
…derError The gated dashboard verifies a session cookie by trying each registered DashboardAuthProvider's verify_session in turn (the session cookie stores only the access token, not which provider issued it). A provider that doesn't recognise a token returns None; a provider whose IDP/JWKS is unreachable raises ProviderError. The loop used to return HTTP 503 on the FIRST ProviderError, before any later provider got a turn. With multiple providers stacked, that means an unreachable IDP for a session you didn't even use blocks login through a different, reachable provider. Concrete repro: a self-hosted-OIDC session hits the 'nous' provider first (registered earlier); nous tries to reach Nous Portal's JWKS, which is unreachable in a self-hosted deployment, so it raises — and the gate 503s before the 'self-hosted' provider can verify the token. Hit live while testing the new self-hosted OIDC plugin against a local Keycloak. Fix: a ProviderError from one provider is logged and the loop continues to the next. A 503 is returned only if NO provider verified the token AND at least one was unreachable — distinguishing a transient IDP outage (don't force a needless re-login) from a token that's genuinely invalid (fall through to refresh/relogin). Single-provider behaviour is unchanged. Tests: adds an _UnreachableProvider stub and three cases — unreachable provider first must not block a working second; all-unreachable still 503s; reachable-but-unrecognised falls through to 401/relogin (not 503). Mutation-tested: reverting the fix makes the first case fail with the exact 503 bug.
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
3 |
First entries
tests/plugins/dashboard_auth/test_self_hosted_provider.py:28: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/dashboard_auth/self_hosted/__init__.py:77: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/plugins/dashboard_auth/test_self_hosted_provider.py:26: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
✅ Fixed issues: none
Unchanged: 5081 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
2 tasks
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Operators running their own IdP can now gate the
hermes dashboardbehind any conformant OIDC server with zero per-IdP code. Salvage of #38806 (@benbarclay) onto currentmain.Generic self-hosted OpenID Connect auth provider as a drop-in plugin implementing the existing
DashboardAuthProviderprotocol — Authentik, Keycloak, Zitadel, Authelia, Auth0, Okta, Google, etc. Plus a multi-provider verify-chain fix surfaced while testing it live.Changes
plugins/dashboard_auth/self_hosted/: OIDC discovery (cached, issuer-pinned, HTTPS-enforced w/ loopback http exception), auth-code + PKCE (S256) public client, ID-token verification (RS256/ES256) against discovered JWKS withiss/audpinned, standard OIDC claim mapping,refresh_tokensilent re-auth, RFC 7009 revocation on logout.hermes_cli/dashboard_auth/middleware.py: aProviderErrorfrom one provider (its IdP/JWKS unreachable) no longer aborts the verify chain — the loop continues so a later reachable provider can verify. 503 only when no provider verifies and at least one was unreachable (transient outage vs. genuinely-invalid token). Single-provider behaviour unchanged.website/docs/.../web-dashboard.md: Self-hosted OIDC section + copy-paste Keycloak worked example.Salvage notes
Original branch was 21 commits behind and predated the password-login subsystem (
ed9e8ba09). The originalmiddleware.pydiff would have reverted/auth/password-loginout of_GATE_PUBLIC_PREFIXES. Cherry-picked onto current main; the 3-way merge dropped the stale deletion, and I verified/auth/password-loginsurvives. Docs conflict (adjacent username/password + OIDC sections) resolved keeping both.Validation
test_dashboard_auth_middleware.py+test_self_hosted_provider.py/auth/password-loginrevertOriginal PR: #38806. Authorship preserved per-commit (will rebase-merge).
Infographic