Skip to content

Quick Start: SSO & SCIM

These two are independent of the bootstrap-key/human-registration setup in Authentication — reach for them once an org wants its identity provider (Entra ID, Okta, Google, …) doing the login and/or user-provisioning work instead of local passwords and manual invites.

Each organization can connect its own OIDC identity provider — Entra ID, Google, Okta, or any other OIDC-compliant IdP — so its members sign in without a password. An org admin sets this up from the dashboard’s SSO settings page (or directly via the API):

Terminal window
curl -X POST http://localhost:8000/orgs/{org_id}/sso \
-H "X-API-Key: change-me-to-a-strong-secret" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Acme Corp",
"issuer": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"client_id": "<app-registration-client-id>",
"client_secret": "<app-registration-client-secret>",
"default_role": "member",
"allowed_domains": ["acme.com"]
}'

From Entra ID: register an application, grant it the openid/email/profile scopes, and set its redirect URI to {your-api-base-url}/auth/sso/callback.

A member then signs in either of two ways:

  • From the login page — enter your email and select “Continue with SSO.” The dashboard calls POST /auth/sso/discover to find your org from your email’s domain (via allowed_domains), then redirects to GET /auth/sso/{org_id}/start.
  • Via a direct link — GET /auth/sso/{org_id}/start works without the discovery step, for orgs that share the link directly.

Either way, the browser is redirected to the IdP, authenticates there, and is redirected back to GET /auth/sso/callback, which completes the login and sets the same session cookie a password login would. The first SSO login for an email that doesn’t yet have an account provisions one automatically at default_role; an email that already has a local-password account is linked to it instead (the password keeps working too) rather than creating a duplicate.

default_role can never be owner — same reasoning as invitation links: an automatically-granted role must never hand out full ownership. allowed_domains is optional; if set, no other organization’s connection can claim the same domain, and login is rejected for any email outside it.

A claimed domain also takes over the platform’s Google / Microsoft sign-in buttons, if the deployment offers them: an address at that domain choosing Continue with Google is sent into this org’s SSO connection instead. So members can’t get round the org’s identity provider with a personal account on the same address.

An SSO session satisfies the organization’s require 2FA policy on its own — the IdP owns the second factor, and TOTP enrollment is only available to password accounts.

SSO above covers authentication — how a member logs in. SCIM 2.0 provisioning covers lifecycle — Entra ID (or any SCIM-compliant IdP) pushing users and groups into an org on its own schedule, independent of anyone actually logging in. The two are independent: an org can use either, both, or neither.

From the dashboard’s SSO page, enable SCIM to get a tenant URL and a bearer token (shown once):

Terminal window
curl -X POST http://localhost:8000/orgs/{org_id}/scim \
-H "X-API-Key: change-me-to-a-strong-secret" \
-H "Content-Type: application/json" \
-d '{
"default_role": "member",
"group_role_mappings": [
{"pattern": "^admin", "role": "admin"}
]
}'
# → {"token": "..."} — copy it now, it is never shown again

In Entra ID: open the Enterprise Application → Provisioning → set mode to Automatic → Tenant URL is the base_url from GET /orgs/{org_id}/scim (e.g. https://your-api/scim/v2/{org_id}) → Secret Token is the value above. Assign users/groups to the application and Entra provisions them on its own ~40-minute cycle (or on demand via “Provision on demand”).

What happens on each side:

  • Users — a SCIM User links to an existing local account by email (same linking rule as SSO) or creates one; PATCH .../Users/{id} with {"active": false} deactivates the org membership (Entra does this automatically when you unassign someone), active: true reactivates it.
  • Groups — a SCIM Group maps 1:1 to a Team; creating, renaming, or deleting a group does the same to the Team. Assigning members to the group syncs Team membership.
  • Group → role mapping — group_role_mappings is an ordered list of {pattern, role} rules, each pattern a regular expression matched against a user’s current SCIM group names. The first rule that matches any of them wins; if none match, the config’s own default_role applies. role (and default_role) can never be owner — same guardrail as SSO’s default_role. This mapping is a deliberate, guarded exception to Team’s own “no access-control effect” design — recomputed only by SCIM group-membership changes, never by ordinary dashboard Team management, and never applied to a member currently at owner (SCIM can never demote or remove the org’s owner, no exception).

The bearer token is a separate credential from a regular API key (/auth/keys) — it only authorizes the /scim/v2/{org_id}/* surface, nothing else, and can be rotated (POST /orgs/{org_id}/scim/rotate-token) or revoked (DELETE /orgs/{org_id}/scim) independently of SSO.

See Authentication for the key/session/role basics, and API Endpoints — SSO & SCIM for the full endpoint reference.