Skip to content

Set up single sign-on (OIDC)

Point leancosts at your organization’s Keycloak (or any OIDC identity provider) so your team signs in with your IdP instead of magic-link email. SSO is configured per tenant, entirely through the app: no environment variables, no redeploy. It layers on top of magic-link, which stays as the bootstrap path until you choose to enforce SSO.

Everything below lives on Admin → Team → Sign-in (/admin?tab=team&sub=sso) and requires a tenant admin (the governance.admin capability).

In your identity provider (Keycloak realm, Okta, Auth0, …; for Entra ID see the Entra section below), create a confidential OIDC client for leancosts:

  • Redirect / callback URI: https://app.leancosts.com/api/auth/sso/callback/sso_<your-workspace-slug> (your workspace slug is the …/sso/<slug> segment of your sign-in URL). If your IdP enforces exact matching and rejects the sign-in later, copy the exact redirect_uri from that error and register it verbatim.
  • Scopes: openid email profile.
  • Client authentication: on (confidential): you’ll get a client ID and client secret.
  • Make sure the realm’s discovery document ({issuer}/.well-known/openid-configuration) is publicly reachable from the internet: leancosts probes it directly.

Keycloak is the tested path. In the Keycloak admin console, in your realm:

  1. Clients → Create client. Client type OpenID Connect, any client ID (e.g. leancosts).
  2. Capability config: turn Client authentication on and keep Standard flow on.
  3. Login settings → Valid redirect URIs: the callback URI from section 1.
  4. Credentials tab: copy the Client secret.
  5. Issuer URL for the leancosts form: https://<keycloak-host>/realms/<realm>. It must match the issuer value in the discovery document exactly (same path and case; a trailing slash is ignored).
  6. For role mapping (section 5 below): Clients → <client-id> → Client scopes tab → <client-id>-dedicated → Configure a new mapper → Group Membership. Token claim name groups, Full group path off (otherwise values arrive as /finops-admins), and Add to userinfo on. leancosts reads the claims from the userinfo endpoint, so a claim that is only in the ID token is not seen.
  7. Users must have Email verified set in Keycloak, or the sign-in is rejected while Require verified email is on.

Entra ID works as a per-tenant realm, with the specifics below. leancosts stores the endpoints it finds at save time and, because Entra’s userinfo endpoint lives on another host (graph.microsoft.com), reads the claims from the signed ID token instead.

In the Entra admin center, App registrations → New registration:

  1. Supported account types: this organizational directory only (single tenant).
  2. Redirect URI: platform Web, the callback URI from section 1.
  3. Certificates & secrets → New client secret: copy the Value, not the secret ID.
  4. Token configuration → Add optional claim → ID: add email and xms_edov. Accept the prompt that adds the Microsoft Graph permissions.
  5. For role mapping (section 5): Token configuration → Add groups claim, choose Groups assigned to the application, and tick ID.
  6. API permissions: Microsoft Graph delegated openid, email and profile (the defaults) are enough.

In the leancosts form, the Issuer URL is https://login.microsoftonline.com/<directory-tenant-id>/v2.0, with the Directory (tenant) ID from the app’s Overview page. Use the v2.0 URL (the xms_edov claim only exists there). common and organizations are rejected by Test connection (issuer mismatch). A good probe reads claims from the signed ID token · email verified by xms_edov.

Entra’s email claim is a plain directory attribute that Microsoft does not verify. leancosts therefore trusts the xms_edov claim (“email domain owner verified”) for Require verified email, and keys the identity on the issuer and subject, not the email.

  • A member whose mail is on a domain verified in your tenant is accepted.
  • A member whose mail is on an unverified domain is refused (AUTH_EMAIL_NOT_VERIFIED), and nothing is created.
  • A guest invited with a Gmail, Microsoft account or one-time-passcode address also gets xms_edov true and is accepted. Restrict this with the workspace’s allowed email domains.
  • A user without a mail attribute sends no email claim and cannot sign in.
  • If the xms_edov optional claim is not added, every new sign-in is refused. Turning off Require verified email is the less safe fallback.

Entra sends object IDs, not names. Paste each group’s object ID, in lower case, as the mapping value: matching is case-sensitive. Past 200 groups Entra sends a pointer instead of the list (group overage): leancosts then applies no role and records auth.oidc.group_overage in the audit log. A user who already had a mapped role keeps it; leancosts never downgrades from the claim. Groups assigned to the application keeps the list short.

