Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.cativa.digital/llms.txt

Use this file to discover all available pages before exploring further.

The Quick Start covers the happy path in 5 steps. This guide is the deep version: three implementation patterns (SPA, traditional backend, native mobile), id_token validation via JWKS, expected error behaviors and what to do with the token when it expires.

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

  1. An OAuth App registered in the Console — go to app.cativa.digital/admin/developers, OAuth Apps tab, click Create app. Save the client_id and client_secret (the secret is shown only once).
  2. A redirect URI registered in the same app. You can add multiple (e.g. production + staging + localhost).
  3. 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/social/v1/sso/{customerName}/.... The discovery document lives at https://apis.cativalab.digital/social/v1/sso/{customerName}/.well-known/openid-configuration and lists every endpoint and supported algorithm (S256 for PKCE, ES256 for id_token).

Pick the right flow

When to use: your app is a static frontend (React/Vue/Svelte) served by CDN, with no trusted backend to hold 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.
function base64UrlEncode(arrayBuffer) {
  const bytes = new Uint8Array(arrayBuffer);
  let str = '';
  for (const b of bytes) str += String.fromCharCode(b);
  return btoa(str)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

function generateRandomString(length = 64) {
  const bytes = new Uint8Array(length);
  crypto.getRandomValues(bytes);
  return base64UrlEncode(bytes);
}

async function sha256Base64Url(input) {
  const encoded = new TextEncoder().encode(input);
  const hash = await crypto.subtle.digest('SHA-256', encoded);
  return base64UrlEncode(hash);
}

2. Redirect to /authorize

const verifier = generateRandomString(64);
const challenge = await sha256Base64Url(verifier);
const state = crypto.randomUUID();

sessionStorage.setItem('cativa_pkce_verifier', verifier);
sessionStorage.setItem('cativa_oauth_state', state);

const params = new URLSearchParams({
  client_id: import.meta.env.VITE_CATIVA_CLIENT_ID,
  redirect_uri: 'https://myapp.com/callback',
  response_type: 'code',
  scope: 'openid profile email',
  code_challenge: challenge,
  code_challenge_method: 'S256',
  state
});

window.location.href =
  `https://apis.cativalab.digital/social/v1/sso/${customerName}/authorize?${params}`;

3. Handle the callback

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 with the code_challenge it stored from step 2.
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const returnedState = url.searchParams.get('state');
const expectedState = sessionStorage.getItem('cativa_oauth_state');
const verifier = sessionStorage.getItem('cativa_pkce_verifier');

if (!code || returnedState !== expectedState) {
  throw new Error('Invalid OAuth state');
}

const res = await fetch(
  `https://apis.cativalab.digital/social/v1/sso/${customerName}/token`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      client_id: import.meta.env.VITE_CATIVA_CLIENT_ID,
      client_secret: import.meta.env.VITE_CATIVA_CLIENT_SECRET,
      redirect_uri: 'https://myapp.com/callback',
      code_verifier: verifier
    })
  }
);

if (!res.ok) {
  const err = await res.json();
  throw new Error(`OAuth error: ${err.error_description ?? err.error}`);
}

const tokens = await res.json();
sessionStorage.removeItem('cativa_pkce_verifier');
sessionStorage.removeItem('cativa_oauth_state');
Even in a SPA, Cativa requires client_secret in the /token body. If you want a flow with truly no secret on the frontend, set up a small proxy endpoint on your backend that takes the code from the SPA and performs the exchange server-side (which is the Traditional backend pattern below).

4. Fetch the profile and start the local session

const userRes = await fetch(
  `https://apis.cativalab.digital/social/v1/sso/${customerName}/userinfo`,
  { headers: { Authorization: `Bearer ${tokens.access_token}` } }
);
const user = await userRes.json();
// { sub, name, email, picture }

// Persist the user in SPA state + access_token in memory.
Don’t persist the 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:
https://apis.cativalab.digital/social/v1/sso/{customerName}/jwks
The discovery doc points to this JWKS — OIDC client libraries fetch and cache the key automatically.

Node.js with jose

import { createRemoteJWKSet, jwtVerify } from 'jose';

const issuer = `https://apis.cativalab.digital/social/v1/sso/${customerName}`;
const JWKS = createRemoteJWKSet(new URL(`${issuer}/jwks`));

async function verifyIdToken(idToken, clientId) {
  const { payload } = await jwtVerify(idToken, JWKS, {
    issuer,
    audience: clientId,
    algorithms: ['ES256']
  });
  return payload; // { sub, name, email, picture, iat, exp, iss, aud, ... }
}

Python with PyJWT

import jwt
from jwt import PyJWKClient

issuer = f'https://apis.cativalab.digital/social/v1/sso/{customer_name}'
jwks_client = PyJWKClient(f'{issuer}/jwks')

def verify_id_token(id_token, client_id):
    signing_key = jwks_client.get_signing_key_from_jwt(id_token)
    return jwt.decode(
        id_token,
        signing_key.key,
        algorithms=['ES256'],
        issuer=issuer,
        audience=client_id
    )

Session and token refresh

The access_token returned by Cativa SSO is short-lived (configured per OAuth App, default 1h). Two strategies:
  1. Silent re-login when it expires — when expires_in reaches 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).
  2. 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 Cativa access_token is used only at login time.
Today the Cativa OIDC /sso/{customerName}/token endpoint does not support grant_type=refresh_token — only authorization_code. Refresh-token flow for external OAuth apps is on our roadmap. For now, use one of the two strategies above. When support ships, this page will be updated with an example.

Logout

There is currently no dedicated OIDC end_session endpoint for external OAuth apps. The recommended flow:
  1. On your side: drop the local session (delete cookie/session/Keychain), redirect the user to a “logged out” destination.
  2. 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

The redirect_uri sent to /authorize (and re-confirmed at /token) must be identical to one of the URIs registered on the OAuth App in the Console. Compare carefully: trailing slash, http vs https, and case in paths matter.
The code returned by /authorize has a short TTL (a few minutes). If your exchange takes a while — because the backend redirects to another route first, or because you’re debugging by hand — the code expires. Always exchange it immediately in the callback.
The code is single-use. If your callback is hit twice (e.g. user reloaded the callback page), the second exchange fails with this error. That’s expected — just redirect to the SPA home when you already have tokens in session.
Check:
  • You didn’t swap client_id and client_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 .env when the file came from Windows).
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 access_token expired or is malformed. Check Authorization: Bearer <access_token> (don’t use the id_token here — /userinfo validates the access_token). If the token is fresh and you still get 401, validate the iss of the access_token (must be https://apis.cativalab.digital/social/v1/sso/{customerName}).
Confirm:
  • algorithms: ['ES256'] (Cativa signs with ES256, not RS256).
  • issuer exactly https://apis.cativalab.digital/social/v1/sso/{customerName} (no trailing slash).
  • audience is your client_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.

Quick Start: API Key

For server-to-server calls (no interactive user), use an API Key instead of OAuth.