feat: built-in OIDC authentication for self-hosted - #4176
Conversation
|
@mulaw is attempting to deploy a commit to the Hatchet Team on Vercel. A member of the Team first needs to authorize it. |
|
Hey @awapf. Can you include an AI disclosure section to the PR body? See https://github.com/hatchet-dev/hatchet/blob/main/CONTRIBUTING.md#ai-usage |
df6023c to
80bfae2
Compare
Thanks for the pointer @gregfurman — I've added the 🤖 AI Disclosure to the description. Apologies for not reading CONTRIBUTING.md closely before opening this. On that note, I also see PRs are meant to reference an issue labeled |
Add native OIDC support for self-hosted instances, enabling direct integration with providers like Keycloak, PocketID, and any OpenID Connect compliant identity provider. Changes: - Add OIDC OAuth2 start and callback handlers - Add OIDC config struct with env var bindings (SERVER_AUTH_OIDC_*) - Use OIDC discovery to resolve auth/token endpoints at startup - Cache oidc.Provider for ID token verification (no per-request discovery) - Add OIDC routes to OpenAPI spec and regenerate server code - Expose 'oidc' scheme via /api/v1/meta endpoint - Add SSO button to frontend auth page - Support domain restrictions for OIDC users - Default scopes: openid, profile, email Closes hatchet-dev#3052
- Add nil guards for OIDCOAuthConfig/OIDCProvider in both handlers to prevent nil pointer panic when OIDC is disabled - Move AllowSignup check from start handler to CreateUser path so existing OIDC users can still log in when signup is disabled - Require email_verified before authorizing or linking by email - Add UserInfo endpoint fallback when ID token lacks optional claims (email, name, email_verified) - Validate UserInfo sub claim matches ID token sub per OIDC spec - Normalize BaseURL with TrimRight to prevent double-slash redirect URIs - Add 30s timeout context for OIDC discovery at startup - Enforce 'openid' scope is always present in configured scopes - Add ScopesString field for reliable env var binding and parse it before building OIDCOAuthConfig so SERVER_AUTH_OIDC_SCOPES works
- declare UserUpdateOidcOauthStart/Callback in the authz rbac.yaml public list (alongside the Google/GitHub OAuth routes); the API refuses to start otherwise - allow "oidc" in the OAuthOpts.Provider validator so the user-create path succeeds after a verified login - regenerate the OpenAPI server bindings for the new routes
PR hatchet-dev#3607 shipped without tests. An in-process mock OIDC issuer (discovery + JWKS + signed ID tokens) lets the OIDC paths be exercised without an external IdP: - unit: getOIDCClaimsFromToken accepts a valid token and rejects a wrong audience / missing id_token - integration: upsertOIDCUserFromToken creates then updates a user against a real database (the path that requires provider="oidc")
Add the OIDC provider variables to the self-hosting configuration-options page, alongside the existing Google/GitHub auth config, plus a note on issuer discovery and the callback redirect URI.
… OIDC emails Don't hard-reject an unverified email in the OIDC callback (the Google/GitHub handlers don't either). Store emailVerified = claims.EmailVerified || SERVER_AUTH_SET_EMAIL_VERIFIED, reusing the existing instance-wide setting, so providers that don't emit email_verified (e.g. Microsoft Entra ID) are handled by the dashboard's verify-email gate unless SetEmailVerified is enabled.
|
Based on testing against Microsoft Entra ID (#2648), I changed how email verification is handled. Rather than rejecting logins without a verified email, the OIDC callback now creates the user and stores emailVerified = claims.EmailVerified || SetEmailVerified — consistent with the existing Google/GitHub handlers, and reusing the instance-wide SERVER_AUTH_SET_EMAIL_VERIFIED instead of a new OIDC-specific flag. So providers that emit email_verified (Google, Keycloak) work as before, and providers that don't (Entra ID, which has no email_verified claim) work too — auto-verified when SET_EMAIL_VERIFIED=true, or gated behind the verify-email screen for admin approval when false. Validated end-to-end against a real Entra tenant in both modes. This also closes #2648. |
# Conflicts: # api/v1/server/oas/gen/openapi.gen.go
|
Rebased onto latest main (was 219 commits behind). The only merge conflict was the generated api/v1/server/oas/gen/openapi.gen.go — resolved by re-running hack/oas/generate-server.sh; every hand-written file merged cleanly. Green and ready for review. |
Closes #3052. Supersedes #3607 (and the earlier #1139), building on @yaswanthkumar1995's work and carrying it to the finish line.
Adds a generic OIDC provider so self-hosted instances can authenticate against any OIDC-compliant IdP that issues an
emailclaim (Keycloak [tested], Authentik, Dex, Okta, Microsoft Entra ID [tested], …), following the exact pattern of the existing Google/GitHub OAuth handlers — as @abelanger5 noted on Discord, "OIDC is a small lift on top of the existing OAuth."What it does
oidcprovider:/api/v1/users/oidc/{start,callback}, mirroring the Google/GitHub handlers. Usescoreos/go-oidc/v3with OIDC discovery from an issuer URL and proper ID-token verification (JWKS), plus a UserInfo fallback and asubmatch.SERVER_AUTH_OIDC_ENABLED,_CLIENT_ID,_CLIENT_SECRET,_ISSUER_URL,_SCOPES(defaultopenid profile email).sso(control-plane) provider — they're orthogonal; this is purely self-hosted.restrictedEmailDomains,allowSignup, andemailVerified.emailclaim (Hatchet identifies users by email).email_verifiedis honored when present; providers that don't emit it (e.g. Microsoft Entra ID) are auto-verified via the existingSERVER_AUTH_SET_EMAIL_VERIFIED, or left for the dashboard's verify-email gate — consistent with the Google/GitHub handlers. Closes Enable Microsoft Entra ID OAuth authentication #2648.What's new vs #3607
This is #3607 rebased on
main, plus the things needed to actually merge it:OAuthOpts.Providerwas validated asoneof=google github sso, so creating a user withprovider="oidc"failed after a successful login and ID-token verification. Addedoidcto the allowed set.UserUpdateOidcOauthStart/Callbackinapi/v1/server/authz/rbac.yaml(public, like the Google/GitHub routes) — the API refuses to start otherwise.go test: ID-token verification accepts a valid token, rejects wrong-audience / missingid_token) and an integration test (go test -tags integration: user create + update upsert against a real DB — the exact path the validator bug broke).openapi.gen.go.emailVerified = claims.EmailVerified || SetEmailVerified, so providers without anemail_verifiedclaim (e.g. Entra ID) work — auto-verified via the existing instance setting, or behind the verify-email gate for admin approval.task testandtask test-integrationare green. Verified end-to-end against a live Keycloak (login → code exchange → JWKS verification → user upsert → session).Dependencies / licensing
Adds
coreos/go-oidc/v3and (transitively)go-jose/go-jose/v4, both Apache-2.0 — compatible with the project's MIT license, and the same family of permissive deps already in the tree (gRPC, OTel, …).go-oidcis the canonical Go OIDC library (used by Kubernetes, Dex).Happy to adjust naming/structure to taste — keen to get this landed for the folks in #3052.
🤖 AI Disclosure
main, theoneofvalidator fix andrbac.yamlregistration, the unit + integration tests, the docs, and drafting this description. All of it was human-directed and human-validated: the suite was run locally (task test/task test-integration) and the flow was verified end-to-end against a live Keycloak and Microsoft Entra before opening. The underlying OIDC feature is @yaswanthkumar1995's from feat: add built-in OIDC authentication support #3607.