OIDC Single Sign-On (SSO)

Temps lets you log in to the console with any standards-compliant OpenID Connect (OIDC) identity provider, so your team authenticates through your existing IdP instead of a separate Temps password. Use it when you want centralized identity, group-driven roles, and just-in-time account creation — for example to route all logins through Okta, Auth0, Keycloak, Google Workspace, or Microsoft Entra ID (Azure AD). It works with any compliant OIDC provider; the console also ships setup templates for the common ones plus a Generic template for everything else (Authentik, Zitadel, etc.).

How it works

When a provider is enabled, the login form renders a "Sign in with {provider name}" button per enabled provider. Clicking it runs a standard OIDC authorization-code flow:

  1. Discovery — Temps fetches the provider's OpenID configuration from its issuer URL.
  2. Authorization redirect — the browser is sent to the IdP via GET /api/auth/oidc/login/{slug}. Temps generates a server-side state token and an ID-token nonce, and uses PKCE with a SHA-256 code challenge.
  3. Callback — the IdP redirects back to GET /api/auth/oidc/callback. Temps exchanges the code, then validates the state token and the nonce.
  4. Identity from the ID token — all identity claims come exclusively from the cryptographically verified ID token. Temps does not call the IdP's userinfo endpoint.
  5. Provisioning & roles — on first login, just-in-time (JIT) provisioning creates the Temps account (gated per provider by jit_provisioning). IdP groups/claims are mapped to Temps roles via role mappings ordered by priority.

The public login route is keyed by an opaque slug, not the provider's database ID. The slug is the kebab-cased provider name plus a 4-byte SHA-256 suffix, so internal integer IDs are never exposed to unauthenticated callers and can't be enumerated.

Provider configuration fields

These columns live on each oidc_providers row. Templates prefill most of them; defaults are applied on create.

FieldDefaultPurpose
templategenericSetup template: okta, auth0, keycloak, google, microsoft, or generic. Prefills issuer URL, scopes, and claim names.
group_claimgroupsID-token claim that carries the user's IdP groups, used for role mapping.
role_claimrolesID-token claim that carries roles, used for role mapping.
default_roleuserTemps role assigned when no role mapping matches.
trust_idp_emailfalsePer-provider opt-out of the email_verified ID-token gate. Leave off unless your IdP never emits the claim (see note below).

Routes

All routes are served under the /api prefix.

Public (login flow)

MethodPathPurpose
GET/api/auth/oidc/providersEnabled-provider summaries for the login page. Returns the deterministic slug, name, and template — never the integer DB ID.
GET/api/auth/oidc/login/{slug}Redirects the browser to the IdP authorize URL.
GET/api/auth/oidc/callbackCompletes login, sets the session cookie, honors MFA. Re-checks provider.enabled.

Admin (requires the SettingsWrite permission)

MethodPathPurpose
POST/api/admin/oidc/providersCreate a provider.
GET/api/admin/oidc/providersList providers (full detail, including integer ID).
PATCH/api/admin/oidc/providers/{provider_id}Update a provider.
DELETE/api/admin/oidc/providers/{provider_id}Delete a provider.
POST/api/admin/oidc/providers/{provider_id}/testTest connection (discovery against the issuer).
GET/api/admin/oidc/providers/{provider_id}/usersList users federated through this provider.
GET/api/admin/oidc/providers/{provider_id}/role-mappingsList role mappings for this provider.
POST/api/admin/oidc/providers/{provider_id}/role-mappingsCreate a role mapping.
DELETE/api/admin/oidc/role-mappings/{mapping_id}Delete a role mapping.

All provider and role-mapping writes (create / update / delete) emit audit logs. Updates record a fields_changed list so an auditor can tell whether (for example) the client secret was rotated without comparing values.

Role mapping

Role mappings translate an IdP group into a Temps role. Each mapping has a priority (default 100); when a user logs in, mappings are evaluated in ascending priority (then by ID), and the first match wins. The idp_group value is capped at 256 characters and rejects control characters.

What happens when a user is in multiple IdP groups? The mapping with the lowest priority value that matches one of the user's groups wins — lower numbers are evaluated first. This lets you express precedence: give the admin group priority: 100, engineers priority: 200, and so on, so a user who is in both groups lands on the admin role.

Fallback chain when no mapping matches. Resolution runs in this exact order:

  1. Group mappings, in ascending priority. A mapping whose idp_group is the literal * is a catch-all that matches every user, so a low-priority * mapping (for example priority: 999) is the cleanest way to say "everyone who reaches this point gets the user role."
  2. role_claim fallback — if no group mapping matched, Temps reads the provider's role_claim (default roles) from the ID token and tries the first role value there.
  3. default_role — if neither matched, the provider's default_role (default user) is assigned.

When SSO assigns a role on login, it replaces any other Temps roles the user previously held, so the resolved role is authoritative on every login rather than additive.

The email_verified gate and trust_idp_email

By default Temps refuses to link an incoming SSO identity onto an existing local account — or to JIT-provision a new one — unless the IdP's ID token asserts email_verified: true. This is an anti-account-takeover measure: because the users table has a UNIQUE(email) constraint and SSO can both link to and create accounts, an attacker who can register victim@example.com at a configured IdP without verifying that mailbox could otherwise seize the victim's existing password or magic-link account on first SSO login. oidc_service::resolve_user enforces the check on both the link path and the JIT-provision path; when the claim is absent and trust_idp_email is false, the request is rejected with OidcError::EmailNotVerified and a warn is logged on the tracing target temps_auth::oidc::abuse.

trust_idp_email (per-provider, default false) exists for the one case where the gate is pure noise: corporate IdPs that never emit the claim and where an admin controls every account. Okta's Org Authorization Server is the canonical example — there is no Claims tab to add email_verified and no Token Preview to confirm it.

