OIDC and single sign-on
Audience: operator · Profile:
both· Read this when: you want single sign-on for the dashboard instead of local passwords, or you are working out which of the dashboard's OIDC features you need.
This page sets up single sign-on for the dashboard. The dashboard speaks OIDC in several other places too, each with its own page:
| Page | Read it when |
|---|---|
| The dashboard's own identity | you want the dashboard to reach AWS, Azure, GCP or a k3s cluster with a short-lived token instead of a stored key. Here the dashboard is the issuer. Its tokens come from the SPIRE server that Remote Agents also uses |
| Dex | you want one issuer that both the dashboard and your on-prem clusters trust |
| Kubernetes federation with your IdP | you want people signing in to managed clusters as themselves, with their IdP groups as RBAC subjects. One page per provider: Entra ID, Okta, PingOne, other OIDC |
| Identity providers | you want to browse or change users and groups in Entra ID, Okta or PingOne, rather than sign in through one. That lives in the Directories section |
| Sign in with Microsoft (Entra OAuth) | you already use the legacy Entra-only sign-in button, or are moving from it to the generic path below |
What is it?
The OIDC integration lets people sign in to the dashboard with your existing
identity provider (IdP) instead of a local username and password. It speaks
generic OpenID Connect, driven entirely by the provider's discovery document
(<issuer>/.well-known/openid-configuration), so one implementation covers any
compliant IdP — you point it at an issuer, register a client, and it reads the
authorize, token and JWKS endpoints from the provider itself. There is no
per-provider code path to maintain.
Under the hood it uses the authorization-code flow with PKCE (S256, always sent), and every returned ID token has its signature verified against the provider's JWKS, along with the issuer and audience, before anyone is logged in.
OIDC login sits alongside the dashboard's other sign-in methods and doesn't replace them:
- Local password accounts — unaffected.
- FIDO2 / WebAuthn MFA — unaffected.
- "Sign in with Microsoft" (legacy Entra/Azure path) — configured separately under the Azure settings and unaffected. Its button shows only when that path has a client ID and tenant, and sits below this one when both are set. If you'd rather run Entra through this generic path, you can — see Provider quick reference.
Scope. GitHub is not supported — it isn't an OIDC provider (no discovery document, no ID token). SAML is not supported either. This integration is OIDC only.
Supported providers
Any spec-compliant OIDC provider works. Verified/common ones:
| Provider | Notes |
|---|---|
| Keycloak | Groups arrive in the groups claim (add a groups mapper to the client scope). |
| Authentik / Authelia | groups claim. |
| Okta | Group memberships often surface as roles rather than groups — set Groups claim accordingly. |
| Auth0 | Groups/roles usually need a custom claim/action to be emitted. |
| Google Workspace | Standard OIDC; Google does not emit groups in the ID token — group→workstream mapping won't apply. |
| JumpCloud | groups claim. |
| Ping | groups claim (configurable). |
| GitLab | Standard OIDC. |
| Microsoft Entra ID | groups claim carries group object IDs, not display names — map by object ID (see below). |
If your IdP publishes <issuer>/.well-known/openid-configuration and issues a
standard ID token, it will work even if it isn't in this list.
Prerequisites
| Requirement | Notes |
|---|---|
| An OIDC identity provider | Must expose a discovery document at <issuer>/.well-known/openid-configuration. |
| Ability to register an application/client with the IdP | Confidential or public client — both work (PKCE is always sent). |
| A redirect (callback) URI you can register | <dashboard-base-url>/api/auth/oauth/oidc/callback (exact value shown in the Settings panel). |
| The dashboard reachable at a stable base URL | The base URL the browser uses must match the redirect URI you register. |
| Dashboard admin access | The OIDC panel and group mappings are admin-only. |
Setup
Step 1 — Register an application with your IdP
Create an OIDC application/client in your provider and note the client ID (and client secret, if you use a confidential client). Set the redirect / callback URI to:
<dashboard-base-url>/api/auth/oauth/oidc/callback
For example, https://dashboard.example.com/api/auth/oauth/oidc/callback. The
OIDC settings panel prints the exact value the server will send — copy it from
there to avoid typos. The URI must match exactly, including scheme, host and
port.
<dashboard-base-url> is the origin a browser reaches the UI at. The server
takes it from, in order:
- UI base URL (
UI_BASE_URL, or the field on the OIDC panel); WEBAUTHN_ORIGIN, unless it is a localhost/loopback origin (the shipped default);PUBLIC_BASE_URL;- the incoming request.
On a single-host install nothing needs setting. On a split-host install —
agents on their own hostname behind the gateway sidecar, with PUBLIC_BASE_URL
set to that agent hostname because enrolment codes embed it — step 3 would send
the IdP back to the agent hostname, which 404s every path but /api/agent/*.
Setting WEBAUTHN_ORIGIN to the UI host (which a split-host install already
needs for security keys) is enough; set UI_BASE_URL when it is not. Neither
changes what agents sign against. The same origin is used for the Entra
(/api/auth/oauth/azure/callback) callback.
A public client (no secret) is fine — PKCE protects the code exchange. Leave the client secret blank in that case.
If you want group-based access (recommended), make sure the application is configured to emit a groups (or roles) claim in the ID token. Note which claim name your provider uses.
Step 2 — Configure the dashboard
On a new install, the setup wizard's Admin step has a Single sign-on (OIDC) — optional panel with the first four fields below; the rest keep their defaults until you change them in Settings. Afterwards, or on an existing instance, go to Settings → Integrations → Single sign-on (OIDC) and fill in:
| Field | Config key | Default | Notes |
|---|---|---|---|
| Issuer URL | oidc_issuer |
— | Base URL only, e.g. https://keycloak.example.com/realms/main. /.well-known/openid-configuration is appended automatically. |
| Client ID | oidc_client_id |
— | From step 1. |
| Client secret | oidc_client_secret |
— | Optional — leave blank for a public client. Stored encrypted; shown as dots on reload. Leave the dots untouched to keep the stored value. |
| Button label | oidc_provider_name |
issuer host | Shown on the login page as "Sign in with …". |
| Scopes | oidc_scopes |
openid profile email groups |
Space-separated. openid is always included even if you omit it. |
| Groups claim | oidc_groups_claim |
groups |
The ID-token claim that carries the user's group names/IDs, used for workstream mapping. Set to roles for Okta-style setups. |
There is no separate "enable" toggle. SSO goes live the moment an issuer and client ID are saved, and turns off when you clear the issuer. Settings are stored encrypted in the application database and apply immediately — no
.envedit and no restart.
Step 3 — Test the connection
Click Test connection in the panel. This fetches the live discovery document
and reports the endpoints the provider advertises, its scopes_supported, and
whether the groups claim you configured appears in the provider's
claims_supported.
The groups-claim check is advisory. Many providers omit
claims_supportedentirely, and some emitgroupswithout advertising it — so a "not advertised" result is not necessarily a problem. A typo'd issuer, on the other hand, fails here with the exact URL it couldn't reach.
Step 4 — Map groups to workstreams (authorization)
Authentication proves who someone is; group mappings decide what they can do.
On the RBAC → Groups tab (/rbac#groups), add a mapping for each provider group:
- Group ID — the value that appears in the user's groups claim (for Entra, the group's object ID, not its name).
- Workstream — the dashboard workstream that group's members are placed in.
- Default permissions (optional) — a JSON object of permission scopes granted to that group.
On every SSO login the dashboard:
- Reads the user's group claims and matches them against your mappings.
- Assigns the matched workstreams and unions each matched group's default permissions into the user's session permissions (so removing someone from a group in the IdP drops the corresponding access on their next login).
- Auto-creates the user on first login if any group matched — no pre-registration needed.
- Rejects the login (
/login?error=not_authorized) if the user is in none of the mapped groups.
If you configure no group mappings at all, the dashboard falls back to legacy behavior: the user must already exist, and SSO authenticates them. An existing local account needs Sign in with SSO turned on first (below).
How an SSO identity finds its account
An SSO login is matched to an account by the identity provider's subject (sub; for
Entra, the object id oid), never by email alone. The first time a subject signs in:
- No account has its email → a new account is created (if a group mapping matched).
- An account has its email → it is linked only if all of these hold, and refused
otherwise (
/login?error=sso_link_refused, with the reason in the log): - the provider verified the email. For OIDC that means
email_verified: trueon theemailclaim itself;preferred_username,upnandunique_namenever count, because on many providers the user can set them. For Entra it means signing in through one specific tenant: multi-tenant (common,organizations) sign-in links by subject only; - the account is not already linked to a different subject;
- it is not a local (password) account, unless an administrator has turned on Sign in with SSO for it on RBAC → Users → Edit. That switch ends password sign-in for the account at once, and the next SSO login with a matching verified email links it. Untick it to undo before that happens. Your own account cannot be switched; ask another administrator.
Once linked, the subject alone signs the account in; a later change of email at the
provider does not matter. Every link is written to the audit log
(sso_account_linked).
Before this, any SSO login was matched by email to any existing account, administrators included, which let anyone able to put that address on an IdP profile sign in as it.
For the permission model and how group-derived permissions combine with admin-granted ones, see the Entitle user-JIT design doc.
What it enables
| Capability | Description |
|---|---|
| SSO login button | A "Sign in with <label>" button appears on the login page once configured. |
| Auto-provisioning | Users in a mapped group are created automatically on first login. |
| Group-driven access | Workstreams and permissions are derived from IdP group membership and re-synced on every login. |
| Permission enforcement | Derived permissions feed the dashboard's scope/level checks (vms, aws, k8s, cloud_database, …). |
| Workload tokens | Optional, separate from sign-in: the same (or another) IdP's access tokens for workloads, mapped to service accounts. Configured under Workload tokens in this panel; needs an audience, not a client id. |
Dex (optional)
Dex can sit between the dashboard and your IdP, so that on-prem clusters trust the same issuer people sign in to the dashboard with. It is optional for the dashboard, and the only way people reach an on-prem (k3s) cluster. See Dex.
Provider quick reference
Issuer URL format per provider (paste the base into the Issuer URL field):
| Provider | Issuer URL example | Groups claim |
|---|---|---|
| Keycloak | https://keycloak.example.com/realms/<realm> |
groups |
| Okta | https://<org>.okta.com |
groups or roles |
| Auth0 | https://<tenant>.auth0.com/ |
custom (namespaced) |
| Google Workspace | https://accounts.google.com |
none in ID token |
| Microsoft Entra ID | https://login.microsoftonline.com/<tenant-id>/v2.0 |
groups (object IDs) |
| JumpCloud | https://oauth.id.jumpcloud.com/ |
groups |
Troubleshooting
"Could not fetch the OIDC discovery document from …" — the issuer URL is wrong
or unreachable from the dashboard container. The error includes the exact URL it
tried; open it in a browser. Use Test connection to confirm. Remember: enter
the base issuer only — don't include /.well-known/openid-configuration
yourself.
"ID token failed validation" — usually an audience or issuer mismatch, or clock
skew. The client ID you configured must equal the token's aud. Confirm the
dashboard host's clock is correct.
"Token response contained no id_token — is the 'openid' scope granted?" — the
openid scope wasn't granted for this client. Ensure openid is in the app's
allowed scopes at the IdP (the dashboard always requests it).
Redirect fails at the provider (redirect_uri mismatch) — the registered
callback must match <dashboard-base-url>/api/auth/oauth/oidc/callback exactly,
including scheme, host and port. Copy the value shown in the Settings panel.
The IdP redirects back to a 404 on the agent hostname — PUBLIC_BASE_URL is
the agent host and neither UI_BASE_URL nor a non-localhost WEBAUTHN_ORIGIN is
set, so the callback was built from the agent host. Set UI base URL on the
OIDC panel to the host users sign in on (see Step 1).
error=not_authorized after login — the user isn't in any mapped group, or the
IdP isn't actually emitting the groups claim. Check RBAC → Groups (/rbac#groups), confirm the app
emits the claim (Entra sends group object IDs, so map by ID not name), and set
the Groups claim field to match your provider (e.g. roles for Okta).
error=not_registered — no group mappings are configured, so login falls back
to requiring a pre-existing local account and none matched this user. Add a group
mapping, or create the local account first.
error=account_disabled — the matching dashboard account is deactivated.
Re-enable it in user management.
Not to be confused with
This page is about dashboard login SSO. Several other features also speak OIDC and are configured elsewhere:
| Feature | What it is | Where |
|---|---|---|
| Cluster RBAC from your IdP | federating Kubernetes cluster RBAC to Entra, Okta, PingOne or another IdP via OIDC | Kubernetes federation with your IdP |
| Workload tokens | a CI job or service calls the dashboard's API with an access token its own IdP issued; no SSO client needed | Service accounts → Tokens from your own IdP |
| SPIFFE workloads | a workload presents a JWT-SVID at /api/oauth/token |
Service accounts → SPIFFE workloads |
| The dashboard as an issuer | the dashboard publishes <issuer>/.well-known/openid-configuration so AWS, Azure, GCP and k3s can trust its own SPIFFE identity |
The dashboard's own identity |
| A Workload Lab trust domain as an issuer | a SPIRE lab's OIDC Discovery Provider, which the dashboard and a k3s API server verify JWT-SVIDs against | SPIFFE and SPIRE |
Dex for on-prem kubectl |
the kubeconfig people download for a cloud=local cluster |
Dex, and Kubernetes |