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.

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

- 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'semailandfullNamefields. - 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
emailandfullName. - 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.
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.ymlbindsSSO_ENC_SECRETto the Coolify magic variableSERVICE_PASSWORD_64_SSO, auto-generated on first deploy - you don't set it yourself. - Manual deploys: set
SSO_ENC_SECRETto a random 64-character string and treat it likeJWT_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.
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).

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.