Skip to main content
CYPEX Documentation
Support
v2.0.0 Latest stable release View changelog ->

OIDC setup guide

10 min read

This is the hands-on companion to SSO (the architecture overview). It walks through adding a real provider, signing in through it, and approving the resulting user — using real screenshots captured against a Generic OIDC provider configured against Google, plus the JWT claim trace that applies to every provider identically.

Before you start

  • You need the Organizations administrator role for the target organization (or system admin).
  • The host running the SSO Gateway needs outbound HTTPS to the IdP’s discovery, authorization, token, and JWKS endpoints.
  • Register the CYPEX callback URL on the IdP side before you save the provider — the default is {app-origin}/auth/{providerType}/callback (e.g. http://localhost:4000/auth/oidc/callback in dev), and the gateway rejects a login whose stored callbackUrl doesn’t match the IdP’s registered redirect URI byte-for-byte.

Step 1 — Add a provider

Under Authentication → SSO Providers → Add provider:

New SSO provider — Identity and OIDC discovery

Organization, Provider type, and Display name under Identity; Issuer URL and Discover endpoints under OIDC discovery.

Organization, Provider type, and Display name under Identity; Issuer URL and Discover endpoints under OIDC discovery.

Three fields matter most:

  • Organization — which tenant this provider belongs to. A provider is always scoped to exactly one organization.
  • Provider type — the dropdown offers five: Generic OIDC, Generic OAuth2, Google, GitHub, Microsoft. Google and Microsoft are first-class shortcuts; every other spec-compliant IdP — Azure AD (if you don’t use the Microsoft shortcut), Keycloak, JumpCloud, Auth0 — goes through Generic OIDC. Each type has its own strategy in the SSO Gateway, so the five shipped types cover their vendors’ quirks; adding a further spec-compliant IdP is configuration only, through Generic OIDC, with every endpoint read from the row you’re creating — no new code to write or deploy.
  • Issuer URL — paste this and click Discover endpoints to auto-fill Authorization URL, Token URL, User info URL, JWKS URL, and End session URL from the IdP’s .well-known/openid-configuration document. All fields it fills stay editable, so you can override any single endpoint without losing the rest.

Known-good issuer URLs for Generic OIDC — this is the exact form used for the screenshots below, pointed at Google instead of the Google shortcut, to keep the walkthrough provider-agnostic:

ProviderIssuer URL
Googlehttps://accounts.google.com
Microsoft Entra ID (Azure AD)https://login.microsoftonline.com/{tenant-id}/v2.0
Keycloakhttps://{host}/realms/{realm}
JumpCloudhttps://oauth.id.jumpcloud.com/
Auth0https://{tenant}.auth0.com/

The login button’s icon is derived independently from the Issuer URL’s hostname (accounts.google.com, *.microsoftonline.com, *.jumpcloud.com all pattern-match to a known brand), not from which of the five dropdown options was picked — a Generic OIDC provider pointed at Google’s issuer still renders the Google “G” icon, exactly as in the screenshots below.

Scroll down for the endpoints, scope, callback URL, and identity-linking policy:

New SSO provider — Endpoints and Advanced

Callback URL defaults to the app-origin pattern above; Scope defaults to `openid profile email`. Auto-link by email is off by default.

Callback URL defaults to the app-origin pattern above; Scope defaults to `openid profile email`. Auto-link by email is off by default.

Auto-link by email decides what happens on a brand-new identity’s first successful login:

  • On — if the IdP asserts a verified email that matches an existing CYPEX user, the SSO identity is linked to that user silently, no admin action needed.
  • Off (the default) — every new identity lands in the pending-approval queue regardless of email match, until an admin explicitly approves it (Step 4 below).

A real filled-in example — Generic OIDC against Google, organization smi_org_admin:

New SSO provider — filled example

Client ID and Client secret are OAuth2 credentials issued by the IdP's own console; the secret is AES-256-GCM encrypted at rest and never shown again after Create provider.

Client ID and Client secret are OAuth2 credentials issued by the IdP's own console; the secret is AES-256-GCM encrypted at rest and never shown again after Create provider.

Clicking Create provider sends:

POST /sso/providers
{
  "organizationId": "1",
  "providerType": "oidc",
  "providerName": "SSO SMI",
  "issuerUrl": "https://accounts.google.com",
  "clientId": "<client-id>",
  "clientSecret": "<client-secret>"
}

→ 201 Created
{ "success": true, "provider": { "id": "<id>", "providerName": "SSO SMI", "isActive": true, ... } }

The provider now appears in the list, Active:

SSO Providers — list

Status, Last used, and row actions (test, view, edit, delete) per provider.

Status, Last used, and row actions (test, view, edit, delete) per provider.

Click the row’s test action any time to dry-run the configuration end to end — it re-fetches discovery and reports per-endpoint status (authorization_url, token_url, userinfo_url, jwks_url, client_credentials, oidc_discovery) without touching a real user session.

Step 2 — Sign in

On the CYPEX login page, local username/password stays available. Selecting the organization reveals its provider button:

Login — pick the organization

The SSO button only appears once an organization with an active provider is selected.

The SSO button only appears once an organization with an active provider is selected.

Login — provider button

One button per enabled provider for that organization.

One button per enabled provider for that organization.

Clicking it hands off to the IdP’s own login/consent screens — CYPEX has no control over this part of the flow, and neither the login form nor the consent screen ever sees the other’s credentials:

IdP — choose account

The identity provider's own account chooser, shown here for Google. Nothing on this screen is CYPEX UI.

The identity provider's own account chooser, shown here for Google. Nothing on this screen is CYPEX UI.

IdP — consent

Standard OAuth2 consent. Continue redirects back to CYPEX's callback URL with an authorization code.

Standard OAuth2 consent. Continue redirects back to CYPEX's callback URL with an authorization code.

Under the hood this is a PKCE-bound exchange, not a bare authorization-code redirect:

  1. The SPA generates a 32-byte random verifier, derives challenge = base64url(SHA-256(verifier)), and calls GET /auth/oidc?provider_id=<id>&code_challenge=<challenge>. The gateway stores {state, code_challenge} in Redis (10-minute TTL by default — configurable via the sso_oauth_state_ttl_seconds platform setting) and redirects to the IdP.
  2. The IdP redirects back to /auth/oidc/callback?code=…&state=…. The gateway exchanges the code for the user’s profile, stashes it in Redis under a one-shot opaque exchange_code (60-second TTL), and redirects the browser to /sso-callback?code=<exchange_code>. No database write and no JWT happen at this point — the callback URL is publicly reachable, so provisioning can’t be triggered by a forged callback alone.
  3. /sso-callback posts { code, code_verifier } to POST /auth/sso/exchange. The gateway atomically consumes the Redis blob, constant-time-compares SHA-256(verifier) against the stored challenge, and only then runs the user-provisioning step and mints tokens.
Info
PKCE is enforced per-provider (requirePkce, default true) — the steps above are what a new provider gets out of the box. A provider explicitly configured with requirePkce=false (a legacy-compatibility escape hatch, not the default path) skips step 3’s verifier check entirely.

Step 3 — First login without a role mapping

This provider has no t_sso_role_mapping rule yet, so the brand-new identity is parked, not signed in:

Login — pending approval

"Your identity was verified. Your account is pending administrator approval" — the IdP round-trip succeeded; CYPEX access did not.

"Your identity was verified. Your account is pending administrator approval" — the IdP round-trip succeeded; CYPEX access did not.

An admin sees it under Users → Pending SSO:

Users — Pending SSO tab

Email, external ID, provider, organization, a suggested role (when one can be derived), and how long ago the request came in.

Email, external ID, provider, organization, a suggested role (when one can be derived), and how long ago the request came in.

Rejecting instead of approving, re-opening a rejected identity, and the audit trail each action leaves are covered in full on Pending and rejected users.

Step 4 — Approve and assign a role

PATCH /sso/user-mappings/:id/approve
{ "cypexRole": "smi_org_admin" }

Approve SSO user?

Role is pre-filled from a matching t_sso_role_mapping rule, or the provider's default_role as a fallback hint — always editable before approving.

Role is pre-filled from a matching t_sso_role_mapping rule, or the provider's default_role as a fallback hint — always editable before approving.

Approving always provisions a fresh CYPEX user from the captured external profile — this identity has never existed in CYPEX before. The user signs in via the SSO provider only; no password is ever set for it. On the next attempt, the same identity signs straight in and lands on the dashboard:

Dashboard — after SSO sign-in

Signed in as the newly-provisioned user, role badge in the top-right matching the role chosen at approval.

Signed in as the newly-provisioned user, role badge in the top-right matching the role chosen at approval.

JWT claim mapping and the RLS trace

An SSO-issued token is not a special or reduced token — the exchange step uses the same JWT minting path as the rest of the platform, so it carries the identical claim set already documented in Organizations — The JWT tenant claim:

ClaimSource at SSO exchange timeRLS use
sub, user_idThe CYPEX user id provisioned (first login) or matched (returning login) by findOrCreateUserIdentifies the caller; not itself read by RLS policies
roleThe PostgreSQL role chosen at approval time (or inherited on subsequent logins)Grants/denies via normal PostgreSQL role privileges, same as local-auth users
org_idThe organization the provider belongs toRead by cypex.current_organization_id() — the single claim RLS row-filters on
organization_idsEvery organization the user can access (empty for system admins, who bypass org filtering entirely)Read by cypex.current_user_organization_ids()
isSuperAdmin / isOrganizationAdminComputed from the user’s role via computePlatformClaimsRead by cypex.is_admin() / cypex.is_organization_admin()
Warning
There is no separate tenant_id claim in CYPEX. org_id is the tenant identifier — it carries the organization’s bigint id, not a UUID, and it is the only claim RLS policies filter rows on. The gateway API transports the id as a JSON string; PostgreSQL casts it back to bigint inside the policy helpers. A provider’s own IdP may call its tenant-scoping claim something else (Entra’s tid, for instance) — that claim is read during provisioning to help resolve group/role mappings, but it is never itself forwarded into the CYPEX JWT or read by an RLS policy.

Nothing about this propagation path is SSO-specific: once the token is minted, PostgREST and every RLS policy treat it exactly like a token from local username/password login. SSO’s entire job is producing a token with the right org_id/role, once, at exchange time — see Organizations — How the JWT reaches PostgreSQL for the full request-time trace (SET LOCAL request.jwt.claims → the four RLS helper functions → row-filtered policies).

Sanitised request/response reference

Preview a provider’s endpoints before saving (no persistence):

POST /sso/providers/discover
{ "issuerUrl": "https://accounts.google.com" }

→ 200 OK
{
  "success": true,
  "endpoints": {
    "issuerUrl": "https://accounts.google.com",
    "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
    "tokenUrl": "https://oauth2.googleapis.com/token",
    "userinfoUrl": "https://openidconnect.googleapis.com/v1/userinfo",
    "jwksUrl": "https://www.googleapis.com/oauth2/v3/certs",
    "endSessionUrl": null
  }
}

Dry-run an existing provider’s configuration:

POST /sso/providers/<id>/test

→ 200 OK
{
  "success": true,
  "provider_id": "<id>",
  "test_results": {
    "authorization_url": "OK",
    "token_url": "OK",
    "userinfo_url": "OK",
    "jwks_url": "OK",
    "client_credentials": "OK",
    "oidc_discovery": "OK"
  }
}

client_secret is never present in any of these responses, including to the admin who owns the provider — after POST /sso/providers returns 201, the plaintext value is gone from every subsequent read.

Troubleshooting

SymptomCauseFix
sso_error=missing_pkce on /loginThe callback ran but the OAuth state carried no code_challenge — login was initiated by a stale SPA build or bypassed /loginRe-trigger sign-in from /login, not a bookmarked/direct IdP URL
sso_error=invalid_verifier on /login/sso-callback posted a verifier whose SHA-256 didn’t match the stored challenge — usually a different browser/tab than the one that started loginRestart sign-in in the same tab; check for privacy extensions clearing sessionStorage
sso_error=invalid_or_expired_codeThe one-shot exchange code was already consumed or its 60-second TTL elapsedNon-retryable — restart sign-in
sso_error=missing_code/sso-callback was opened without an opaque code, or sessionStorage had no verifierRestart sign-in from /login; don’t open /sso-callback manually
Discovery request times outThe gateway host can’t reach the IdPCheck egress firewall and DNS; in Docker, the sso_gateway container needs outbound HTTPS
Issuer mismatchThe IdP returned a different issuer than the one suppliedCopy the exact string from the IdP’s own discovery document, watching for a trailing slash
invalid_redirect_uri on loginThe stored callbackUrl doesn’t byte-for-byte match a redirect URI registered with the IdPFix either side — scheme, host, port, and path all matter
401 Unauthorized calling the admin API directlyThe caller’s JWT role isn’t cypex_admincypex_user is refused on create / update / delete / test / discover — sign in as an org or system admin
User stuck on “pending administrator approval”No t_sso_role_mapping rule matched, and Auto-link by email is off (or the email isn’t verified by the IdP)Approve the user under Users → Pending SSO, or add a role mapping rule so future logins from that group/role resolve automatically

See also