orbotodocs
Admin guideIdentity & access

Single sign-on (OIDC and SAML)

Connect an OIDC or SAML 2.0 identity provider, enforce it per domain, and recover when it breaks.

orboto supports SSO via OIDC and SAML 2.0, configured entirely from Admin → SSO - no config-file edits, no REST calls needed.

Single sign-on providers

Add a provider

Admin → SSO → Add provider opens a form gated by protocol:

The add-provider form with the OIDC/SAML protocol toggle

  • OIDC needs the issuer URL, client ID and client secret from the IdP's app registration, the scopes to request (default openid profile email), and which claims map to orboto's email and fullName fields.
  • SAML 2.0 needs the IdP's entity ID, SSO URL and optional SLO URL, the IdP's signing certificate (PEM), an SP private key (PEM) to sign outbound requests and the metadata document, the NameID format, and which assertion attributes map to email and fullName.
  • Both protocols also take a label (shown on the login button), allowed domains, enforce for domains, and a default JIT role assigned on a user's first SSO login.

Added in v0.212.5 Saving changes to an existing provider asks for a recent MFA step-up, like the other sensitive admin actions.

Once created, expand the provider row to copy the ACS / redirect URI (OIDC callback or SAML ACS URL) and, for SAML, the SP metadata URL - paste these into the IdP's trust configuration. The expanded row also lets you edit the label, domain lists and default role in place.

Secrets are write-only after save. To rotate a client secret, IdP certificate or SP private key, delete the provider and re-create it with the new value - there's no in-place secret rotation yet.

Secrets at rest

Per-provider credentials - OIDC client secrets, SAML IDP certificates, SAML SP private keys - are sealed with AES-256-GCM before they touch the database. The key is derived from SSO_ENC_SECRET.

  • Coolify one-click install: docker-compose.yml binds SSO_ENC_SECRET to the Coolify magic variable SERVICE_PASSWORD_64_SSO, auto-generated on first deploy - you don't set it yourself.
  • Manual deploys: set SSO_ENC_SECRET to a random 64-character string and treat it like JWT_SECRET. Losing it means every stored provider config becomes unreadable and every SSO-bound user falls back to password login (which may be disabled for their domain).
  • Rotating it is destructive - every stored config becomes undecryptable, and affected providers show a red "configuration can't be decrypted" banner. Rotation procedure: announce a maintenance window, delete every affected provider in the admin UI, rotate the secret, restart the API, re-create each provider. Super-admin password login keeps working throughout (see below).

Enforcement model

Each provider has two domain lists:

  • Allowed domains - which email domains may use this provider. Empty = unrestricted.
  • Enforce for domains - which domains MUST use this provider. Users on these domains can't log in with a password at all.

enforce_for_domains is the lock-in feature for Business/Enterprise - it prevents "vanity accounts" where someone sets up a personal password on their company email. Without it, password login stays available alongside SSO as a fallback.

Super-admin bypass

Users with the global super-admin role are always exempt from enforce_for_domains - password login keeps working for them regardless of domain restrictions. This mirrors GitLab, GitHub Enterprise and Sentry: a local-credential path for the platform-root admin so a misconfigured IdP can't lock the whole system out.

Every super-admin is a "break-glass" account for SSO enforcement - audit who holds the role regularly, and don't issue it to operational staff just because it's convenient.

Linking existing accounts

Added in v0.212.5 Before the rules below, an OIDC provider's email_verified policy decides whether the identity provider's email_verified claim allows linking by email at all. Set it in the provider's Editable settings (or on the create form), or with PATCH /admin/sso/providers/:id and { "emailVerifiedPolicy": "..." }, which does not need the client secret again:

PolicyBehaviour
refuse_on_false (default, also for providers created before the setting)Link unless the claim is explicitly false. An absent claim links.
require_trueLink only when the claim is true. The strings "true" and "false" count as booleans. Use it for identity providers that always send the claim.
ignoreThe claim is never read. Use it only for identity providers that never send it and whose addresses you control.

orboto reads the claim from the ID token only. An identity provider that sends email_verified only from its userinfo endpoint counts as not sending it, so require_true refuses every link by email for that provider.

A refused link returns the user to the login page with the message that the identity provider has not confirmed the address, the login log records email_unverified, and the audit log records sso.link_refused with the reason idp_email_unverified. The policy applies only when a login would link to an existing account by email: an identity that is already linked and a newly provisioned account are not affected. SAML providers have no email_verified claim and no setting.

