Skip to main content

SAML single sign-on

Experimental, off by default. SAML sign-in and SCIM provisioning sit behind the saml-scim beta feature: turn it on under Settings, Beta features or with APP_EXPERIMENTAL=saml-scim. While off, the routes answer 404, the settings page and the sign-in button are hidden, and a “require single sign-on” setting is not applied. The service provider is the sign-in library’s native SAML implementation; Levelrail adds a stricter response check, account policy and role mapping around it. It is tested against a signing test identity provider, not yet against a live Okta, Entra ID or Keycloak tenant (see What has not been verified).
Connect one or more SAML 2.0 identity providers (IdPs). Each enabled connection adds a button to the sign-in page. Directory groups can decide each person’s role, and you can require single sign-on for everyone except root users.

Before you start

  • An https dashboard URL. Set it under Settings, General. Identity providers post sign-in responses to it, and the SP entity ID and assertion consumer service (ACS) URL are built from it, never from the request’s Host header. When the dashboard URL changes, the connection’s URLs follow at its next use.
  • The control plane master key. The service provider signing key is generated at the first start with a master key and stored encrypted with it. Without one, SAML stays off and the settings page says so.
  • Root. Every SSO setting needs a root session or token.

Set up a connection

  1. Open Settings, Single sign-on and choose Add SAML connection. Give it the name people will see on the button and save it.
  2. Open Configure. Copy the three service provider values into a new SAML app at your IdP:
  3. Back in the sheet, paste the IdP metadata URL or XML and choose Import metadata. That fills the IdP entity ID, sign-in URL and signing certificate. When the metadata lists several signing certificates, the newest one valid today is kept.
  4. Pick who may sign in (see Accounts), turn on Show on the sign-in page and save.
A connection is saved in two halves: the IdP binding (entity ID, sign-in URL, certificate, attribute names) goes to the sign-in library as soon as it is complete, and the Levelrail half (name, enabled, allowed domains, account creation, default role, role sync) stays in Levelrail. An incomplete connection is kept as a draft and cannot be enabled. Once a connection is complete its IdP fields can be changed but not cleared; delete it and add a new one instead. Deleting a connection also removes every account link made through it. From the CLI:

What is checked on every sign-in

Sign-in is SP-initiated only: it always starts from the button on the sign-in page. Every response passes two checks, and the browser lands back on the sign-in page with a short error (and the reason goes to the control plane log and the audit log) if either fails. Levelrail’s check, before the library sees the response:
  • InResponseTo names the AuthnRequest this browser started, and the assertion carries a bearer confirmation for that same request. Each request ID is single use, expires after ten minutes, and is bound to the browser by a secure cookie. This is what refuses IdP-initiated, forwarded and replayed responses: the library on its own accepts IdP-initiated responses and does not check InResponseTo.
  • The assertion names this connection’s entity ID as its audience. An assertion without an audience restriction is refused.
  • The response carries a valid XML signature from the stored IdP certificate, the issuer is the configured IdP entity ID, Destination and the bearer Recipient are this connection’s ACS URL, NotBefore and NotOnOrAfter hold (three minutes of clock skew), and the response and assertion are no older than 90 seconds.
The library’s check, which then links the identity and issues the session:
  • The same signature, issuer, audience and time checks, plus: the assertion itself must be signed. A response that is signed only on the outer element is refused.
In-flight sign-ins are held in memory, so a control plane restart in the middle of one means starting again from the button. Encrypted assertions should work (the IdP can encrypt to the service provider certificate shown at the bottom of the settings page), but are not covered by a test yet.

Accounts and roles

A person is matched in this order:
  1. A subject (NameID) already linked to an account through this connection.
  2. An existing account with the same email. The subject is linked to it.
  3. A new account, only when Create accounts on first sign-in (--jit) is on. Without it, people need an account first (for example an invite).
Levelrail decides which of these applies and prepares the account before the library runs, then checks that the library signed in exactly that account. If anything after that fails, an account created by this sign-in is removed again, so a refused sign-in never leaves an account or a session behind. Allowed email domains restrict every sign-in, linked or not, to those domains. A disabled account is refused.

