Enterprise SSO
An Enterprise Connection is one IdP-side identity provider — SAML 2.0 or OIDC — wired into an environment so that a customer’s employees can sign in with their corporate credentials. authn.sh exposes both protocols through a single resource shape; consumers branch on protocol. Available from v0.6.
Use enterprise connections when your customer’s IT team manages user identity centrally (Okta, Azure AD / Entra ID, Google Workspace, OneLogin, Auth0 Enterprise, Keycloak, ADFS, …) and you don’t want to mirror every employee in a password table.
The model
Section titled “The model”One EnterpriseConnection row carries one IdP, either SAML or OIDC. The protocol field discriminates and is immutable after create — switching protocols means deleting the row and creating a fresh one so the IdP-side registration starts clean.
{ "id": "entcon_01HKX9SY9V7H7TF8C8K7J9X4ZA", "object": "enterprise_connection", "protocol": "saml", "name": "Acme Okta", "enabled": true, "organization_id": "org_01HKX9SY9V7H7TF8C8K7J9X4ZB", "domains": ["acme.example"], "default_role": "org:member", "attribute_mapping": { "email_address": "urn:oid:0.9.2342.19200300.100.1.3", "first_name": "urn:oid:2.5.4.42", "last_name": "urn:oid:2.5.4.4", "provider_user_id": "nameid" }, "saml_idp_entity_id": "https://idp.acme.example/saml/metadata", "saml_sso_url": "https://idp.acme.example/saml/sso", "saml_idp_certificate": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----", "saml_signing_algorithm": "RSA_SHA256", "saml_audience_uri": "https://acme.authn.sh/v1/saml/entcon_01HKX9SY9V7H7TF8C8K7J9X4ZA/metadata", "saml_acs_url": "https://acme.authn.sh/v1/saml/entcon_01HKX9SY9V7H7TF8C8K7J9X4ZA/acs", "saml_sp_entity_id": "https://acme.authn.sh/v1/saml/entcon_01HKX9SY9V7H7TF8C8K7J9X4ZA/metadata", "created_at": 1714723000000, "updated_at": 1714723000000}The fields fall into four buckets:
- Identity —
id,protocol,name,enabled.enabled: falsehides the connection from sign-in routing and rejects theenterprise_sso/samlstrategy; existingEnterpriseAccountlinks survive the toggle untouched. - Routing —
organization_id,domains,default_role. See Scope + Domains and sign-in routing below. - Protocol-specific config — the
saml_*block for SAML, theoidc_*block for OIDC. Exactly one block is populated; the other isnull-padded. - Mapping —
attribute_mapping(see Attribute mapping below).
Secrets are write-only
Section titled “Secrets are write-only”Two fields never appear in any GET response:
oidc_client_secret— accepted on POST / PATCH, encrypted at rest, never returned.saml_signing_key— PEM-encoded SP signing key for AuthnRequests / LogoutRequests. Accepted on POST / PATCH (or auto-generated by the server when omitted on create), encrypted at rest, never returned.
Operators rotate either by patching the row with a new value, or by sending null on PATCH to clear (and, for saml_signing_key, regenerate).
Scope: instance-wide vs org-scoped
Section titled “Scope: instance-wide vs org-scoped”A connection has exactly one scope, decided at create time and immutable thereafter:
organization_id | Scope | Who can use it |
|---|---|---|
null | Instance-wide | Any user in the environment whose email matches a workspace-verified domain in domains[]. |
org_… | Org-scoped | Only members (or first-time sign-ups whose domain matches a verified OrganizationDomain) of that org. |
Instance-wide is the right pick when your product is single-tenant and the customer is a single company. Org-scoped is what you want for B2B SaaS where each customer org runs its own IdP. The two coexist on the same environment — a workspace-wide Okta tenant for your own staff, plus per-customer connections for the customers who bring their own IdP.
default_role is the Role.key (e.g. org:member, org:admin) assigned to the OrganizationMembership that gets minted on JIT provisioning when the connection is org-scoped. Ignored for instance-wide connections, since there’s no org to mint a membership into.
Domains and sign-in routing
Section titled “Domains and sign-in routing”The domains[] array lists verified email domains that route to this connection. Each entry must already exist as a verified OrganizationDomain (for org-scoped connections) or a workspace-verified domain (for instance-wide). Sending an unverified domain returns 422 enterprise_connection_domain_unverified.
A SignIn enters the enterprise-SSO path in two ways:
- Implicit, domain-based. The user types their email on
<SignIn />. The server resolves the domain, finds the matchingEnterpriseConnection, narrowsSignIn.supported_strategiesto["enterprise_sso"], and populatesSignIn.enterprise_connection_id. The SDK then issues aChallengecarrying the IdP redirect URL. - Explicit, by connection_id. The SDK passes
strategy: "enterprise_sso"(or"saml"— they’re aliases) with a knownconnection_idon the Challenge create. Useful for “Sign in with Acme Okta” buttons where the email isn’t typed yet.
When a domain has more than one connection claiming it — rare, but legal during a migration — the SignIn surfaces all candidates and the SDK prompts. The two failure modes:
| Error | When it fires |
|---|---|
enterprise_sso_no_connection | The requested connection_id doesn’t exist, is disabled, or doesn’t claim the identifier’s domain. |
enterprise_sso_multiple_connections | Multiple connections match and the SDK didn’t pass an explicit connection_id. The error payload carries the candidate list. |
For the full enrollment-mode semantics — what happens after the IdP asserts the identity — see Verified domains and enrollment modes.
Server-computed URLs (register these with the IdP)
Section titled “Server-computed URLs (register these with the IdP)”Three URLs are computed by the server from the connection id and the environment slug. They appear on the resource as read-only fields, and operators paste them verbatim into the IdP-side admin console.
| Protocol | Field | Format |
|---|---|---|
| SAML | saml_acs_url | https://<env_slug>.authn.sh/v1/saml/<id>/acs — per-connection AssertionConsumerService URL. |
| SAML | saml_sp_entity_id | https://<env_slug>.authn.sh/v1/saml/<id>/metadata — SP EntityID; this URL also serves the SP metadata XML so the IdP can ingest it. |
| OIDC | oidc_redirect_uri | https://<env_slug>.authn.sh/v1/enterprise-sso-callback — shared across the env’s OIDC connections; the server identifies the connection via the OAuth state parameter. |
These are environment-stable — they don’t change unless the operator rotates the connection id (i.e. deletes and re-creates).
Attribute mapping
Section titled “Attribute mapping”attribute_mapping is a flat map from authn.sh field names (keys) to IdP claim or SAML attribute paths (values). The platform ships defaults that work for >90% of IdPs; operators only need to override when the IdP emits non-standard names.
{ "email_address": "email", "first_name": "given_name", "last_name": "family_name", "provider_user_id": "sub", "organization_role": "groups[0]"}The five standard keys:
| Key | Mapped to | OIDC default | SAML default |
|---|---|---|---|
email_address | User.email_addresses[].email_address | email | urn:oid:0.9.2342.19200300.100.1.3 |
first_name | User.first_name | given_name | urn:oid:2.5.4.42 |
last_name | User.last_name | family_name | urn:oid:2.5.4.4 |
provider_user_id | EnterpriseAccount.provider_user_id | sub | nameid |
organization_role | OrganizationMembership.role on JIT provisioning | — | — |
organization_role is the only mapping with no built-in default — supply it when you want the IdP to drive role assignment (e.g. “members of the eng-admins SAML group get org:admin”). Without it, JIT-provisioned memberships fall back to default_role.
Unmapped IdP claims / SAML attributes land on EnterpriseAccount.public_metadata as a free-form bag — handy for groups[], department, cost_center, and similar IdP extras you want to read server-side without mapping each one to a User field.
The persistent link: EnterpriseAccount
Section titled “The persistent link: EnterpriseAccount”When a user signs in through an enterprise connection for the first time, the server creates an EnterpriseAccount row that links the user to the connection. Every subsequent sign-in via the same connection reuses the link.
{ "id": "entacc_01HKX9SY9V7H7TF8C8K7J9X4ZA", "object": "enterprise_account", "enterprise_connection_id": "entcon_01HKX9SY9V7H7TF8C8K7J9X4ZA", "provider_user_id": "alice@acme.example", "email_address": "alice@acme.example", "verified": true, "public_metadata": { "groups": ["engineering", "sso-admins"], "department": "Platform" }, "linked_at": 1714723000000, "last_signed_in_at": 1714896500000, "created_at": 1714723000000, "updated_at": 1714896500000}What you should know:
provider_user_idis the IdP’s stable subject — the SAMLNameIDor the OIDCsub. Unique per connection.email_addressis what the IdP told us;verified: truemeans the IdP confirmed it (viaemail_verifiedfor OIDC, or a mapped verified-flag attribute for SAML). Whentrue, the resolver attaches the email to the parentUser.email_addresses[]automatically.public_metadatacarries everything that didn’t map to aUserfield.linked_atcaptures the first successful sign-in;last_signed_in_atis the most recent (nulluntil the second).- The IdP-issued OIDC
id_tokenis encrypted at rest and never appears in any payload. The SAML assertion XML is discarded after signature verification — only the parsed attributes survive onpublic_metadata. The SDK has no way to read either; sign-in resumption uses authn.sh’s own session cookie, not the IdP’s tokens.
A user can have multiple EnterpriseAccount rows when more than one connection asserts their identity (e.g. a contractor logged into both their own Okta and the customer’s Azure AD). The full list hangs off the User resource as User.enterprise_accounts[].
What’s next
Section titled “What’s next”- Per-org SSO setup walkthrough — the customer-side flow: an org admin adds a connection via
<OrganizationProfile />. - Verified domains and enrollment modes — how DNS-TXT domain verification feeds into SSO routing and JIT provisioning.
- SCIM 2.0 setup — directory sync from Okta, Azure AD, Google Workspace, or Rippling for full lifecycle automation (provision on hire, deprovision on offboard).
- Provisioning attribute mapping — the per-org override layer for non-standard SCIM attribute names.