Added in v0.211.3 The first SSO login of an identity whose email matches an existing local account links that account, but only when the account has proved it owns the mailbox. Accounts that are already linked or provisioned through SCIM, super-admins, accounts created by an admin, accounts created by accepting an invitation from an admin, and accounts that completed a mailed password reset count as verified. They are linked and keep their sessions, API keys, second factors and password.

Added in v0.230.0 An account counts as provisioned through SCIM only when SCIM manages it: SCIM created it, an admin allowed SCIM to adopt it, or an admin handed it over on Admin → Users. An account that merely carries a SCIM externalId does not count. See SCIM provisioning.

An account that never verified its email (typically a self-registered one) is handled by the domain setting:

  • Domain enforced: the account is linked and everything it held is removed in the same step: sessions end, API keys are deleted (Added in v0.230.0 and the live connections opened with them close), OAuth grants are revoked (shown as "Removed when linking SSO" on the OAuth clients page), second factors and backup codes are removed, and the password is replaced by a random value.
  • Domain not enforced: the login is refused and the login page says the account has not confirmed its mailbox yet: reset your password through the emailed link, then sign in with SSO again. The login log records the refusal. The reset verifies the account, so the next SSO login links it. An admin can also add the domain to Enforce for domains.

An API key on an enforced domain follows the password rule: it is refused unless its account is already linked to an SSO provider or is a super-admin.

The login completes in the browser that started it

Added in v0.212.5 Starting an SSO login sets an httpOnly cookie in the browser, valid for ten minutes, and the callback signs the user in only when that browser presents it. A callback without it is refused with the "invalid or expired state" answer, no session is created and the login log records invalid_state. OIDC always applies the binding. SAML applies it when the public API URL is https (or a loopback host such as localhost), because the identity provider posts the response from another site and the cookie then needs the Secure attribute.

Added in v0.230.0 SAML needs https. A SAML sign-in only completes in the browser that started it, and that check needs a secure cookie, which browsers keep only on https (or on localhost during development). On a workspace whose API is reached over plain http on any other host, SAML sign-in is refused (at the start and at the callback) with "SAML sign-in needs the API served over https on this host", the login log records https_required, and saving or activating a SAML provider there writes one warning to the server log. Serve the API over https. OIDC providers are not affected. Before 0.230.0 such a SAML callback skipped the check (binding_skipped); it now fails closed.

Login log

Admin → SSO → Login log records every handshake - success, signature failure, or domain-mismatch rejection - filterable by provider. Check this first when a provider "isn't working": a spike of one error code usually points straight at the cause (see Troubleshooting below).

The SSO login log filtered to one provider, with success and failure rows

Troubleshooting

A single super-admin forgot their password

They use the normal forgot-password flow on the login page. Super-admins are domain-enforcement-exempt by design, so the reset email goes out even if every non-super-admin is SSO-enforced.

The super-admin's IdP went down mid-login

Their password login still works (see the bypass rule above). Sign in with email + password, check the provider's status in Admin → SSO, and toggle it inactive if you need to keep non-super-admins out until the IdP is back.

The super-admin also forgot their password (no self-service path)

Run the CLI recovery tool on the host (SSH to the API container or host machine):

pnpm --filter @orboto/api admin:reset-password <email>

This mints a single-use, 30-minute-TTL password-reset token and prints the reset URL to stdout. Forward it to the super-admin out-of-band (phone, Signal) - do not email it, since the point of this path is that the email system might itself be broken. The tool refuses to run for non-super-admin accounts and requires host access; there's no remote invocation path.

The only super-admin lost their password and can't reach the host

Recoverable only with direct database access:

UPDATE users
  SET password_hash = '$2a$12$<bcrypt-hash-of-temporary-password>'
  WHERE email = 'admin@example.com';

Generate the bcrypt hash separately (for example node -e "require('bcryptjs').hash('temp-pw', 12).then(console.log)"). After login, change the password immediately via Profile → Password. This is the last resort - reality doesn't guarantee anyone has both host access and a remembered password at the moment of panic.

A cert rotation or clock skew broke logins for one provider

Filter the login log to that provider. invalid_signature points at a rotated IdP certificate (re-import it); invalid_state usually means clock skew or an abuse attempt; domain_mismatch means the login email's domain isn't on that provider's allowed-domains list.

Deleting a provider

Deleting a provider unbinds every user tied to it (their row stays, just without an SSO identity) and immediately fails any in-flight login for that provider. JIT-provisioned users have a random password hash, so they can't fall back to password login until you send them a reset link - communicate that before deleting a provider that JIT-created accounts.

Licensing

No license gate ships on SSO today - every install can use it out of the box.

On this page