User guide: OIDC/SSO for kairon-ui
ui.oidc.enabled (Helm chart, off by default) adds "Sign in with SSO" to
the dashboard's login screen -- an Authorization Code + PKCE flow against
an external OpenID Connect identity provider, alongside (not instead of)
ui.auth.users and the legacy shared ui.token.
Why this breaks Go-stdlib-only, deliberately
This is the one place in Kairon that pulls in dependencies beyond the Go
standard library: golang.org/x/oauth2 and github.com/coreos/go-oidc/v3
(which itself needs github.com/go-jose/go-jose/v4 for JWT/JWK
handling). Every other Kairon component -- kairon-controller,
kairon-node, kaironctl, and kairon-ui itself without OIDC enabled --
stays exactly as stdlib-only as before; enabling ui.oidc.enabled doesn't
change what those components link against at all.
The reason this exception was made, specifically here: real JWT signature verification against a JWK set (rotating keys, multiple algorithms, clock skew, replay/nonce handling) is genuinely hard to get right, and hand- rolling it would trade a well-reviewed, widely-used library for a custom implementation nobody should trust more. That's a different calculus than everywhere else Kairon avoids a dependency -- those are almost always "this would be convenient, not necessary." Real OIDC verification is closer to necessary once you've decided to support it at all.
Setup
Register a client with your identity provider first. You'll need:
- Redirect URI:
https://<your-kairon-ui-host>/api/v1/auth/oidc/callback-- register this exactly; kairon-ui never guesses its own externally- reachable URL (proxies and ingress hostnames vary too much to infer safely), soui.oidc.redirectURLmust match what you register, exactly. - Client type: confidential (with a client secret) or public (PKCE
only, no secret) both work --
ui.oidc.clientSecretmay be left empty for a public client. PKCE is always used regardless. - Scopes:
openidis required;ui.oidc.scopesdefaults toopenid,profile,email. Whatever claimui.oidc.usernameClaimnames (defaultemail) must actually be returned by your provider for the scopes you request.
helm upgrade --install kairon ./charts/kairon -n kairon-system \
--set ui.enabled=true \
--set ui.oidc.enabled=true \
--set ui.oidc.issuerURL=https://idp.example.com \
--set ui.oidc.clientID=kairon-ui \
--set ui.oidc.clientSecret=... \
--set ui.oidc.redirectURL=https://kairon.example.com/api/v1/auth/oidc/callback
ui.oidc.issuerURL must serve a standard OIDC discovery document at
{issuerURL}/.well-known/openid-configuration. kairon-ui fetches this
once, at process startup -- a misconfigured or unreachable issuer fails
the container immediately (a visible CrashLoopBackOff), not a confusing
500 the first time someone tries to sign in.
ui.oidc.existingSecret/existingSecretKey reference a pre-created
Secret for the client secret instead, the same escape hatch every other
credential in this chart already has (ui.existingSecret,
migration.tlsSecretName, ...).
What actually happens (the flow)
- The browser hits
GET /api/v1/auth/oidc/login. kairon-ui generates a PKCE code verifier and a nonce, signs both into the OAuth2stateparameter (HMAC-derived fromsessionSecret, but with a distinct, domain-separated key -- astatevalue can never be replayed as a session token or vice versa), and redirects to the provider. - The operator authenticates with the provider (however it wants -- password, hardware key, another SSO hop, whatever). kairon-ui never sees a password.
- The provider redirects back to
GET /api/v1/auth/oidc/callbackwith acodeand the samestate. kairon-ui verifiesstate's signature and expiry (10 minutes), exchangescodefor tokens (PKCE-verified), then verifies the returned ID token's signature, issuer, audience, expiry, and nonce against the provider's published keys. ui.oidc.usernameClaim(defaultemail) becomes the session's username, and kairon-ui issues a normal session token -- the exact samesignSessionprimitivePOST /api/v1/auth/loginalready uses. Ifui.oidc.adminGroupsis set, the ID token's ownui.oidc.groupsClaim(defaultgroups) is also recorded on the token, so admin-gated routes (uiapi.Server.isAdminIdentity) can re-check it against current config on every later request -- see "Identity and authorization model" below. From here on, an OIDC-authenticated session verifies through the exact samewithAuthpath a password session does either way --verifySessionjust also returns whatever groups (if any) were recorded at login time.- kairon-ui redirects the browser to
/oidc/callback#token=...-- a URL fragment, deliberately, never the query string: a fragment is never sent to any server on a subsequent request, so the token never lands in an access log or aRefererheader. The SPA reads it client-side and stores it exactly like a password login's token (sessionStorage).
Identity and authorization model
Username collisions are intentional, not a bug to work around.
ui.oidc.usernameClaim puts OIDC-authenticated sessions in the same
username namespace ui.auth.users occupies. If your identity provider
returns alice@example.com for the same person who also has a
ui.auth.users entry named alice@example.com, both login methods
attribute audit log entries to the identical username -- generally
desirable (one person, one identity, regardless of how they signed in).
Choose a username claim whose values won't accidentally collide across
different real people if that's a concern in your environment.
OIDC admin capability is opt-in, via group mapping. By default,
ui.oidc.adminGroups is empty and an OIDC-authenticated session can do
everything a normal operator can -- create/delete Machines, migrate,
snapshot -- but nothing admin-only, exactly Kairon's original OIDC
behavior. Setting ui.oidc.adminGroups (and, if your IdP doesn't call the
claim "groups", ui.oidc.groupsClaim) grants admin capability to any
OIDC session whose ID token's groups claim contains one of the configured
names -- including POST /api/v1/users/{username}/password, resetting
another operator's password. This is checked fresh on every request
against the current ui.oidc.adminGroups config
(uiapi.Server.isAdminIdentity), not baked into the session token at
login time -- an operator changing adminGroups (and rolling the
deployment) takes effect for an already-signed-in caller's very next
request, without them needing to log in again. The group membership
itself, though, is only as fresh as the caller's own last login: it comes
from the ID token issued at sign-in time, not re-queried from the IdP on
every request, so a group added to someone's IdP account mid-session
won't grant admin until they sign in again.
POST /api/v1/auth/password (changing your own password) still
doesn't apply to an OIDC session, admin group or not -- an OIDC identity
has no PasswordHash in Kairon at all (findUser never finds an
OIDC-only username in ui.auth.users), so there's nothing for that route
to change regardless of admin status. Only POST /api/v1/users/{username}/password
(an admin resetting someone else's static-account password) is
affected by adminGroups.
Logout works normally. POST /api/v1/auth/logout revokes an
OIDC-issued session token exactly like a password-issued one -- there's
no separate "OIDC logout" concept, and no round trip back to the identity
provider (a real limitation: signing out of Kairon doesn't sign the
operator out of the identity provider itself, or any other application
using it).
Real limits today (first cut)
- Group-to-admin mapping (
ui.oidc.adminGroups) is a flat allowlist of group names -- no group-to-role hierarchy, no nested/derived groups, and no support for an IdP that only exposes group membership via a separate userinfo/Graph API call instead of the ID token's own claims (this reads only what's already in the verified ID token, deliberately, to avoid an extra network round trip and a second thing to verify). - An admin-group membership change on the IdP side only takes effect at
the caller's next login (the ID token is only ever read at sign-in
time) -- see above. A change to
ui.oidc.adminGroupson the Kairon side takes effect immediately, for every already-signed-in caller, no re-login needed -- the two have different freshness properties, don't confuse them. - No provider-initiated (SP-initiated only) logout or single-logout (SLO) propagation back to the identity provider.
- No refresh-token use: a kairon-ui session is a fixed 12-hour token, same TTL as a password login, not renewed against the identity provider in the background. Re-authenticating after expiry means clicking "Sign in with SSO" again.
ui.oidc.usernameClaimis a single, fixed claim name -- no fallback chain (e.g. "tryemail, thenpreferred_username") and no provider-specific claim transformation.