id_token validation via JWKS, illustrative responses for each endpoint, expected error behaviors, and what to do with the token when it expires.
Prefer learning by example? The
cativa-sso repo has runnable reference apps. The login-com-cativa folder (Node + Express) implements every step in this guide end to end.Scenario
You have an app — could be a React SPA on a different domain, a Rails portal with server-side sessions, or a native iOS/Android app. Your community already lives on Cativa and you want users to log into your app with the same account they use on the community, without creating separate credentials. Cativa exposes a standard OIDC IdP per tenant. Any OIDC client library (oidc-client-ts, auth0/spa-js, next-auth, passport-openidconnect, AppAuth-iOS, AppAuth-Android) can talk to it from the tenant’s discovery URL.
Prerequisites
- An OAuth App registered in the Console — go to app.cativa.digital/admin/developers, OAuth Apps tab, click Create app. Save the
client_id(YOUR_CLIENT_ID) andclient_secret(YOUR_SECRET) — the secret is shown only once. - A redirect URI registered in the same app. You can add multiple (e.g. production + staging + localhost).
- The
customerName(tenant slug) — confirm with the community admin. It’s the tenant’s public Cativa subdomain.
Cativa SSO endpoints follow OIDC and are organized per tenant:
https://apis.cativalab.digital/tenant/api/v2/sso/{customerName}/.... The discovery document lives at https://apis.cativalab.digital/tenant/api/v2/sso/{customerName}/.well-known/openid-configuration and lists every endpoint and supported algorithm (S256 for PKCE, ES256 for id_token).The flow at a glance
The discovery document is the source of truth for the endpoints. A
GET on the tenant’s .well-known/openid-configuration returns something like this (illustrative response; full schema in the API Reference):Pick the right flow
- SPA with PKCE
- Node/Express backend
- Mobile with deep link
When to use: your app is a static frontend (React/Vue/Svelte) served by CDN, with no trusted backend to hold the Don’t persist the
client_secret. PKCE (Proof Key for Code Exchange) replaces the client secret with a verifier/challenge pair generated per login.1
Generate code_verifier and code_challenge
The
code_verifier is a random string that stays in the browser. The code_challenge is the SHA-256 of the verifier, base64url-encoded — that one goes into /authorize.2
Redirect to /authorize
Store the
verifier and state before leaving the page (you’ll need them in the callback).3
Exchange the code for tokens
Cativa redirects the user to
https://myapp.com/callback?code=...&state=.... Validate state and exchange the code for tokens.Since SPAs cannot hold a client_secret, /token accepts PKCE as proof: you send the original code_verifier (not the challenge) and Cativa recomputes SHA-256 and compares it with the code_challenge it stored in the previous step./token replies 200 OK with (illustrative response; full schema in the API Reference):4
Fetch the profile and start the local session
/userinfo replies 200 OK with (illustrative response):access_token in localStorage — it’s vulnerable to XSS. Keep it in memory (SPA state) or sessionStorage if you accept losing the session across tabs.Validate the id_token via JWKS
The id_token returned by /token is a JWT signed with ES256. You must validate the signature before trusting any claim (sub, email, etc.) — that prevents an attacker from substituting a forged token.
The public key for validation is published at the tenant’s JWKS:
id_token payload looks like this (illustrative):
Session and token refresh
Theaccess_token returned by Cativa SSO is short-lived (configured per OAuth App, default 1h). Pick the strategy that fits:
-
Refresh with
refresh_token(recommended) — everyauthorization_codeexchange already returns arefresh_token(opaque). Swap it for a freshaccess_tokenwhen the current one expires:The response (illustrative) carries a newaccess_tokenand a newrefresh_token— tokens are rotated, so persist the new refresh token and discard the old one (it is revoked on use): -
Silent re-login when it expires — when
expires_inreaches 0, redirect the user back through/authorize. If they still have an active Cativa session, the IdP returns a fresh code without prompting for a password again (single sign-on). -
Treat the token as a session bound to your app — store only the identity (
sub,email) on your session and issue your own JWT/cookie. The Cativaaccess_tokenis used only at login time.
Logout
There is currently no dedicated OIDCend_session endpoint for external OAuth apps. The recommended flow:
- On your side: drop the local session (delete cookie/session/Keychain), redirect the user to a “logged out” destination.
- Optional, if you also want to log them out of the Cativa community: redirect them to
https://{customerName}.cativa.digital/logout(replacing{customerName}with the tenant subdomain).
A standard OIDC
end_session endpoint for external partners is on our roadmap. When available, this page will be updated with cURL and a post_logout_redirect_uri example.Common errors
error: invalid_client — invalid client_secret
error: invalid_client — invalid client_secret
Check:
- You didn’t swap
client_idandclient_secret. - The secret hasn’t been rotated in the Console (generating a new one invalidates the old).
- There’s no extra whitespace/newline in the env var (this happens with
cat .envwhen the file came from Windows).
error: invalid_grant — PKCE verification failed
error: invalid_grant — PKCE verification failed
The
code_verifier sent to /token doesn’t match the code_challenge sent to /authorize. Common causes: you generated a new verifier before the callback (lost the original), you’re SHA-256-ing different bytes (UTF-8 vs ASCII), or you’re applying standard base64 instead of base64url.The user can sign in but the id_token fails my JWKS validation
The user can sign in but the id_token fails my JWKS validation
Confirm:
algorithms: ['ES256'](Cativa signs with ES256, not RS256).issuerexactlyhttps://apis.cativalab.digital/tenant/api/v2/sso/{customerName}(no trailing slash).audienceis yourclient_id.- Your library is fetching JWKS from the right issuer (and caching it, so you don’t hit the endpoint on every login).
Next steps
Badges as Permissions
How the
sub from id_token shows up in the community and how badges control what that user can access.Federated login (your own IdP)
The inverse flow: Cativa as the Relying Party, authenticating members through the company’s own IdP.
Quick Start: API Key
For server-to-server calls (no interactive user), use an API Key instead of OAuth.
Example app (GitHub)
Runnable reference implementations (Node + Express). The
login-com-cativa folder implements this exact flow: discovery, PKCE, token exchange and JWKS validation.