feat(dashboard-auth): generic self-hosted OIDC provider (+ multi-provider verify fix) - #38806
Closed
benbarclay wants to merge 2 commits into
Closed
benbarclay wants to merge 2 commits into
benbarclay wants to merge 2 commits into
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.
Collaborator
|
Merged via #38917. Your commits were cherry-picked onto current main with your authorship preserved in git log (rebase merge — 616c0a3, f57ce34). One salvage note: your branch predated the password-login subsystem (ed9e8ba), so the original middleware.py diff would have reverted Verified: 90 targeted tests pass + 4-scenario E2E of the verify chain. Thanks! |
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
Adds a generic self-hosted OpenID Connect auth provider for the
hermes dashboardOAuth gate, so operators running their own IdP can gate the dashboard without any per-IdP code. Works against any conformant OIDC server — Authentik, Keycloak, Zitadel, Authelia, Auth0, Okta, Google, etc.This is a pure drop-in plugin (
plugins/dashboard_auth/self_hosted/) implementing the existingDashboardAuthProviderprotocol — it touches no core auth/runtime/login paths. The one core change is an independent middleware bug fix (see below) surfaced while testing the plugin live.What the plugin does
{issuer}/.well-known/openid-configuration(cached; advertisedissuerpinned to config; endpoints required to be HTTPS, loopbackhttpallowed for local-dev IdPs).jwks_uriwithiss/audpinned to the configuredissuer/client_id, and maps standard OIDC claims (sub,email,name/preferred_username,groups→org_id) onto aSession.refresh_tokengrant for silent re-auth; RFC 7009 revocation on logout when the IdP advertises it.Verifies the ID token, not the access token, because OIDC guarantees the ID token is a signed JWT carrying identity, whereas access-token format is opaque to the client per spec (many IdPs issue opaque access tokens). This is the only universally-correct choice across self-hosted IdPs. (The bundled
nousprovider verifies its access token because Nous Portal mints a custom JWT access token — a non-OIDC shortcut.)Public PKCE clients only for now; confidential clients (
client_secret) are left as a documentedTODOseam.Configuration
Or via env (env-wins-config, empty-is-unset — same convention as the
nousplugin):HERMES_DASHBOARD_OIDC_ISSUER,HERMES_DASHBOARD_OIDC_CLIENT_ID,HERMES_DASHBOARD_OIDC_SCOPES.Bundled bug fix (core — needs review)
While testing the plugin against a local Keycloak I hit a real multi-provider bug in
hermes_cli/dashboard_auth/middleware.py. The gate verifies a session cookie by trying each registered provider'sverify_sessionin turn (the cookie stores only the access token, not the issuing provider). A provider whose IdP/JWKS is unreachable raisesProviderError— and the loop used to return HTTP 503 on the firstProviderError, before any later provider got a turn.Concretely: a self-hosted-OIDC session hits the
nousprovider first (registered earlier);noustries to reach Nous Portal's JWKS, which is unreachable in a self-hosted deployment, so it raises → the gate 503s before theself-hostedprovider can verify the token.Fix: a
ProviderErrorfrom one provider is logged and the loop continues. 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.Docs
Adds a Self-hosted OIDC provider section to
web-dashboard.md, including a copy-paste Keycloak worked example (realm import JSON +docker run+ dashboard wiring + login walkthrough), with a note that the same pattern applies to Authentik/Zitadel/Authelia.Testing
/api/auth/mereturnsprovider: self-hosted).pathspec-missing failures in unrelated code.Test Plan