Organizations for B2B Single Sign-On OIDC & SAML
- New to Ory? Talk to the team about features and plans.
- Already a customer? Open a support ticket.
Ory Kratos uses organizations as a way to group users within one project. An organization always belongs to one Ory project. Within a project, an identity can belong to an organization or remain without an organization. Organizations contain multiple OIDC or SAML SSO connections. All members of an organization must use one of the organization's OIDC or SAML SSO connections to log in.
An organization can have multiple domains. Registrations for email addresses with a domain that belongs to an organization must go through one of the organization's OIDC or SAML SSO connections.
Some identity providers do not validate email domain ownership. This can lead to situations where Organization login is configured
for a specified domain such as @example.com, but due to this lack of this email domain ownership validation by the identity
provider, a users with other email domains such as @gmail.com can still authenticate successfully via the identity provider.
This will end up with the user being part of the configured organization in your Ory project, even if the domain does not match any of the configured domains.
Organizations require identifier-first authentication and two-step registration when using Account Experience 2.x or Ory Elements 1.x.
Manage organizations
- Ory Console
- API
To create, update, or delete organizations via the Ory Console, go to
Organizations → All organizations in the Ory Console.
Organizations are part of the project configuration. Read them with the getProject endpoint and change them with the patchProjectWithRevision endpoint. To authenticate your requests, create a workspace API key.
All examples below use curl to make HTTP requests and jq to parse JSON responses. You can use any HTTP client to make these requests.
Before copy & pasting the following examples, export your PROJECT_ID and WORKSPACE_API_KEY:
export PROJECT_ID="..."
export WORKSPACE_API_KEY="ory_wak_..."
Every change is applied to a specific revision of the project configuration, and every change creates a new revision. Fetch the current revision ID before each change:
export REVISION_ID=$(curl -s "https://api.console.ory.com/projects/$PROJECT_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" | jq -r ".revision_id")
If the project configuration changed after you fetched the revision ID, for example because someone edited it in the Ory Console,
the request fails with 409 Conflict instead of overwriting that change. Fetch the project again and retry with its new revision
ID. Updates and deletions address organizations and connections by their position in a list, and the change you conflicted with
may have moved them, so also look up the index again in the same response before retrying.
List organizations
curl -X GET --location "https://api.console.ory.com/projects/$PROJECT_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
| jq ".organizations"
Create an organization
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "add",
"path": "/organizations/-",
"value": {
"label": "Hello demo",
"domains": ["test.example.com"]
}
}
]' \
| jq ".project.organizations[-1]"
Leave out the id field. Ory Network assigns the organization ID, and the response includes it. Use that ID as $ORGANIZATION_ID
in the examples below.
Get an organization
curl -X GET --location "https://api.console.ory.com/projects/$PROJECT_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
| jq --arg id "$ORGANIZATION_ID" '.organizations[] | select(.id == $id)'
Update an organization
Patches address an organization by its position in the organizations list. Fetch the project once, and take both the revision ID
and the organization's index from that response:
PROJECT=$(curl -s "https://api.console.ory.com/projects/$PROJECT_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY")
export REVISION_ID=$(echo "$PROJECT" | jq -r ".revision_id")
export ORG_INDEX=$(echo "$PROJECT" | jq --arg id "$ORGANIZATION_ID" '.organizations | map(.id) | index($id)')
Then change the organization's fields at that index:
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "replace",
"path": "/organizations/'"$ORG_INDEX"'/domains",
"value": ["test.example.com", "other.example.com"]
}
]' \
| jq --arg id "$ORGANIZATION_ID" '.project.organizations[] | select(.id == $id)'
Delete an organization
An organization's SSO connections and SCIM clients can't exist without it, so remove them in the same request as the organization. The following builds that patch from the current configuration. It removes every OIDC connection, SAML connection, and SCIM client of the organization, highest index first so earlier removals don't shift later ones, and then the organization:
PROJECT=$(curl -s "https://api.console.ory.com/projects/$PROJECT_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY")
export REVISION_ID=$(echo "$PROJECT" | jq -r ".revision_id")
echo "$PROJECT" | jq --arg id "$ORGANIZATION_ID" '
def removals($path; $list):
[$list // [] | to_entries[] | select(.value.organization_id == $id) | .key]
| sort | reverse | map({op: "remove", path: "\($path)/\(.)"});
.services.identity.config as $c
| removals("/services/identity/config/selfservice/methods/oidc/config/providers"; $c.selfservice.methods.oidc.config.providers)
+ removals("/services/identity/config/selfservice/methods/saml/config/providers"; $c.selfservice.methods.saml.config.providers)
+ removals("/services/identity/config/scim/clients"; $c.scim.clients)
+ [{op: "remove", path: "/organizations/\(.organizations | map(.id) | index($id))"}]
' > delete-organization.json
Review delete-organization.json, then apply it:
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d @delete-organization.json \
| jq ".project.organizations"
Create SSO connections for an organization
After creating an organization, continue by adding one or more SSO OIDC connections.
- Ory Console
- API
Go to Organizations → All organizations in the Ory Console and continue by configuring the selected organization.
The SSO connections are part of the project configuration. To authenticate, use the same workspace API key as for managing
organizations, and fetch the current REVISION_ID before each change as described in
Manage organizations. The examples use curl to make HTTP requests and
jq to parse JSON responses.
Enable OIDC authentication
Organization OIDC connections only work when the OIDC method is enabled for the project:
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "replace",
"path": "/services/identity/config/selfservice/methods/oidc/enabled",
"value": true
}
]' \
| jq ".project.services.identity.config.selfservice.methods.oidc.enabled"
Create an SSO connection
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "add",
"path": "/services/identity/config/selfservice/methods/oidc/config/providers/-",
"value": {
"client_id": "...",
"client_secret": "...",
"id": "ory-GPx1yPH4",
"issuer_url": "https://example.org",
"label": "My OIDC provider",
"mapper_url": "base64://bG9jYWwgY2xhaW1zID0gc3RkLmV4dFZhcignY2xhaW1zJyk7IHsgaWRlbnRpdHk6IHsgdHJhaXRzOiB7IGVtYWlsOiBjbGFpbXMuZW1haWwgfSB9IH0=",
"organization_id": "6bb1c7d1-3b3e-4995-9e09-35649dc45a2b",
"provider": "generic",
"pkce": "auto",
"scope": ["openid", "offline_access", "email"]
}
}
]' \
| jq ".project.services.identity.config.selfservice.methods.oidc.config.providers"
Some notes on the fields of the JSON payload:
client_idandclient_secretare the credentials of the OIDC provider.idis a unique identifier for the connection, and part of the redirect URL, and can be a random string. This is$PROVIDER_IDbelow.organization_idis the ID of the organization to which the connection belongs, and part of the redirect URL. This is$ORGANIZATION_IDbelow.mapper_urlis the URL to a JSONnet file that maps the OIDC provider's claims to Ory's identity schema. You can use thebase64scheme to embed the JSONnet file directly in the JSON payload. The example encodeslocal claims = std.extVar('claims'); { identity: { traits: { email: claims.email } } }.pkcedetermines whether Ory Kratos Identities uses PKCE during the OIDC flow:auto,force, ornever. See the note below and the PKCE documentation for details.
The redirect URL to be set in the external OIDC provider's configuration is
https://$PROJECT_SLUG.projects.oryapis.com/self-service/methods/oidc/organization/$ORGANIZATION_ID/callback/$PROVIDER_ID.
If you set pkce: force, you must whitelist a different redirect URL with the OIDC provider:
https://$PROJECT_SLUG.projects.oryapis.com/self-service/methods/oidc/callback.
See the PKCE documentation for details.
List SSO connections
curl -X GET --location "https://api.console.ory.com/projects/$PROJECT_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
| jq ".services.identity.config.selfservice.methods.oidc.config.providers"
Result:
[
{
"client_id": "...",
"client_secret": "...",
"id": "ory-GPx1yPH4",
"issuer_url": "https://example.org",
"label": "My OIDC provider",
"mapper_url": "https://storage.googleapis.com/bac-gcs-production/94292215dbabe405bebff988b5356663fcf5cba35b10fc6ca2cfea7bc7049e906e01d271a3daeb844203efdfbf2cff356274daa63d5afc2a379bbe93b8b40e63.jsonnet",
"organization_id": "6bb1c7d1-3b3e-4995-9e09-35649dc45a2b",
"provider": "generic",
"scope": ["openid", "offline_access", "email"]
}
]
Delete an SSO connection
To delete an SSO connection, you need to specify the index of the connection in the path field. The index is the position of the
SSO connection in the list of connections.
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "remove",
"path": "/services/identity/config/selfservice/methods/oidc/config/providers/0"
}
]' \
| jq ".project.services.identity.config.selfservice.methods.oidc.config.providers"
Note that the organization_id field contains the organization ID to which the connection belongs.
See it live
After having set up everything, go to your registration page. Entering an email that ends with the organization's domain, such as
@test.example.com from the example above, shows a Sign in with <connection label> button instead of the password
field. Clicking it will take you to sign in with the SSO connection. The SSO connection is not visible for email addresses that
are not managed by the organization.
Pre-provisioning identities in an organization
After creating an organization, you can pre-provision identities in it. This is useful when you need to create related data in your system but need the identity to be created in Ory Network first, before the user logs in for the first time.
To achieve this, set the organization_id property to the ID of the created organization in the identity, either when creating
the identity, or by updating the identity's data using the Ory APIs.
To automate provisioning from an external identity provider instead, configure a SCIM client for the organization. The SCIM server creates and updates identities in the organization automatically. Because an identity can belong to only one organization, provisioning a user that already exists in a different organization fails with a conflict — see How SCIM relates to organizations for the full behavior and how to resolve it.
Bind an identity to an organization through the settings flow
A signed-in user can join an organization by linking one of its SSO connections through the
settings flow. When the user completes the link, Ory sets the identity's
organization_id to that organization. There is no need to sign the user out or to pre-provision the identity.
To scope a settings flow to an organization, pass the organization ID in the organization query parameter when you initialize
the flow:
https://$PROJECT_SLUG.projects.oryapis.com/self-service/settings/browser?organization=$ORGANIZATION_ID
The same parameter works for the API (native) flow at /self-service/settings/api.
Ory only honors the organization query parameter when one of the identity's email addresses is under the organization's
configured domains. This prevents any signed-in user from scoping a flow to an arbitrary organization and enumerating its SSO
connections. When no email address matches, the flow silently falls back to a regular, unscoped settings flow.
The check uses the identity's email verifiable addresses. An identity whose schema has no verification.via: email annotation has
no email verifiable address and therefore cannot use the organization query parameter. This domain check does not apply when the
identity is already bound to an organization — see below.
An organization-scoped settings flow behaves differently from a regular settings flow:
- The settings flow returns link nodes only for the organization's OIDC and SAML connections. Other settings methods, such as password or social sign-in, are not part of the flow.
- Connections the identity has already linked render as disabled. The organization owns the binding, so the user cannot remove it.
- Submitting a link starts authentication with the organization's identity provider. On the callback, Ory links the credential and
sets the identity's
organization_id. - Unlink submissions are rejected. To remove a member from an organization, use the identity admin API.
When the signed-in identity is already bound to an organization, that organization takes precedence over the organization query
parameter. An identity cannot be silently re-bound to a different organization through the settings flow.
If the identity's organization_id points to an organization that was deleted, the settings flow falls back to a non-organization
scoped flow. This keeps account recovery and self-service settings working for users who were offboarded from a deleted
organization.
Multi-factor authentication for organization members
Organization members can complete second factors such as TOTP or WebAuthn to reach the aal2 authenticator assurance level.
Before a second factor completes, Ory verifies that the session was authenticated through an OIDC or SAML SSO connection that is
currently configured for the member's organization. A session without that proof — for example one whose SSO connection was
removed or moved to a different organization since sign-in — cannot complete a second factor. The user must sign in through the
organization's SSO connection again first.
Organization ID in OAuth2 and OpenID Connect tokens
When an organization member signs in through an Ory OAuth2 / OpenID Connect flow using the hosted consent screen (Account Experience), the issued tokens carry the identity's organization ID:
organization_idin the ID token claims.organization_idin the access token session data, surfaced underextin JWT access tokens and promotable to a top-level claim.
Identities that don't belong to an organization get no such claim. For configuration details and behavior, see Organization ID claim.
SAML
SAML (Security Assertion Markup Language) is an XML-based open standard used for exchanging authentication and authorization data between parties. The SAML integration in Ory Network uses the B2B Organization feature.
SAML via Ory Network
This guide will walk you through the steps required to set up SAML Single Sign-On (SSO) with Ory Network.
Prerequisites
Before proceeding, ensure you are on a plan that supports SAML SSO. SAML is available exclusively on select Enterprise plans. Contact us if you need SAML support.
- Ory Console
- API
- Go to Organizations → All organizations in the Ory Console to create an organization.
- Select "Add a new Enterprise SAML SSO connection" and follow the instructions to configure the SAML connection. Fill out the following form fields:
- Label: A descriptive name for the SAML connection. This will be displayed to users.
- Data mapping: A mapping from the SAML attributes to Ory's identity schema.
- Raw IDP metadata XML: The XML metadata file from your SAML Identity Provider (IdP).
- After saving, copy the ACS URL and SP metadata URL shown in the provider details dialog. Use these values when configuring the SAML application in your Identity Provider.
- Navigate to your login screen to test the SAML connection.
Create an organization
Create an organization as described in Manage organizations, and note its ID. Fetch the current
REVISION_ID before each of the following requests.
Enable SAML authentication
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "replace",
"path": "/services/identity/config/selfservice/methods/saml/enabled",
"value": true
}
]' \
| jq ".project.services.identity.config.selfservice.methods.saml"
Create a SAML connection
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "add",
"path": "/services/identity/config/selfservice/methods/saml/config/providers/-",
"value": {
"id": "some-provider-id",
"label": "My SAML provider",
"mapper_url": "base64://...",
"raw_idp_metadata_xml": "base64://...",
"organization_id": "<your-organization-id>"
}
}
]' \
| jq ".project.services.identity.config.selfservice.methods.saml"
After creating the provider, configure your IdP with the provider's ACS URL. You can copy the ACS URL from the provider details
page in the Console, or construct it from the project configuration. The URLs below apply to providers on the default SAML engine
(Polis), which is used when the provider doesn't set engine or sets it to jackson. Providers on the native engine use
different URLs; see Native SAML engine.
By default, all providers of a project share one ACS URL:
- Default:
https://$PROJECT_SLUG.projects.oryapis.com/saml/api/oauth/saml - Custom audience base URL (
audience_override_base_url):https://$CUSTOM_DOMAIN/saml/api/oauth/saml - Proxy ACS URL (
proxy_acs_url):$PROXY_ACS_URL
When the project has per-provider ACS URLs enabled, a new provider gets its own ACS URL:
- Default:
https://$PROJECT_SLUG.projects.oryapis.com/saml/api/oauth/saml/$PROVIDER_ID - Custom audience base URL (
audience_override_base_url):https://$CUSTOM_DOMAIN/saml/api/oauth/saml/$PROVIDER_ID - Proxy ACS URL (
proxy_acs_url):$PROXY_ACS_URL, exactly as written
With a proxy ACS URL, your proxy redirects the SAML responses it receives to Ory; see Redirecting from a proxy ACS URL.
Replace $PROJECT_SLUG with your project slug and $PROVIDER_ID with the provider ID you assigned (the id field in the request
above, some-provider-id in the example; the Console API and the onboarding portal expose the same value as provider_id). Ory
identifies the project by the host of a per-provider ACS URL or, for a proxy ACS URL, by the host that your proxy redirects to.
This lets you use the same provider ID in several projects — for example a development and a production project connected to the
same Google Workspace.
Within one project, give every SAML provider its own provider ID. Providers that share one also share their callback URL and ACS
URL, and the configuration of one can be applied to the other. Sign-ins through one or both of them can then fail. The API
responds with 400 Bad Request to a change that adds a SAML provider with a provider ID that another SAML provider in the project
already uses. A project whose SAML providers already share a provider ID can still be saved, as long as the change doesn't add
another provider with that ID.
- id: The provider ID. It must be 4 to 64 characters from
A-Z,a-z,0-9,.,~,_and-. Treat it as permanent: changing it creates a new SAML connection. With per-provider ACS URLs, the provider ID selects the provider's route, so before users can sign in again, update the IdP application with the new ACS URL, or your proxy with the new redirect target. - label: A descriptive name for the SAML connection. This will be displayed to users.
- mapper_url: A mapping from the SAML attributes to Ory's identity schema.
- raw_idp_metadata_xml: The XML metadata file from your SAML Identity Provider (IdP).
Besides the ACS URL, configure the SAML application in your SAML Identity Provider with the service provider (SP) entity ID, also called audience URI. On the default engine (Polis), every provider of an organization uses the same SP entity ID, with or without per-provider ACS URLs:
- Default:
https://$PROJECT_SLUG.projects.oryapis.com/self-service/methods/saml/organizations/$ORGANIZATION_ID - Custom audience base URL (
audience_override_base_url):https://$CUSTOM_DOMAIN/self-service/methods/saml/organizations/$ORGANIZATION_ID
Shared and per-provider ACS URLs
On the default engine (Polis), the providers of a project share one ACS URL unless the project enables per-provider ACS URLs. With the shared ACS URL, Ory selects the connection by the entity ID of the IdP that sent the SAML response, so every provider in the project needs an IdP with a different entity ID.
Enable per-provider ACS URLs when several providers use the same IdP entity ID (for example Google Workspace, where one IdP serves
several SAML applications). Each new provider then gets its own route, /saml/api/oauth/saml/$PROVIDER_ID, and the route selects
the connection. Without a proxy ACS URL, the route is also the provider's ACS URL.
To enable per-provider ACS URLs, go to Project settings → Advanced in the Ory Console and turn on Per-provider SAML ACS
URLs. The setting is stored as the polis.per_provider_acs_url feature flag of the project and is disabled by default.
The setting applies when a provider is created. A provider keeps the ACS URL it was created with, so you can change the setting without updating the IdP application of an existing provider:
- A provider created while the setting is disabled stays on the shared ACS URL after you enable it.
- A provider created while the setting is enabled keeps its own ACS URL after you disable it. The provider's
acs_urlfield holds that URL.
If you show the ACS URL in your own SSO setup flow before the provider exists, derive it from the provider and the setting. If the
provider has a proxy ACS URL, the ACS URL is that URL, exactly as written. Otherwise, use the shared ACS URL when
polis.per_provider_acs_url is false, and the shared ACS URL followed by /$PROVIDER_ID when it is true.
Redirecting from a proxy ACS URL
With a proxy ACS URL (proxy_acs_url), the IdP posts SAML responses to a URL on a host you operate, and your proxy sends the
browser on to Ory. Redirect with HTTP status 307 or 308, so that the browser repeats the POST request with the SAML
response. With 302 or 303, the browser follows the redirect with a GET request and drops the SAML response. Redirect the
browser; don't pass the request on to Ory from your server.
Redirect a response to the route of its provider:
https://$HOST/saml/api/oauth/saml/$PROVIDER_IDwhen the provider is on its own route.https://$HOST/saml/api/oauth/samlotherwise.
$HOST is any host that routes to the project: its custom domain or $PROJECT_SLUG.projects.oryapis.com.
A provider is on its own route when it was created while Per-provider SAML ACS URLs was on, with a provider ID of 4 to 64
characters from A-Z, a-z, 0-9, ., ~, _ and -. Such a provider has an acs_url. The identity configuration
(GET https://api.console.ory.com/projects/$PROJECT_ID) shows it in
services.identity.config.selfservice.methods.saml.config.providers, and
GET https://api.console.ory.com/normalized/projects/$PROJECT_ID returns it in
current_revision.kratos_selfservice_methods_saml_config_providers. Ory sets acs_url, and a save doesn't change it, whatever
value the save sends. The Console shows an ACS URL for every provider, so it doesn't tell the routes apart.
A response to a login that Ory started has a RelayState that starts with boxyhq_jackson_. A response to an
IdP-initiated login has no RelayState, or one that your IdP sets. You can always redirect a response to
a login Ory started to the shared route, whatever URL it arrived at, because Ory finds the provider from the login session. That's
the only option for a proxy ACS URL that several providers share. An IdP-initiated response for a provider
on its own route fails on the shared route.
A new provider on its own route takes its proxy ACS URL, exactly as written, as its ACS URL. The ACS URL is fixed when the
connection is created: changing proxy_acs_url later doesn't change it. On the shared route, a changed proxy_acs_url takes
effect when you save it.
Every new or changed proxy ACS URL must meet these rules:
- It's an absolute URL with the
httporhttpsscheme. - Its host is an IP address or a domain name. Write an IPv6 address in brackets, in its shortest form (RFC 5952, such as
[2001:db8::1]), without a zone, and not as an IPv4-mapped address. A domain name consists of non-empty labels of ASCII letters, digits,-, and_, separated by dots, without a trailing dot, and its last label doesn't start with a digit. Write an internationalized domain name in punycode (xn--…). - If it has a port, the port is from 1 to 65535.
- Its scheme and host are lowercase, and a port has no leading zeros and isn't the scheme's default (
443forhttps,80forhttp). - It has no user name or password.
- It has no whitespace, control characters, or backslashes.
- It's at most 2048 characters long.
- Its path has no
.or..segments, including percent-encoded ones.
A save that breaks a rule fails with 400 Bad Request and changes nothing. The rules apply to every project. An empty value isn't
checked; a new provider on its own route treats it as no proxy ACS URL. An unchanged value isn't checked again. When you create a
project from a configuration, the rules apply to every proxy ACS URL and acs_url in it. When you create an onboarding portal
link, its proxy ACS URL must meet the same rules.
A configuration that you create a project from, or copy to another project, adds the providers the project doesn't have yet as new
providers. If one of them has additional ACS URLs, that project needs Per-provider SAML ACS URLs (the
polis.per_provider_acs_url feature flag) turned on, or the save fails with 400 Bad Request.
Additional ACS URLs
A provider on its own route can accept SAML responses addressed to further URLs besides its ACS URL. Use this when your proxy
receives the responses of one provider at several URLs. For example, a provider with the proxy ACS URL
https://auth.example.com/login?method=sso can also accept the IdP-initiated responses that its IdP sends
to https://auth.example.com/sso/$ORGANIZATION_ID. List up to 10 such URLs in additional_acs_urls. Change the provider index (0
in the example below) to match the provider's position in the providers array.
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "add",
"path": "/services/identity/config/selfservice/methods/saml/config/providers/0/additional_acs_urls",
"value": ["https://auth.example.com/sso/$ORGANIZATION_ID"]
}
]' \
| jq ".project.services.identity.config.selfservice.methods.saml"
- The list applies only to a provider on its own route. Saving a new or changed list for a provider on the shared route fails with
400 Bad Request, and so does saving a provider on the native engine with a non-empty list. - Ory compares the response's Recipient with each URL exactly as written, so the case of the path and a trailing slash must match what the IdP sends. Every new URL in the list must meet the rules for proxy ACS URLs above, so its scheme and host are lowercase and it has no default port.
- Your proxy redirects the responses it receives at these URLs to the provider's route, as it does for the proxy ACS URL.
- When you update the provider, leaving out
additional_acs_urls, or sendingnull, keeps the list, and an empty list ([]) removes it. A JSON Patchremoveofadditional_acs_urlsremoves the list, like[]. - Switching the provider to the native engine removes its list. The Console doesn't show the list, and editing the provider there keeps it.
- The SP metadata and the authentication requests of the provider name only its ACS URL (
acs_url). Add the additional URLs to the IdP application yourself, for example as further reply URLs.
SP entity ID
The SP entity ID identifies Ory as the SAML service provider. The IdP sends it as the audience of every SAML response, so the IdP
application must be configured with it. Each provider stores its SP entity ID in sp_entity_id. The Console shows it in the
provider details dialog, the identity configuration shows it in
services.identity.config.selfservice.methods.saml.config.providers, and
GET https://api.console.ory.com/normalized/projects/$PROJECT_ID returns it in
current_revision.kratos_selfservice_methods_saml_config_providers. A configuration that you copy to another project carries
sp_entity_id, so its providers keep this SP entity ID there. Leave out sp_entity_id to give them the default of that project.
A new provider gets a default when it's first saved:
- Default engine (Polis):
https://$PROJECT_SLUG.projects.oryapis.com/self-service/methods/saml, followed by/organizations/$ORGANIZATION_IDfor a provider of an organization. A custom audience base URL (audience_override_base_url) takes the place ofhttps://$PROJECT_SLUG.projects.oryapis.com, and a proxy SAML audience (proxy_saml_audience_override) takes the place of the whole value. - Native engine:
https://$PROJECT_SLUG.projects.oryapis.com/self-service/methods/saml/metadata.
To use another value, set sp_entity_id. Change the provider index (0 in the example below) to match the provider's position in
the providers array.
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "add",
"path": "/services/identity/config/selfservice/methods/saml/config/providers/0/sp_entity_id",
"value": "https://auth.example.com/saml"
}
]'
- A save that leaves out
sp_entity_id, or sendsnull, keeps the stored value. That includes a save that switches the provider to the other engine, moves it to another organization, changesaudience_override_base_url, or removesproxy_saml_audience_override. To change the value, setsp_entity_id. - A JSON Patch
removeofsp_entity_id, in the identity configuration or on the normalized project revision, keeps the stored value, because Ory fills it. - A new or changed
proxy_saml_audience_overrideon the default engine also setssp_entity_id, unless the same save changessp_entity_iditself. - A new or changed value must be at most 1024 characters long and must not contain whitespace or control characters. It doesn't
have to be a URL. A save that breaks a rule fails with
400 Bad Requestand changes nothing. An unchanged value isn't checked again. - Providers on the shared ACS URL whose IdPs have the same entity ID share one SAML connection, so they must have the same
sp_entity_id. A save that adds such a provider with another value, or changes the value of one of them, fails with400 Bad Requestand names the providers. - A provider that doesn't store an SP entity ID yet has no
sp_entity_idfield. Its SP entity ID is the default above, computed from its current settings, and the next save of the project stores it.
Native SAML engine
Providers that set engine to native are processed by Ory directly instead of by Polis. This engine is in preview and must be
enabled for the project first. Its SAML endpoints are derived from the project's canonical URL,
https://$PROJECT_SLUG.projects.oryapis.com, and the provider ID. They don't use your custom domain, and proxy_acs_url does not
apply. Configure the IdP with these URLs; custom-domain URLs don't match the ACS URL and SP entity ID that native SAML checks
responses against.
- ACS URL:
https://$PROJECT_SLUG.projects.oryapis.com/self-service/methods/saml/acs/$PROVIDER_ID - SP entity ID: the provider's
sp_entity_id. For a new provider, it'shttps://$PROJECT_SLUG.projects.oryapis.com/self-service/methods/saml/metadata. - SP metadata:
https://$PROJECT_SLUG.projects.oryapis.com/self-service/methods/saml/metadata/$PROVIDER_ID
The SP metadata document carries the ACS URL, the entity ID, and the signing certificate, so the simplest way to configure the IdP is to import it. The Console shows all three values in the provider details dialog.
Switching a provider between the default engine and the native engine changes its ACS URL and SP metadata, so the IdP application
has to be updated with the new values before users can sign in again. The provider keeps its SP entity ID. A
provider on its own route also keeps its acs_url and uses it again when you switch it back to the default engine. The native
engine doesn't use it, so the identity configuration doesn't show it while the provider is on the native engine. Switching a
provider to the native engine also removes its additional ACS URLs. With the API, remove
additional_acs_urls or set it to [] in the same update; a native provider with a list fails with 400 Bad Request.
The native engine sends authentication requests with the HTTP-Redirect binding when the IdP metadata lists it, and with HTTP-POST otherwise, like the default engine. To use a specific binding, set Binding in the provider's advanced settings.
The native engine trusts the signing certificates in the IdP metadata for their keys only and doesn't check their validity dates,
so an IdP that keeps signing with an expired certificate still works. To stop trusting a certificate, remove it from the IdP
metadata. Every SAML response must carry a NameID with a value, because the native engine identifies the user by it.
The native engine accepts responses signed with RSA-SHA256, ECDSA-SHA256, or RSA-SHA1, and assertions and NameIDs encrypted with
AES-GCM and RSA-OAEP key transport. It rejects AES-CBC encryption, so configure the IdP to use AES-GCM. Builds with the FIPS 140-3
module enabled don't accept RSA-SHA1 by default.
By default, every project update writes the SAML connections of all providers of the project. To write only the connections of the
providers an update changes, go to Project settings → Advanced in the Ory Console and turn on Write only changed SAML
connections. The setting is stored as the polis.saml_delta_sync feature flag of the project and is disabled by default.
With the setting turned on, a single project update, whether made in the Ory Console or with PUT or PATCH on the project, can
add, change, or remove at most 25 SAML providers in total. An update over the limit is rejected with a 400 Bad Request error and
changes nothing. To change more providers, split the change across several updates.
Data mapping
You can configure the mapping of SAML attributes to Ory's identity schema using Jsonnet. All custom SAML attributes defined at the
Identity Provider (IdP) will be available in the raw_claims object within claims. These attributes can then be mapped to the
Ory identity schema.
Ensure that the appropriate identity schema is created before mapping attributes. The email attribute is natively available in
claims.email, everything else is nested under claims.raw_claims.
Example SAML data mapping
This mapper sets only the email trait. It works with the preset email identity schema:
local claims = std.extVar('claims');
{
identity: {
traits: {
email: claims.email,
},
},
}
To also map firstName and lastName from the SAML Identity Provider, your identity schema must define firstName and
lastName traits. If it doesn't, Ory rejects the mapped traits with a validation error such as
additionalProperties "firstName", "lastName" not allowed and shows the user a registration form instead of signing them in. The
mapper sets each trait only when the IdP sends the attribute:
local claims = std.extVar('claims');
local rawClaims = std.get(claims, 'raw_claims', {});
{
identity: {
traits: {
email: claims.email,
[if std.objectHas(rawClaims, 'firstName') then 'firstName' else null]: rawClaims.firstName,
[if std.objectHas(rawClaims, 'lastName') then 'lastName' else null]: rawClaims.lastName,
},
},
}
Data mapping on the native engine
The data mapper of a provider on the native SAML engine can read three variables:
std.extVar('saml')holds the SAML assertion as Ory validated it. Use it for new mappers on the native engine.std.extVar('claims')holds the same assertion in the shape the default engine passes. A mapper written for the default engine works unchanged unless it reads a claim that only the default engine sets. These claims are listed below.std.extVar('identity')holds the traits and metadata of the identity. See Access the current identity in the mapper.
The saml variable has these fields:
| Field | Value |
|---|---|
subject | The value of the assertion's NameID. Ory identifies the user of the connection by this value. |
subject_format | The format of the NameID, for example urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress. |
issuer | The entity ID of the IdP. |
session_index | The SessionIndex of the assertion's authentication statement. |
attributes | Maps each attribute Name to an array of its untrimmed values. Attributes of the same Name are combined. |
The following mapper uses the NameID as the email address, so it requires an IdP that sends an email address as the NameID. If
your IdP sends an opaque identifier, read the email address from an attribute instead.
local saml = std.extVar('saml');
local attribute(name) = if std.objectHas(saml.attributes, name) then saml.attributes[name][0] else null;
{
identity: {
traits: {
email: saml.subject,
[if attribute('firstName') != null then 'firstName']: attribute('firstName'),
},
},
}
On the native engine, claims and claims.raw_claims don't contain iss, aud, exp, iat, nonce, requested,
polis_login_id, and idHash. claims.sub is the value of an attribute named id, otherwise the NameID, otherwise the
SHA-256 hash of the email address, as on the default engine. It can therefore differ from saml.subject.
The native engine reads the same mapper output as the default engine: identity.traits, identity.metadata_public,
identity.metadata_admin, identity.verified_addresses, and identity.region.
Update identity on login
By default, Ory runs the data mapper only the first time a user signs in through a SAML connection. To keep identity data in sync
with the SAML identity provider (IdP), set update_identity_on_login to automatic on the connection. With this setting, Ory
re-runs the connection's Jsonnet data mapper on every SAML login and updates the identity's traits and metadata whenever they
change.
Use automatic when the SAML IdP is the source of truth for attributes that change over time, such as group memberships, roles, or profile information.
The default is never. Existing SAML connections keep their current behavior until you opt in.
- Ory Console
- API
- Go to Organizations → All organizations in the Ory Console and open the organization that holds the SAML connection.
- Edit the Enterprise SAML SSO connection.
- Set Update identity on login to automatic.
To enable it, set update_identity_on_login on the connection you want to update. Change the provider index (0 in the example
below) to match your connection's position in the providers array.
curl -X PATCH --location "https://api.console.ory.com/projects/$PROJECT_ID/revision/$REVISION_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "replace",
"path": "/services/identity/config/selfservice/methods/saml/config/providers/0/update_identity_on_login",
"value": "automatic"
}
]' \
| jq ".project.services.identity.config.selfservice.methods.saml"
Access the current identity in the mapper
When you set update_identity_on_login to automatic, Ory passes the current identity to the data mapper as an additional variable
std.extVar('identity'). Use it to write conditional logic — for example, to preserve trait values a user has already set, or to
merge SAML IdP data selectively. The identity variable holds the identity's current traits, metadata_public, and
metadata_admin. The example treats a missing or null value as an empty object. To use it, your identity schema must define the
display_name and groups traits.
local claims = std.extVar('claims');
local identity = std.extVar('identity');
local orEmpty(value) = if value == null then {} else value;
local rawClaims = orEmpty(std.get(claims, 'raw_claims'));
local traits = orEmpty(std.get(identity, 'traits'));
// Keep the display name if the user has set one, otherwise use the value from the IdP (if any).
local displayName = std.get(traits, 'display_name', std.get(rawClaims, 'displayName', ''));
{
identity: {
traits: {
email: claims.email,
[if displayName != '' then 'display_name' else null]: displayName,
// Always update groups from the IdP.
[if std.objectHas(rawClaims, 'groups') then 'groups' else null]: rawClaims.groups,
},
},
}
Ory also passes the existing identity to the mapper during account linking. This happens when a user signs in through an SSO connection for the first time and both of the following are true:
- The mapper output doesn't satisfy the identity schema, for example because the identity provider doesn't send a required trait.
- The sign-in conflicts with an existing identity, for example because the email address is already registered.
Ory then evaluates the mapper a second time with std.extVar('identity') set to the existing identity. If the result satisfies
the identity schema, the user goes straight to the sign-in screen that confirms the link with an existing credential, and isn't
asked for the missing traits. Otherwise, Ory asks the user for the missing traits as before.
A mapper opts in by falling back to the existing traits:
local claims = std.extVar('claims');
local identity = std.extVar('identity');
{
identity: {
traits: {
email: claims.email,
// The identity provider doesn't send a name. Reuse the name of the existing identity when linking.
[if "name" in identity.traits then "name" else null]: identity.traits.name,
},
},
}
Linking only adds the SSO credential to the existing identity. It doesn't change the identity's traits, and Ory never shows the
existing identity's traits to the user before they confirm the link. A mapper that doesn't read std.extVar('identity') behaves
as before.
Behavior details
- Metadata preservation. When the mapper omits
metadata_publicormetadata_adminfrom its output, Ory preserves the existing values. When the mapper explicitly outputs{}, the field is cleared. - Schema validation. Before Ory persists the identity, it validates the update against the identity schema. If the mapper produces invalid traits, Ory fails the login with a validation error.
- No credential changes. This feature never modifies credentials. Addresses keep their verification state, and addresses the
mapper lists in
identity.verified_addressesare marked as verified.
Signing certificate expiry details
The SAML connection includes a valid_to attribute, which is an array of expiry dates for the signing certificates associated
with the connection.
Each date is returned in the same format as Node.js crypto.X509Certificate.validTo, matching OpenSSL’s textual representation:
MMM dd HH:mm:ss yyyy GMT
Example:
Jun 10 09:16:40 2038 GMT
You can use this information to externally monitor certificate expirations or integrate it into your internal workflows and alerting systems as needed.
Break-glass account recovery for SSO
When an organization's SSO provider is unavailable, organization members can't sign in. Break-glass account recovery lets designated users bypass SSO enforcement and recover access through email-based recovery.
Each recovery address has a break_glass_for_organization field that stores the organization UUID it applies to. Recovery is only
allowed when this field matches the identity's current organization. If the user moves to a different organization, the flag
doesn't carry over.
For Ory Network users, the easiest way to manage this is through the Ory Console SSO settings.
Set break-glass via the identity admin API
Set the break_glass_for_organization field on a recovery address to the organization's UUID:
curl -X PATCH "https://$PROJECT_SLUG.projects.oryapis.com/admin/identities/$IDENTITY_ID" \
-H "Authorization: Bearer $ORY_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "replace",
"path": "/recovery_addresses/0/break_glass_for_organization",
"value": "<your-organization-id>"
}
]'
To disable break-glass recovery, set the value to the nil UUID:
curl -X PATCH "https://$PROJECT_SLUG.projects.oryapis.com/admin/identities/$IDENTITY_ID" \
-H "Authorization: Bearer $ORY_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"op": "replace",
"path": "/recovery_addresses/0/break_glass_for_organization",
"value": "00000000-0000-0000-0000-000000000000"
}
]'
Grant break-glass recovery only to a small number of trusted users. Any user with this flag can bypass SSO and recover their account through email.
Self-service organization onboarding
- New to Ory? Talk to the team about features and plans.
- Already a customer? Open a support ticket.
The onboarding portal is a powerful tool designed to simplify complex identity and access management tasks for businesses and their partners.
Streamline organization onboarding
The onboarding portal simplifies setting up and managing identity connections with external organizations.
The onboarding portal aims to:
- Simplify complex integrations: A self-service or guided experience removes the need for client IT administrators to be SAML/SCIM experts.
- Reduce onboarding time: External administrators configure connections directly through a user-friendly interface instead of lengthy manual setup and back-and-forth communication.
- Enhance security and control: Shareable links don't expose sensitive information. Administrators configure their side securely while maintaining control over their IdP settings.
- Enable self-service: SaaS providers can let customers manage their own SSO/SCIM connections, freeing up engineering and support teams.
- Scale B2B integrations: Manual management becomes unsustainable as enterprise clients grow. The portal provides a scalable solution.
- Validate instantly: Test SSO connections immediately to verify integration before deployment, reducing troubleshooting time.
The onboarding is relevant for SaaS providers serving B2B or B2B2C customers, enterprise IT administrators managing identity providers, and developers building multi-tenant applications.
Provider support:
- The onboarding portal supports all well known providers. You can also connect any SAML/OIDC compliant provider using the Generic SAML/OIDC IdP option. If you don't find your provider listed, please contact Ory Support.
SCIM support
The onboarding portal allows you to set up SCIM provisioning. For an organization, you can create, update, and remove SCIM servers. Guided walkthroughs are available for Microsoft Entra ID, Google Workspace, and Okta.
IdP-initiated login
SAML connections support IdP-initiated login, where people start the login from their identity provider instead of from your application. Organization administrators turn it on for their connection in the onboarding portal, and project administrators can do the same in the Ory Console. It is off by default.
IdP-initiated login is inherently open to login CSRF: anyone who can reach the assertion consumer service can start a login for an account they control and try to complete it in another person's browser. Turn it on only for connections whose identity provider requires it, and make sure your application does not treat the first authenticated session after an IdP-initiated login as proof of intent for a sensitive action.
Manage onboarding portal links
- Ory Console
- API
Create a link
Go to Organizations → All organizations in the Ory Console and continue by configuring the selected organization.
On the Edit Organization page, under the Onboarding Portal section, click Generate link. If your project has a custom domain configured, you must choose between the project slug and the custom domain as the link's base URL. You can then share this link with the organization administrators.