How this maps onto the library

The sign-in library groups SAML connections, SCIM tokens and directory users under organizations. Levelrail runs exactly one library organization, the instance, created at first start. It is the SAML and SCIM tenant and nothing else: Levelrail teams and their roles stay in Levelrail and are not library organizations.

Group to role mappings

Map IdP groups to stored roles under Group to role mappings, or:
The highest priority group the person belongs to decides the role. Group names match case-insensitively. A person in no mapped group gets the connection’s default role. With Apply the mapped role on every sign-in (--sync-roles), the role is re-applied at each sign-in, so a group change at the IdP takes effect at the next one. With sync on and no default role, a person in no mapped group is refused. The last root user is never demoted by a mapping, so directory drift cannot lock the instance out. The same mappings drive SCIM provisioning groups. The groups attribute defaults to http://schemas.xmlsoap.org/claims/Group, the email attribute to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress and the name attribute to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name. Set the attribute names on the connection when your IdP sends something else (for example email and groups).

Require single sign-on

Turn on Require SSO for non-root users (or levelrail-cli sso settings set --enforce=true). Password, passkey and email code sign-in are then refused for every user without the root ability. OAuth and OIDC sign-in stay available, since those are single sign-on too. It can only be turned on while at least one connection is enabled.

Break-glass access

Root users always keep password sign-in, so a broken IdP never locks the instance. Keep at least one root account with a strong password in your password manager. If that password is lost, reset it on the host:
See Emergency access for the host-side recovery routes.

IdP setup guides

These follow each IdP’s documented SAML setup. They have not been run end to end against these IdPs yet.

Okta

  1. Applications, Create App Integration, SAML 2.0.
  2. Single sign-on URL: the ACS URL. Check Use this for Recipient URL and Destination URL.
  3. Audience URI (SP Entity ID): the entity ID.
  4. Name ID format: EmailAddress. Application username: Email.
  5. Attribute statements: email = user.email, displayName = user.displayName. Set the connection’s email attribute to email and name attribute to displayName.
  6. Group attribute statements: name groups, filter Matches regex .* (or a narrower filter). Set the connection’s groups attribute to groups.
  7. Finish, then on the Sign On tab copy the Metadata URL and import it.

Microsoft Entra ID

  1. Enterprise applications, New application, Create your own application, then Set up single sign on, SAML.
  2. Basic SAML Configuration: Identifier (Entity ID) = the entity ID, Reply URL (ACS) = the ACS URL. Leave Sign on URL empty.
  3. Attributes & Claims: keep the default email claim, which matches the default email attribute. Add a group claim and set the connection’s groups attribute to http://schemas.microsoft.com/ws/2008/06/identity/claims/groups. Choose Cloud-only group display names to receive names, or put the group object IDs in your mappings.
  4. Copy the App Federation Metadata Url from the SAML Certificates section and import it.
  5. Assign users or groups to the application, or Entra refuses them.

Keycloak

  1. In the realm, Clients, Create client, type SAML, Client ID = the entity ID.
  2. Valid redirect URIs: the ACS URL. Master SAML Processing URL: the ACS URL.
  3. Keys tab: turn Client signature required off unless you upload the service provider certificate.
  4. Signature and Encryption: keep Sign assertions on. Signing only the document is refused.
  5. Client scopes, dedicated scope, Add mapper: a User Property mapper for email (SAML attribute name email) and a Group list mapper with attribute name groups and Full group path off. Set the connection’s attribute names to match.
  6. Import https://<keycloak>/realms/<realm>/protocol/saml/descriptor.

What has not been verified

  • Sign-in against a live Okta, Entra ID or Keycloak tenant. The tests sign responses with a local test IdP built on the same SAML library.
  • Encrypted assertions.
  • An IdP certificate rotation: a connection holds one signing certificate, so the new certificate has to be imported at the moment the IdP switches.

Troubleshooting

The control plane log and the audit log (sso.saml.rejected) record the exact reason. Assertion contents are never logged.

API

The sign-in library’s own SAML and organization routes are not exposed; only the routes above reach it.
Last modified on October 15, 2026