Entra sovereign clouds, External ID and B2C are not covered by the xms_edov rule and fail closed. Front them with a Keycloak realm that brokers them.

Open Admin → Team → Sign-in and fill in the form:

FieldWhat to enter
Display nameThe label on the sign-in button, e.g. Acme SSO.
Client IDThe OIDC client you registered for leancosts.
Issuer URLThe realm base URL, e.g. https://kc.acme.com/realms/acme. Discovery runs against {issuer}/.well-known/openid-configuration.
Client secretPaste the secret. It is write-only: stored encrypted and never shown again (you’ll see •••• and a Replace secret button on later edits).
ScopesOptional; defaults to openid email profile.
Require verified emailOn by default: rejects sign-ins where the IdP didn’t assert email_verified (account-takeover defense). Turning it off is unsafe: a sign-in the IdP did not verify can create a user row for someone else’s address, and when that person later signs in by magic-link the earlier identity keeps working. Only turn it off if everyone who can authenticate at your IdP is trusted.

Click Connect realm. The realm is saved in status Draft.

Click Test connection. leancosts fetches your discovery document and reports Discovery OK (with the authorize and token endpoints, where sign-in reads the user’s claims, and the claim that proves the email) or an actionable error. A successful probe moves the realm to Tested.

Sign in once through the IdP at your workspace URL /sso/<your-slug> and pick Continue with {your display name}. A successful round-trip flips the realm to Proven and stamps the time. Only a proven realm can be enforced.

By default, every new SSO user lands pending until an admin approves them. To auto-provision instead, expand Role mapping and turn on Map IdP groups to roles:

  • Group claim: the OIDC claim carrying the user’s groups (the Group Membership mapper from the Keycloak steps emits groups).
  • Group → role mappings: map a group value (e.g. finops-admins) to a leancosts role. If a user is in several mapped groups, the highest-privilege match wins.

Once the realm is Proven, the Require SSO toggle unlocks. Turning it on disables magic-link for everyone in your tenant: they must sign in through your IdP.

New SSO users (without a matching role mapping) arrive pending. Approve them on Admin → Team → Members: approving assigns the role you choose and grants access. Until then they can sign in but see no data.

  • “Could not reach the issuer”: the discovery URL isn’t publicly reachable, or the issuer is wrong. Confirm {issuer}/.well-known/openid-configuration loads from a browser.
  • Sign-in rejected with a redirect_uri error: register the exact callback URL from the error in your IdP client (see step 1).
  • Sign-in rejected for unverified email: the IdP didn’t assert email_verified (Entra: xms_edov, see the Entra section). Verify the user’s email in your IdP, or (less safe) turn off Require verified email.
  • “Your email domain isn’t allowed here” (SSO_DOMAIN_NOT_ALLOWED): the workspace restricts sign-in to specific email domains and the user’s address is outside them.
  • “This account belongs to another workspace” (SSO_EXISTING_USER_OUTSIDE_ORG): an existing leancosts user who belongs only to other workspaces cannot be linked by a new realm. A workspace admin invites the account first, then it signs in.
  • “This sign-in isn’t set up for your workspace” (SSO_PROVIDER_UNRESOLVED): the sign-in link points at a realm that no longer exists or was never saved. Check Admin → Team → Sign-in and use the workspace’s current sign-in URL.
  • “We couldn’t find your account” (SSO_USER_NOT_FOUND): the IdP signed the user in but leancosts could not match the account. Try again; if it repeats, send the diagnostic code to support.
  • A realm user without an invitation sees no workspace: users whose email domain belongs to a workspace with an SSO realm never get a personal workspace of their own. They land pending in the realm’s workspace after their first SSO sign-in, or a workspace admin invites them.
  • Sign-in broke after the IdP moved its endpoints: leancosts stores the endpoints it found when the realm was saved. Open the realm and save it again (or press Test connection to preview) after the IdP changes its token or key URLs. A save while the IdP is unreachable keeps the stored endpoints, and is refused when the realm is proven and the issuer changed.
  • Can’t turn on Require SSO: the realm isn’t Proven yet. Complete one real SSO sign-in first.