Docs
Open the console →
Guides

SAML SSO & SCIM provisioning

Configure SAML 2.0 single sign-on and SCIM 2.0 user/group provisioning for your organization, with IdP group-to-role mapping. Tested with Okta, Microsoft Entra ID and Google Workspace.

Cushy supports SAML 2.0 single sign-on and SCIM 2.0 provisioning as a bring-your-own-IdP feature, alongside OIDC. An Org Admin configures both on the Team screen. Group membership in your IdP drives each user's console role.

Tested identity providers

SAML + SCIM are built and tested against Okta, Microsoft Entra ID (Azure AD) and Google Workspace. The signature verification, claim handling and SCIM protocol accommodate the differences between them (see the matrix below).

SAML: what to hand your IdP

Cushy is the Service Provider (SP). Give your IdP admin these SP details (they are also shown on the Team screen):

FieldValue
SP entity IDhttps://<your-host>/api/auth/saml/metadata
ACS URL (HTTP-POST)https://<your-host>/api/auth/saml/acs
NameID formatemailAddress (persistent / unspecified also accepted)
SP metadataGET https://<your-host>/api/auth/saml/metadata — importable directly

SAML: configure the IdP side in Cushy

On Team → Single sign-on (SAML 2.0) → Configure, paste your IdP's metadata XML (the fastest path — we extract the entity ID, SSO URL and signing certificate for you), or enter them by hand: the IdP entity ID, the SSO URL (HTTPS), and the X.509 signing certificate (PEM or base64). Optionally set the email and groups attribute names, allowed email domains, and the default role for users whose groups don't map to anything. The certificate is validated when you save.

How assertions are verified

Every assertion's signature is verified with hand-rolled exclusive canonicalization (exc-c14n) + RSA-SHA256 against your configured certificate — no third-party XML library. We accept a signature on the Assertion (Okta's default) or the Response (Entra's default), reject anything unsigned, and guard against XML signature-wrapping. We also check NotBefore/NotOnOrAfter (±3 min clock skew), that the Audience equals our SP entity ID, InResponseTo for SP-initiated logins, and single-use replay by assertion ID.

SCIM: automatic user & group provisioning

On Team → SCIM 2.0 provisioning → Issue token, generate a per-organization bearer token (shown once) and copy the SCIM base URL (https://<your-host>/api/scim/v2). Point your IdP's SCIM connector at that base URL and authenticate with Authorization: Bearer <token>. Users are created/updated/deactivated automatically; pushing a user into an IdP group sets their console role via your mappings.

bash
# The SCIM endpoints your IdP will call (Bearer-authed, application/scim+json):
GET    /api/scim/v2/ServiceProviderConfig
GET    /api/scim/v2/Users?filter=userName eq "user@corp.com"
POST   /api/scim/v2/Users            # provision
PATCH  /api/scim/v2/Users/{id}       # e.g. {"op":"replace","path":"active","value":false} → deprovision
POST   /api/scim/v2/Groups           # push a group
PATCH  /api/scim/v2/Groups/{id}      # add/remove members → role applied
Deprovisioning

Setting a user active:false (Okta's soft-delete) OR issuing a SCIM DELETE deactivates the user: their sessions are killed immediately and they can no longer sign in. It is reversible — active:true reactivates them, keeping the same SCIM id.

If your IdP can't push (Keycloak)

SCIM requires the IdP to push. Keycloak has no built-in outbound SCIM client, so a Keycloak tenant should use Directory sync (pull) instead — Cushy reads the directory on a schedule with a read-only service account.

Group → role mapping

On Team → IdP group → role mapping, map an IdP group to one of the console roles (Org Admin, SRE · Cloud Admin, Deployer, Viewer). On both SAML login and SCIM provisioning, a user's highest-privilege mapped group wins; users with no mapped group get the SSO default role. The organization Owner and platform admins are never demoted by the IdP.

Entra ID sends group GUIDs

Microsoft Entra ID usually emits group object IDs (GUIDs) in its groups claim rather than names. Map the GUID string directly (paste it as the group name), and set the groups attribute to http://schemas.microsoft.com/ws/2008/06/identity/claims/groups. Okta and Google send group names under groups.

Tested-IdP matrix

IdPSignsGroups claimNotes
OktaAssertiongroups (names)Import SP metadata; SCIM soft-deletes via active:false; userName eq de-dup.
Microsoft Entra IDResponseschema-URL (GUIDs)Map group object IDs; SCIM uses PATCH and sometimes PUT; email in the emailaddress claim.
Google WorkspaceAssertiongroups (names)SAML SSO; SCIM via a supported connector; email from NameID.

Signing in

Members choose Sign in with SSO on the login page and enter their work email (or org slug). Cushy routes them to your OIDC or SAML IdP automatically. New users are just-in-time provisioned into your organization with their mapped role.

Signing out — SAML Single Log-Out (SLO)

Ending the Cushy session is only half a sign-out: your IdP still holds its own SSO cookie, so the next sign-in is silently re-issued for the same person with no password prompt. SAML Single Log-Out closes that. Paste your IdP's single-logout URL on Team → Single sign-on (SAML 2.0) (importing IdP metadata fills it in automatically when the IdP publishes one) and save.

Saving generates an SP signing keypair for your organization. Cushy needs one because a <LogoutRequest> is an assertion *we* make — without a signature anyone who learned a NameID could terminate your users' IdP sessions, which is why Okta, Entra and Keycloak refuse unsigned logout messages. The private half is encrypted at rest and never leaves the server; the public certificate is shown on the same screen and published in your org-scoped SP metadata.

Register at your IdPValue
SP metadata (org-scoped)https://<your-console>/api/auth/saml/metadata?org=<your-org-slug>
Single-logout service URLhttps://<your-console>/api/auth/saml/slo
Bindings acceptedHTTP-Redirect and HTTP-POST
SP signing certificateCopy it from Team → Single sign-on, or let the IdP read the metadata URL above

Both directions work once that is in place. You sign out of Cushy: the Cushy session is destroyed first and unconditionally, then the browser is sent to your IdP with a signed <LogoutRequest> carrying the NameID and SessionIndex from the original assertion; the IdP's <LogoutResponse> returns you to the login page. Someone signs out at the IdP (or at another application): the IdP sends Cushy a signed <LogoutRequest> and the matching Cushy session — that SessionIndex, or all of the person’s sessions when the IdP names none — is revoked, and Cushy answers with a signed <LogoutResponse>.

If your IdP has no logout endpoint

Single log-out is optional in SAML 2.0. Leave the field blank and sign-out ends the Cushy session only — the screen says so rather than implying more, and the audit trail records idp:saml-slo-unsupported instead of a success.

Limitations (honest)

  • SP-initiated and IdP-initiated SAML are supported, and so is Single Log-Out in both directions; encrypted assertions are not yet.
  • SLO travels over the HTTP-Redirect and HTTP-POST bindings. The SOAP back-channel binding is not implemented, and Cushy answers a POST-bound LogoutRequest over the redirect binding rather than rendering an auto-submitting form.
  • A Cushy sign-out ends the IdP session, not every *other* application's — that is the IdP's job, and how completely it does it is reported honestly (a PartialLogout status is never recorded as success).
  • The SP signing key is generated per organization and never rotated automatically; rotating it means re-installing the new certificate at the IdP.
  • Signature/digest algorithm is RSA-SHA256 (the Okta/Entra default), for logout messages as well as assertions. SHA-1 is intentionally not accepted.
  • SCIM filtering supports userName eq and externalId eq (what IdPs use for de-dup); complex filters are not implemented.
  • One SAML and one OIDC config per organization; email is globally unique across organizations today.