When to enable it: only for corporate IdPs where an admin controls provisioning (Okta Org Authorization Server, Microsoft Entra ID, internal SSO). Never enable it for public IdPs that allow self-signup (Auth0 social logins, Google consumer accounts) — that reintroduces the takeover vector the gate closes.

It bypasses the gate, not the truth: the local users.email_verified column is still only flipped to true when the IdP actually asserted the claim, so the database keeps recording what the IdP really said. Every bypass logs a warn on temps_auth::oidc::trust_bypass with the provider ID, email, and subject. In the console the flag is a switch in the OIDC provider edit form's First-login policy section, inside an amber warning block; create/update audit rows record its value (and add trust_idp_email to fields_changed on update) so an auditor can answer "who turned this on and when."

Enable on an existing provider

curl -X PATCH https://your-temps-instance.com/api/admin/oidc/providers/{provider_id} \
  -H "Authorization: Bearer your-admin-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "trust_idp_email": true }'

Omitting trust_idp_email from any create/update body leaves it false, so you can't accidentally disable the gate by reusing an older payload.

Setting up a provider in the console

  1. Open Settings → Authentication and add an SSO provider.
  2. Pick a template (Okta, Auth0, Keycloak, Google, Microsoft, or Generic). The template prefills the issuer URL, scopes, and group/role/default-role claim names.
  3. Fill in the issuer URL, client ID, and client secret from your IdP's OIDC application.
  4. Set the redirect URL to your console origin's callback path — it must match /api/auth/oidc/callback on the origin where the console is served.
  5. Run Test connection to verify discovery against the issuer.
  6. Configure role mappings (IdP group → Temps role) and a default role.
  7. Enable the provider. The "Sign in with {provider name}" button then appears on the login form.

Local development with Keycloak

Temps ships a one-command local IdP for development under tools/keycloak-dev/ (a docker-compose.yml, a pre-seeded realm-temps.json, and a setup.sh). It brings up Keycloak on http://localhost:8180 with a temps realm already configured for the Temps EE console.

# From the temps repo root
tools/keycloak-dev/setup.sh

This starts Keycloak and prints the realm details:

  • Issuer: http://localhost:8180/realms/temps
  • Client ID: temps-ee · Client secret: temps-ee-dev-secret
  • Redirect URIs: http://localhost:9081/api/auth/oidc/callback and http://localhost:9100/api/auth/oidc/callback (EE web dev)
  • Test users: sso-admin / sso-admin (group temps-admins, role admin) and sso-user / sso-user (group temps-users, role user)

Then in the console (EE dev console at http://localhost:9081), go to Settings → Authentication → Add SSO Provider, choose the Keycloak template, paste the issuer/client values above, set Group claim: groups, Role claim: roles, Default role: user, and run Test connection before enabling.

Schema

OIDC SSO is added by three migrations:

MigrationAdds
m20260522_000001_oidc_ssoCreates oidc_providers and oidc_login_states; adds users.oidc_provider_id and users.oidc_subject with a unique compound index users_oidc_unique as the federation key.
m20260522_000002_oidc_role_mappingsCreates oidc_role_mappings; adds template, group_claim, role_claim, and default_role columns to oidc_providers.
m20260526_000002_add_trust_idp_email_to_oidc_providersAdds oidc_providers.trust_idp_email boolean not null default false.

The federation key is the (oidc_provider_id, oidc_subject) pair on users — the IdP's stable subject identifier, not the email. This is what lets Temps reliably re-recognize a returning SSO user even if their email changes at the IdP.

Provider compatibility notes

A few provider-specific behaviors are handled in the dependency configuration so common IdPs work out of the box:

  • HTTPS to the IdP — the openidconnect 4.0.1 crate is built with the rustls-tls feature so the OIDC HTTP client can speak HTTPS.
  • Auth0 updated_at claim — Auth0 returns updated_at as an ISO-8601 string (rather than Unix seconds) on social/Google connections, so the crate is built with accept-rfc3339-timestamps to parse it.
  • Trailing slash in the issuer — the issuer URL is never trailing-slash-stripped, preserving OIDC Core §16.13 / RFC 8414 byte-equality. Auth0 publishes its issuer with a trailing slash, so stripping it would break discovery.

Debugging a failed discovery

Discovery is the first step of every login, and Test connection (the admin-only POST /api/admin/oidc/providers/{provider_id}/test) exercises exactly that path against the issuer without needing a real user to log in — run it after every config change. The endpoint returns success: false plus the underlying error message rather than throwing, so the failure reason is visible in the response. If discovery fails:

  1. Check the trailing slash. The issuer URL must match what your IdP publishes byte-for-byte (see above). Copy the full issuer URL straight from your IdP's OIDC setup documentation rather than retyping it, and use Test connection to confirm.
  2. Confirm the well-known document exists. Temps fetches the OpenID configuration at the issuer's /.well-known/openid-configuration path. Open that URL (issuer + that suffix) in a browser — if it 404s or returns HTML, the issuer URL is wrong.
  3. Watch for IdP-specific transport quirks. Some IdPs reject requests without certain headers (for example Accept-Encoding) or only serve discovery over HTTPS; the bundled client speaks HTTPS via rustls-tls and follows a 10s-timeout, no-redirect policy.

Security

OIDC SSO ships with hardening against account takeover, SSRF/DNS-rebinding, error leakage, and provider-ID enumeration. See Security Features → OIDC SSO Security for the full breakdown.

Next steps

  • Security Features — OIDC SSO hardening and the broader auth model
  • Teams — assign console access once users are provisioned
  • Admin Listener — keep admin/SSO management routes on a private interface

Was this page helpful?