Skip to content

fix: resolve OIDC endpoints via discovery instead of hardcoded paths - #86

Merged
DominikPinsel merged 1 commit into
mainfrom
fix/oidc-discovery
Aug 13, 2026
Merged

fix: resolve OIDC endpoints via discovery instead of hardcoded paths#86
DominikPinsel merged 1 commit into
mainfrom
fix/oidc-discovery

Conversation

@DominikPinsel

Copy link
Copy Markdown
Owner

Problem

Users logging in via OIDC are persisted with empty username and email (verified in the users table). Root cause: the hub and MCP middleware hardcoded the userinfo URL to {issuer}/oauth/v2/userinfo, which 404s on current Zitadel — the discovery document advertises userinfo_endpoint: /oidc/v1/userinfo.

The failure was silently swallowed (// If userinfo fails, continue with JWT-only user data), and since Zitadel access JWTs don't carry preferred_username/name/email claims, enrichment never happened and UpsertUser(sub, "", "") never wrote anything.

Fix

  • shared/auth/oidc: new FetchDiscovery/ResolveEndpoints — read jwks_uri and userinfo_endpoint from {issuer}/.well-known/openid-configuration. Works with any compliant IdP (Zitadel, Keycloak, Entra ID, Authelia, Okta), not just Zitadel. Falls back to the legacy Zitadel paths if discovery is unreachable at startup (with a warning), so boot is not blocked.
  • services/hub/cmd/hub/wire.go and services/mcp: resolve endpoints via discovery at startup; log when discovery fails. Hub supports OIDC_JWKS_URL / OIDC_USERINFO_URL env overrides as escape hatches.
  • middleware.go: userinfo enrichment failures now log a rate-limited warning (max once per 5 min) instead of being swallowed.

Notes

  • services/mcp/internal/auth/proxy.go already used the correct /oidc/v1/userinfo; its other /oauth/v2/* paths match the current Zitadel discovery document and are left unchanged.
  • After merge + redeploy, existing users with empty username/email repopulate on their next authenticated request (identity persist middleware) — or via the existing username re-sync endpoint.

Verification

  • Live check pre-fix: GET /oauth/v2/userinfo → 404, GET /oidc/v1/userinfo → 401; users table shows empty username/email for all OIDC-created users.
  • New unit tests for discovery (success, 404, invalid JSON, missing jwks_uri, fallback).
  • go build, go vet, golangci-lint run clean for shared/auth/oidc, services/hub, services/mcp; full go test ./... passes for all three.

The hub and MCP middleware hardcoded the userinfo URL to
{issuer}/oauth/v2/userinfo, which 404s on current Zitadel (the
discovery document advertises /oidc/v1/userinfo). The failure was
silently swallowed, so username/email enrichment never happened and
users were persisted with empty username/email forever.

- shared/auth/oidc: add FetchDiscovery/ResolveEndpoints reading
  jwks_uri and userinfo_endpoint from the OIDC discovery document,
  with fallback to the legacy Zitadel paths so a transient discovery
  failure at startup does not block boot
- hub wire.go + mcp main.go: resolve endpoints via discovery, log a
  warning when discovery fails; hub supports OIDC_JWKS_URL and
  OIDC_USERINFO_URL overrides
- middleware: log a rate-limited warning when userinfo enrichment
  fails instead of swallowing the error
@DominikPinsel

Copy link
Copy Markdown
Owner Author

@review-agent please have a look

@ainsel-reviewer ainsel-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Overall: Approve

Clean, targeted fix with good fallback behaviour and adequate test coverage.

Blockers

None.

Suggestions

  • shared/auth/oidc/discovery.go:43Observation: FetchDiscovery validates that jwks_uri is present but does not require userinfo_endpoint. If a provider omits it in an otherwise valid discovery document, the hub will pass an empty UserInfoURL to the middleware and silently skip userinfo enrichment. Suggestion: consider logging a warning when userinfo_endpoint is missing so operators know enrichment is disabled.
  • shared/auth/oidc/discovery.go:64 — The fallback paths (/oauth/v2/keys, /oauth/v2/userinfo) are Zitadel-legacy constants. A brief comment on the fallbackJWKSPath/fallbackUserInfoPath constants noting they are Zitadel-specific legacy defaults would help future readers.
  • services/mcp/internal/auth/middleware.go — The empty-string fallback logic duplicates the fallback already handled by ResolveEndpoints. That is fine for resilience, but be aware it creates two places to update if the legacy paths ever change.

Nits

  • services/hub/cmd/hub/wire.go uses the imported slog package directly, while services/mcp/cmd/server/main.go uses a log alias (log "log/slog"). Picking one convention would be slightly nicer.

What's Done Well

  • Self-contained shared/auth/oidc discovery package with clear fallback semantics.
  • Discovery failure does not block boot — good operational choice.
  • Rate-limited warning replaces the previously silent swallowing of userinfo errors.
  • Env overrides (OIDC_JWKS_URL, OIDC_USERINFO_URL) are useful escape hatches.
  • Unit tests cover success, 404, invalid JSON, missing jwks_uri, and fallback paths.

Testing

New discovery_test.go covers the discovery happy path and several error cases. CI (hub + MCP) is passing.

@ainsel-reviewer

Copy link
Copy Markdown

@DominikPinsel LGTM — approved.

@DominikPinsel
DominikPinsel merged commit c5438e9 into main Aug 13, 2026
5 checks passed
@DominikPinsel
DominikPinsel deleted the fix/oidc-discovery branch August 13, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant