Skip to content

Single sign-on for an app ​

The bridge signs a user in through the OIDC provider, then posts a signed SAML response to the app. It supports two flows: SP-initiated (the app sends an AuthnRequest) and IdP-initiated (the user starts at /idp/{slug}). Diagrams of both flows are on the Login flows page.

Endpoints ​

PathMethodPurpose
/saml/metadataGETThe bridge's IdP metadata: entity ID, SSO endpoints, and the signing certificate. Give this to each app.
/saml/ssoGET (HTTP-Redirect) or POST (HTTP-POST)Receives the app's AuthnRequest.
/idp/{slug}GETStarts an IdP-initiated sign-in for one app. Put this link in the app's portal or bookmark.
/saml/callbackGETThe OIDC redirect URI. Users never call it directly.
/healthzGETLiveness check.

The entity ID is {SAMLBRIDGE_PUBLIC_URL}/saml/metadata.

Flow ​

SP-initiated ​

  1. The app sends an AuthnRequest to /saml/sso. The bridge checks the issuer, the ACS, the destination, the binding, the time window, the signature when required, and that the request ID is new.
  2. The bridge redirects the browser to the OIDC provider with prompt=none. If there is no session at the provider, it asks again without prompt, so the user sees the sign-in page. ForceAuthn sends prompt=login. IsPassive never shows a sign-in page.
  3. The OIDC provider returns the browser to /saml/callback. The bridge checks iss and state, exchanges the code, and validates the ID token. All claims come from the ID token.
  4. The bridge posts a signed Response to the ACS. The response contains a signed Assertion. InResponseTo is the AuthnRequest ID.

IdP-initiated ​

  1. The user opens /idp/{slug}, optionally with ?RelayState=.... An unknown slug, a disabled app, and an app with IdP-initiated SSO turned off all get the same local error page.
  2. The bridge runs the same OIDC steps as SP-initiated, starting with prompt=none.
  3. The bridge posts a signed Response to the app's default ACS. There is no InResponseTo. The ACS, audience, and entity ID come only from the app's stored settings. The default ACS is the endpoint the metadata marks as default, or the lowest index.
  4. RelayState is sent only when it's on the app's allow-list. Otherwise it's dropped. A second RelayState parameter is refused.

An app can use one flow or both. Turn each on with apps set (see CLI).

What the app receives ​

  • Subject (NameID): the claim and format the admin sets with apps set. For example, sub in persistent format is the stable choice.

  • Attributes: only the ones the admin maps. Each value comes from an ID token claim. roles and groups produce one value per entry. A missing optional attribute is left out, and a missing required one refuses the login.

  • AuthnContext: from the amr value of the sign-in at the OIDC provider:

    amrAuthnContextClassRef
    pwd, or pwd mfa (a recovery code)urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
    pwd otp mfa (an authenticator app)urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract
    anything elseurn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
  • Timing: the assertion is valid for 2 minutes. AuthnInstant is the OIDC auth_time.

Allowed claims ​

Apps can map any claim name of letters, digits, and underscores, starting with a letter, 1 to 64 characters long. The name must come from the ID token of the OIDC provider. A name the provider doesn't send is handled like any missing claim: a required one refuses the login, and an optional one is left out.

Choosing a claim that users can change (for example email) as the NameID creates a new account at the app whenever the value changes. The CLI warns about this.

Refusals ​

  • Local error page (no response is sent anywhere): unknown or disabled app, an ACS that isn't registered, a replayed request, a missing or invalid pending login, a state or iss mismatch, or an invalid ID token. The bridge never redirects or posts to an unvalidated URL.
  • SAML error response to the app's ACS, once the ACS is validated, for SP-initiated logins only: a request the bridge refuses (for example, an unsigned request from an app that must sign), a sign-in that failed at the OIDC provider, a missing required claim, or an AuthnContext the request asked for and the sign-in doesn't meet (NoAuthnContext). IsPassive with no session gets NoPassive. An IdP-initiated login has no request to answer, so every refusal in its flow is the local page.

Signatures ​

  • The bridge signs both the Response and the Assertion with RSA-SHA256.
  • An AuthnRequest signature is verified when one is present, and it is required when the app's metadata has AuthnRequestsSigned="true". Only RSA-SHA256 is accepted. For HTTP-Redirect, the signature is checked over the raw query string. SHA-1 and ECDSA are refused.
  • The bridge doesn't verify signatures on the app's metadata. Only load metadata from a source you trust, and check the certificate fingerprint before confirming it (see CLI).

Replay and time windows ​

  • AuthnRequest IDs: each ID is accepted once, through the pending login. A pending login is taken and deleted by the callback, so the same login can't finish twice.
  • Assertion IDs: each assertion gets a new random 128-bit ID. The bridge doesn't keep a list of them. An SP that reuses an assertion is outside the bridge's control, so the SP must enforce its own replay checks.
  • Clock skew: 1 minute. An AuthnRequest's IssueInstant may be up to 5 minutes old, plus the skew, and up to the skew in the future. The window's edge is accepted.
  • Pending login: 10 minutes. A callback after that is refused.

Scopes requested from the OIDC provider ​

The bridge asks the OIDC provider only for the scopes the admin sets for each app with apps set --scope (see CLI). A scope name can be any name of 1 to 64 letters, digits, underscores, hyphens, colons, or dots. openid is always requested. With no list set, the default is openid, plus profile for preferred_username, name, given_name, or family_name, and email for email or email_verified. roles and groups are never part of the default.

The bridge doesn't check that a mapped claim's scope is requested. If the provider doesn't put a required claim in the ID token, the login is refused. Give the app the scope each of its claims needs.

Limits ​

  • Request size: a POST /saml/sso body larger than 64 KB gets 413 Payload Too Large and is not parsed.
  • No rate limit: the bridge doesn't limit request rates. Put the bridge behind a reverse proxy or firewall that does, if the endpoints are reachable from the internet.

Limitations ​

  • Single logout isn't supported. Signing out of the OIDC provider doesn't end app sessions, and signing out of an app doesn't end the OIDC session. Use short session lifetimes at the app.
  • Metadata is registered from a file and its signature is not verified. Trust comes from the admin confirming the certificate fingerprint.
  • Changing an app's metadata needs the app to be removed and added again (see CLI).