Skip to content

OIDC federation

Federation lets an enterprise customer sign in with their own identity provider. Your application still talks to one authorization server; the platform brokers the login to the customer’s IdP behind it, verifies what comes back, and provisions the user into the tenant.

Your integration does not change. You call /authorize exactly as in the standard flow — the brokering happens on the other side.

Requires sso:write. With a discovery URL, the endpoints are fetched for you:

POST /v1/organizations/org_01J…/sso/connections
Authorization: Bearer sk_live_…
{
"type": "oidc",
"name": "Acme corporate IdP",
"oidc_discovery_url": "https://idp.acme.com/.well-known/openid-configuration",
"oidc_client_id": "",
"oidc_client_secret": "",
"domains": ["acme.com"]
}
{
"id": "conn_…",
"organization_id": "org_01J…",
"type": "oidc",
"name": "Acme corporate IdP",
"status": "active",
"oidc_issuer": "https://idp.acme.com",
"domains": ["acme.com"]
}

Without discovery, supply oidc_authorization_endpoint, oidc_token_endpoint, and oidc_jwks_uri explicitly. The client secret is stored encrypted and never returned.

The redirect URI to register at the customer’s IdP is the tenant’s SSO callback:

https://acme.oauth.work/sso/callback

Customers can create their own connection through an Admin Portal link rather than sending you their client secret — which is usually what their security team prefers.

Two ways to reach the connection:

Explicitly, by org:

GET /authorize?…&organization=org_01J…

Implicitly, by email domain:

GET /authorize?…&login_hint=ada@acme.com

The domain resolves to the org through a verified domain, and that org’s active connection takes over. This is what makes a single sign-in box work for many enterprise customers: the user types their email, and they land at their own IdP.

If the client is itself org-bound, its org is used and organization must match it — otherwise the request fails with invalid_request.

  1. The platform redirects to the IdP’s authorization endpoint with scope=openid email profile, a nonce, and PKCE on the brokered leg. IdPs that do not support PKCE ignore the extra parameters harmlessly.
  2. The IdP returns to /sso/callback.
  3. The state is consumed — one callback per brokered flow, so a replayed callback fails.
  4. The code is exchanged and the returned id_token is verified against the IdP’s JWKS.
  5. The user is provisioned just-in-time into the tenant, or matched to an existing record.
  6. The original /authorize flow resumes, and your application receives its code.

The result reaches you as an ordinary authorization code. The ID token you verify is signed by the tenant’s key, not the customer’s IdP — you verify one issuer regardless of how many customers federate.

Claim names differ between providers. Override the defaults per connection:

{
"attribute_mapping": {
"email": "mail",
"name": "displayName",
"given_name": "givenName",
"family_name": "surname"
}
}

Unmapped attributes fall back to the standard OIDC claim names. sub from the IdP is always stored as the user’s external id, so a user whose email address changes at the IdP still matches the same record.

A user arriving through federation is created in the tenant on first login, with the member role. The same user records back SCIM provisioning — both converge on (external_id, connection_id), so a directory that syncs users and an IdP that federates logins do not produce two accounts for one person.

Writes an sso_login audit event; connection creation writes sso_connection_created.

GET /v1/organizations/org_01J…/sso/connections # sso:read

Returns id, type, name, status, issuer or SAML entity id, and domains. Secrets and certificates are never included.

For customers whose identity provider speaks SAML rather than OIDC, the same connection resource takes type: "saml". See SAML 2.0.