SSO & Identity Provider Overview
Federation is included with the product; there is no separate tier to unlock. IdP endpoints mount under /api/auth/idp/* and require a valid license only. The login and callback routes are reachable pre-auth (they are the login flow); the admin config routes (/api/auth/idp/config) require an authenticated admin. The federation layer is provider-agnostic at the point of role resolution: SAML, OIDC, and LDAP each terminate in the same downstream contract, a normalized identity (email, display name, group claim set) that is reconciled against the internal users table and issued a first-party session JWT.
Configuration & Persistence
SSO is configured under DB DevOps → Settings → Identity Providers. Saving PUTs to /api/auth/idp/config (admin only), which persists the config in the settings store with the OIDC/OAuth client secret and LDAP bind credential encrypted at rest (AES-256-GCM via ENCRYPTION_KEY), and applies it to the running server immediately, no restart. On startup the server loads the persisted config and initializes the enabled providers. The enableSSO master switch gates all three protocols; a provider is live only when both enableSSO and its own enabled flag are set. Secrets are never returned by the read endpoint; the settings form shows blank secret fields, and leaving one blank on save keeps the stored value.
The login screen renders an SSO button only for a provider that is actually configured, read live from /api/auth/idp/status. With nothing configured, only the local password form shows. Local login stays available even with SSO enabled, so an admin always has a break-glass path.
Provider Matrix
The auth_provider column on users records the terminal provider for each account. Recognized values: local (bcrypt password auth, the non-federated default), azure_ad, okta, google, aws_cognito, plus the protocol-level markers saml, oidc, and ldap. The three protocols below are the transport; the named providers are configured on top of them (Azure AD / Okta over either SAML or OIDC; Google and Cognito over OIDC; on-prem AD over LDAP).
| Protocol | Binding | Trust anchor | Entry endpoint |
|---|---|---|---|
| SAML 2.0 | Front-channel (HTTP-Redirect / HTTP-POST), signed assertions | IdP X.509 signing cert (assertion signature verification) | /api/auth/idp/saml/login → callback /saml/callback |
| OIDC | Authorization Code flow (back-channel token exchange) | clientId/clientSecret at the token endpoint | /api/auth/idp/oidc/login → callback /oidc/callback |
| LDAP / AD | Direct bind (no browser redirect); credentials posted to the SP | Service-account bind DN + LDAPS channel | /api/auth/idp/ldap/login (POST username/password) |
Identity Resolution Pipeline
Every successful authentication, regardless of protocol, converges on the same steps:
- Assertion / token / directory-entry validation. SAML verifies the assertion signature against
certand enforceswantAssertionsSigned; OIDC verifiesstate/nonceand exchanges the code at the token endpoint; LDAP performs the two-stage service-bind-then-user-bind (the user password is verified by the directory, never by the app). - Claim extraction. Email and display name are pulled via the provider's
attributeMapping; group membership is read from the group claim (SAMLgroupsattribute, OIDCgroupsclaim, or the LDAPmemberOfDN list). - Reconciliation (JIT). The identity is matched by email against
users. On miss, an account is provisioned; on hit,last_loginis updated. - Role resolution. Group claims are intersected with the configured
roleMappingto derive the effective role. - Session issuance. A first-party HS256 JWT (8h expiry) is minted. The IdP session and the Simcha session are decoupled: token lifetime is governed locally, not by IdP session TTL, and there is no back-channel single-logout.
JIT Provisioning Semantics
First-login provisioning sets auth_provider to the terminating protocol and marks email_verified = TRUE (the IdP is treated as the verification authority; no confirmation email is sent). Provisioning is idempotent on email as the natural key, an existing local account matched by the same email is adopted into the federated flow rather than duplicated. Critically, role is assigned on account creation and re-resolved on each login from live group claims, but a role explicitly set by an admin is authoritative: manual role changes are not clobbered by subsequent SSO logins. Deprovisioning is not automatic, disabling a user at the IdP blocks new sessions (the IdP refuses to assert) but does not revoke an unexpired local JWT; deactivate the account in-app to hard-cut access before token expiry.
Runtime Status Probe
GET /api/auth/idp/status reports which providers are wired without leaking secrets, use it in health checks and post-deploy smoke tests:
curl -s http://localhost:5002/api/auth/idp/status | python3 -m json.tool
{
"saml": { "enabled": false },
"oidc": { "enabled": false },
"ldap": { "enabled": false }
}
A disabled provider returns 503 from its login endpoint (<protocol> not configured), the provider block is absent or enabled: false. Because the login screen only renders buttons for providers this probe reports as enabled, a user should never reach a 503 from the UI.
Role Model
Four internal roles form a strict privilege lattice. Group-to-role config lives in the roleMapping object; matching is case-insensitive on the group name/claim, and a user in multiple mapped groups resolves to the highest role (Admin > Approver > Release Manager > User).
| Role | Config key | Default group | Grants |
|---|---|---|---|
| Admin | adminGroups | ["Admins"] | All operations, user/role management, system + IdP configuration |
| Approver | approverGroups | ["Approvers"] | Approve/reject change scripts in the multi-sig workflow |
| Release Manager | releaseManagerGroups | ["Release-Managers"] | Execute scripts, assemble and drive releases |
| User | (fallthrough) | any unmapped group | Author/edit scripts, request approvals, IDE access |
{
"roleMapping": {
"adminGroups": ["DB-Platform-Admins", "IT-Infrastructure-Admins"],
"approverGroups": ["DB-Change-Approvers", "DBA-Team-Leads"],
"releaseManagerGroups": ["DB-Release-Engineers", "DevOps-Team"]
}
}
Any claim set that intersects none of the mapped groups falls through to User. Because resolution is claim-driven, provision new privilege at the IdP (add the user to a mapped group) rather than in-app, the change takes effect on the user's next login.