SSO & Identity Provider Overview
Federation is an Enterprise-tier capability. All IdP endpoints mount under /api/auth/idp/* behind requireTier('enterprise'); Starter and Professional licenses receive 403 on every route in this namespace regardless of correct provider config. 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.
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), distinct from the 403 tier gate, 503 means the license is fine but the provider block is absent or enabled: false.
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.