Configure provider-specific data mappings
You configure data mappers in the Onboarding Portal by attaching a Jsonnet script to each provider that supports mapping (for example, social sign-in providers, SAML, SCIM).
The mapper must return an object like:
local claims = std.extVar('claims'); // or std.extVar('scim') for SCIM
{
identity: {
traits: {
// map incoming data to your identity schema traits
[if 'email' in claims then 'email' else null]: claims.email,
},
},
}
For SAML organizations, SAML attributes are available under claims.email and claims.raw_claims.
SAML mapping
For SCIM provisioning, the SCIM user object is available as std.extVar('scim') and you map it to the identity in the same way.
SCIM mapping
For social sign-in, all upstream data is available as std.extVar('claims'), including standard claims and any raw_claims for
non-standard fields.
Social Sign-In mapping

Onboarding portal links can also be managed using the Ory API. To authenticate your requests, create a workspace API key. All examples below use curl to make HTTP requests. You can use any HTTP client to make these requests. Before copy & pasting the following examples, export your PROJECT_ID,ORGANIZATION_ID and WORKSPACE_API_KEY:
Create an onboarding portal link
curl -X POST --location "https://api.console.ory.com/projects/$PROJECT_ID/organizations/$ORGANIZATION_ID/onboarding-portal-links" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expires_at": "2025-06-01T00:00:00Z",
"enable_sso": true,
"enable_scim": true,
"custom_hostname_id": "use a custom domain id from branding, omit the field otherwise",
"saml_mapper_url": "base64://{BASE64_ENCODED_JSONNET}"
}'
Result:
{
"id": "d36ad207-e1a8-4eac-bc1b-c25e223cc15b",
"value": "Fhca7DvuRLjQpDslMpYHMfsqSoy7vbI92PAxNlziZDidGBiW",
"project_id": "836b03f7-fdce-466a-9407-8595a0f615ce",
"organization_id": "517cc5e7-1ab6-4dcf-ba02-00fad003af92",
"enable_sso": true,
"enable_scim": true,
"created_at": "2025-06-02T05:23:45.299988Z",
"expires_at": "2025-06-03T05:23:45Z",
"custom_hostname_id": "aa4c8766-02b8-423a-80d2-47adf8e8c2ca"
}
The response contains a value token that you can use to construct the onboarding portal link as follows:
https://your-slug.projects.oryapis.com/onboarding/<value>
Update an onboarding portal link
curl -X POST --location "https://api.console.ory.com/projects/$PROJECT_ID/organizations/$ORGANIZATION_ID/onboarding-portal-links/$ONBOARDING_PORTAL_LINK_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expires_at": "2025-06-01T00:00:00Z",
"enable_sso": true,
"enable_scim": true
}'
Result:
{
"id": "655fa1d7-8404-4aa5-a90d-7f0aee060bc6",
"value": "emH8rO0f9S8VtLXLhBdNc9vBPSgsoT3qYHQQd7ROf8HwMmtK",
"project_id": "94d32bfd-08ea-4a11-a026-b9d7ddda0f09",
"organization_id": "9e83d579-ba29-43dd-b245-28c525c82066",
"enable_sso": true,
"enable_scim": true,
"created_at": "2025-08-21T15:48:00.101704Z",
"expires_at": "2027-06-01T00:00:00Z"
}
Delete an onboarding portal link
curl -X DELETE --location "https://api.console.ory.com/projects/$PROJECT_ID/organizations/$ORGANIZATION_ID/onboarding-portal-links/$ONBOARDING_PORTAL_LINK_ID" \
-H "Authorization: Bearer $WORKSPACE_API_KEY"
List onboarding portal links
curl -X GET --location "https://api.console.ory.com/projects/$PROJECT_ID/organizations/$ORGANIZATION_ID/onboarding-portal-links" \
-H "Authorization: Bearer $WORKSPACE_API_KEY"
Result:
{
"links": [
{
"id": "d6458578-dbeb-45ee-95e7-3b98168ff313",
"value": "eHSoZlTTffFP76ns0DOqBI8CT2daCXqRqO3MSVFi7eEbaRIu",
"project_id": "836b03f7-fdce-466a-9407-8595a0f615ce",
"organization_id": "517cc5e7-1ab6-4dcf-ba02-00fad003af92",
"enable_sso": true,
"enable_scim": true,
"created_at": "2025-06-02T05:44:24.883665Z",
"expires_at": "2025-06-03T05:44:24Z"
}
]
}
Expiry
The link generated from the Ory Network Console expires in 7 days. The API allows you to set an expiration value with a minimum expiry of 1 day. Once the link expires, it no longer works. You can also delete the link and generate a new one.
Handover and using the portal
Once you generate the link, share it with the organization's administrator. They can access this link multiple times until its expiration to manage SSO and SCIM connections.

Manage SSO

Manage SCIM
