API reference: SSO
Cushy per-organization OIDC SSO configuration API: read, create/update (with server-side discovery and an encrypted secret), and enable or disable single sign-on.
Per-organization OIDC configuration. The entire surface — reads included — is org-admin only (manage_users_sso) and session-only. The client secret is encrypted at rest and never returned.
/api/org/ssoOrg Admin (session only)Return the org's SSO config (or null). The secret is never included — only a set flag.
{ "sso": { "type": "oidc", "issuer": "https://idp.example.com", "clientId": "cc-app",
"clientSecretSet": true, "defaultRole": "Viewer", "emailDomains": ["acme.io"],
"enabled": true, "updatedAt": "2026-07-01T10:00:00.000Z" } }| Status | When |
|---|---|
401 | Not signed in. |
403 | Caller lacks manage_users_sso. |
/api/org/ssoOrg Admin (session only)Create or update the OIDC config. Runs discovery against the issuer and stores resolved endpoints; enables SSO.
| Name | In | Required | Description |
|---|---|---|---|
issuer | body | yes | IdP issuer URL (https). |
clientId | body | yes | OIDC client id. |
clientSecret | body | no | Client secret (required on first save; blank keeps existing on update). |
defaultRole | body | no | Role for JIT users; defaults to Viewer. |
emailDomains | body | no | Up to 20 domains routed to this IdP. |
{ "issuer": "https://idp.example.com", "clientId": "cc-app", "clientSecret": "…", "defaultRole": "Viewer", "emailDomains": ["acme.io"] }{ "ok": true, "sso": { "…": "as GET" } }| Status | When |
|---|---|
400 | Non-https issuer, missing client id, bad role, invalid domains, missing first-time secret, or discovery failure. |
403 | Caller lacks manage_users_sso. |
503 | Secret storage is temporarily unavailable — retry. |
/api/org/ssoOrg Admin (session only)Enable or disable SSO without changing the config.
| Name | In | Required | Description |
|---|---|---|---|
enabled | body | yes | true or false. |
{ "enabled": false }{ "ok": true, "sso": { "enabled": false, "…": "…" } }| Status | When |
|---|---|
400 | enabled is not a boolean. |
403 | Caller lacks manage_users_sso. |
404 | SSO isn't configured yet. |
Staff console SSO (platform realm)
A SEPARATE surface for the platform's OWN identity provider — what /admin signs in against. Staff realm only (cc_staff; a customer session, even a platform admin's, gets 401) plus the SCP actions platform.security:read_sso / :manage_sso. See Staff console SSO.
/api/admin/security/ssoStaff realm · platform.security:read_ssoThe staff IdP config (or null), the redirect URI to register at the IdP, and the staff-role catalog. The client secret is never included.
{ "sso": { "issuer": "https://idp.snoweasl.com/realms/snoweasl", "clientId": "cushy-staff-console",
"clientSecretSet": true, "emailDomains": ["snoweasl.com"], "groupsClaim": "groups",
"roleMap": { "/cushy-platform-admins": "Platform Admin" }, "enabled": true },
"redirectUri": "https://…/api/admin/auth/oidc/callback", "staffRoles": ["Platform Owner", "…"] }| Status | When |
|---|---|
401 | No staff session. |
403 | Staff role lacks platform.security:read_sso. |
/api/admin/security/ssoStaff realm · platform.security:manage_ssoConfigure the staff IdP. Runs OIDC discovery server-side, stores the resolved endpoints, encrypts the client secret, and enables SSO.
| Name | In | Required | Description |
|---|---|---|---|
issuer | body | yes | https:// issuer URL; discovery must resolve. |
clientId | body | yes | The confidential client's id. |
clientSecret | body | no | Required on first save; omit to keep the stored secret. |
emailDomains | body | no | Allowed email domains (array or comma string). |
groupsClaim | body | no | ID-token claim carrying groups; required if roleMap is set. |
roleMap | body | no | { group: staffRole }. Non-empty ⇒ sign-in requires a mapped group. |
{
"issuer": "https://idp.example.com/realms/staff",
"clientId": "cushy-admin",
"clientSecret": "…",
"emailDomains": ["snoweasl.com"],
"groupsClaim": "groups",
"roleMap": { "cushy-platform-admins": "Platform Admin" }
}{ "ok": true, "sso": { "issuer": "https://idp.example.com/realms/staff", "clientId": "cushy-admin",
"authorizeUrl": "https://idp.example.com/realms/staff/protocol/openid-connect/auth",
"tokenUrl": "…", "jwksUrl": "…", "redirectUri": "https://cushy.example.com/api/admin/auth/oidc/callback",
"emailDomains": ["snoweasl.com"], "groupsClaim": "groups", "enabled": false, "clientSecretSet": true } }| Status | When |
|---|---|
400 | Non-https issuer, discovery failure, missing client id/secret, unknown staff role, or a role map without a groups claim. |
401 | No staff session. |
403 | Staff role lacks platform.security:manage_sso. |
503 | CC_DATA_KEY is not configured, so the secret can't be encrypted. |
/api/admin/security/ssoStaff realm · platform.security:manage_ssoEnable or disable staff SSO without changing the configuration.
{ "enabled": false }{ "ok": true, "sso": { "enabled": true, "clientSecretSet": true, "…": "…" } }| Status | When |
|---|---|
400 | enabled is not a boolean. |
404 | Staff SSO isn't configured yet. |
/api/admin/security/ssoStaff realm · platform.security:manage_ssoRemove the staff IdP configuration and its stored secret. Password sign-in is unaffected.
{ "ok": true, "sso": null }| Status | When |
|---|---|
404 | Staff SSO isn't configured yet. |
/api/admin/auth/oidc/startPublicAvailability probe for the staff login page. Returns only whether staff SSO is on — no issuer, no client id, no account information.
{ "enabled": true }| Status | When |
|---|---|
200 | Always — returns only { enabled }, never the issuer or client id. |
/api/admin/auth/oidc/startPublicBegin a staff SSO sign-in. Mints a single-use state/nonce/PKCE row stamped realm=staff and returns the IdP authorize URL.
{} // no body — state, nonce and PKCE are generated server-side{ "ok": true, "authorizeUrl": "https://idp.example/…/auth?…" }| Status | When |
|---|---|
404 | Staff SSO isn't configured or is disabled. |
/api/admin/auth/oidc/callbackPublic (IdP redirect)Verify the ID token against the STAFF issuer + client id, match an EXISTING platform admin by verified email, and set cc_staff. Never creates or promotes an account.
// 302 redirect — never JSON.
// success → Location: /admin (Set-Cookie: cc_staff=…)
// failure → Location: /admin/login?sso=failed| Status | When |
|---|---|
400 | Unknown/expired/replayed state, a state from the customer realm, an issuer/audience/signature/nonce mismatch, an unverified or disallowed email, no mapped IdP group, or no platform-admin account — all one generic message. |
429 | The shared staff lockout bucket is tripped. |