Skip to content

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.

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:

  • Identityid, protocol, name, enabled. enabled: false hides the connection from sign-in routing and rejects the enterprise_sso / saml strategy; existing EnterpriseAccount links survive the toggle untouched.
  • Routingorganization_id, domains, default_role. See Scope + Domains and sign-in routing below.
  • Protocol-specific config — the saml_* block for SAML, the oidc_* block for OIDC. Exactly one block is populated; the other is null-padded.
  • Mappingattribute_mapping (see Attribute mapping below).

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).

A connection has exactly one scope, decided at create time and immutable thereafter:

organization_idScopeWho can use it
nullInstance-wideAny user in the environment whose email matches a workspace-verified domain in domains[].
org_…Org-scopedOnly 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.

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:

  1. Implicit, domain-based. The user types their email on <SignIn />. The server resolves the domain, finds the matching EnterpriseConnection, narrows SignIn.supported_strategies to ["enterprise_sso"], and populates SignIn.enterprise_connection_id. The SDK then issues a Challenge carrying the IdP redirect URL.
  2. Explicit, by connection_id. The SDK passes strategy: "enterprise_sso" (or "saml" — they’re aliases) with a known connection_id on 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:

ErrorWhen it fires
enterprise_sso_no_connectionThe requested connection_id doesn’t exist, is disabled, or doesn’t claim the identifier’s domain.
enterprise_sso_multiple_connectionsMultiple 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.

ProtocolFieldFormat
SAMLsaml_acs_urlhttps://<env_slug>.authn.sh/v1/saml/<id>/acs — per-connection AssertionConsumerService URL.
SAMLsaml_sp_entity_idhttps://<env_slug>.authn.sh/v1/saml/<id>/metadata — SP EntityID; this URL also serves the SP metadata XML so the IdP can ingest it.
OIDCoidc_redirect_urihttps://<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 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:

KeyMapped toOIDC defaultSAML default
email_addressUser.email_addresses[].email_addressemailurn:oid:0.9.2342.19200300.100.1.3
first_nameUser.first_namegiven_nameurn:oid:2.5.4.42
last_nameUser.last_namefamily_nameurn:oid:2.5.4.4
provider_user_idEnterpriseAccount.provider_user_idsubnameid
organization_roleOrganizationMembership.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.

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_id is the IdP’s stable subject — the SAML NameID or the OIDC sub. Unique per connection.
  • email_address is what the IdP told us; verified: true means the IdP confirmed it (via email_verified for OIDC, or a mapped verified-flag attribute for SAML). When true, the resolver attaches the email to the parent User.email_addresses[] automatically.
  • public_metadata carries everything that didn’t map to a User field.
  • linked_at captures the first successful sign-in; last_signed_in_at is the most recent (null until the second).
  • The IdP-issued OIDC id_token is encrypted at rest and never appears in any payload. The SAML assertion XML is discarded after signature verification — only the parsed attributes survive on public_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[].