Pular para o conteúdo principal
O Quick Start cobre o caminho feliz em 5 passos. Este guia é a versão profunda: três padrões de implementação (SPA, backend tradicional, mobile nativo), validação de id_token via JWKS, comportamento esperado em erros e o que fazer com o token quando ele expira.
Prefere aprender por exemplo? O repo cativa-sso tem apps de referência prontos pra rodar. A pasta login-com-cativa (Node + Express) implementa todos os passos deste guia, ponta a ponta.

Cenário

Sua empresa tem um app — pode ser uma SPA React em outro domínio, um portal Rails com sessões server-side, ou um app iOS/Android nativo. Sua comunidade já vive numa Cativa e você quer que o usuário entre nesse app com a mesma conta que ele usa na comunidade, sem criar credencial separada. A Cativa expõe um IdP OIDC standard por tenant. Qualquer biblioteca OIDC client (oidc-client-ts, auth0/spa-js, next-auth, passport-openidconnect, AppAuth-iOS, AppAuth-Android) consegue falar com ele a partir do discovery URL do tenant.

Pré-requisitos

  1. Um OAuth App registrado no Console — vá em app.cativa.digital/admin/developers, aba OAuth Apps, clique Criar app. Anote o client_id e o client_secret (o secret aparece uma única vez).
  2. Um redirect URI registrado no mesmo app. Pode adicionar múltiplos (ex: produção + staging + localhost).
  3. O customerName (slug do tenant) — confirme com o admin da comunidade. É o subdomínio público da Cativa daquele tenant.
Os endpoints SSO da Cativa seguem OIDC e são organizados por tenant: https://apis.cativalab.digital/tenant/api/v2/sso/{customerName}/.... O documento de descoberta fica em https://apis.cativalab.digital/tenant/api/v2/sso/{customerName}/.well-known/openid-configuration e lista todos os endpoints e algoritmos suportados (S256 para PKCE, ES256 para id_token).

Decida qual fluxo usar

Quando usar: seu app é um frontend estático (React/Vue/Svelte) servido por CDN, sem backend confiável pra guardar client_secret. PKCE (Proof Key for Code Exchange) substitui o segredo do client por um par verifier/challenge gerado a cada login.

1. Gere code_verifier e code_challenge

O code_verifier é uma string aleatória que fica no navegador. O code_challenge é o SHA-256 do verifier, base64url-encoded — esse vai pro /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. Redirecione para o /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://meuapp.com/callback',
  response_type: 'code',
  scope: 'openid profile email',
  code_challenge: challenge,
  code_challenge_method: 'S256',
  state
});

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

3. Trate o callback

A Cativa redireciona o usuário pra https://meuapp.com/callback?code=...&state=.... Valide o state e troque o code por tokens.Como SPA não pode guardar client_secret, o /token aceita PKCE como prova: você manda o code_verifier original (não o challenge) e a Cativa recomputa o SHA-256 e compara com o code_challenge que ela guardou da etapa 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('Estado OAuth inválido');
}

const res = await fetch(
  `https://apis.cativalab.digital/tenant/api/v2/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://meuapp.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');
Mesmo em SPA, a Cativa exige client_secret no body do /token. Se você quer um fluxo realmente sem secret no frontend, monte um endpoint de proxy no seu backend que recebe o code da SPA e faz a troca server-side (cai no padrão Backend tradicional abaixo).

4. Pegue o perfil e faça login local

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

// Persista o usuário no estado da SPA + access_token em memória.
Não persista o access_token em localStorage — ele é vulnerável a XSS. Mantenha em memória (estado da SPA) ou em sessionStorage se aceitar perder a sessão entre abas.

Validar o id_token via JWKS

O id_token retornado pelo /token é um JWT assinado em ES256. Você deve validar a assinatura antes de confiar nas claims (sub, email, etc.) — isso evita que um atacante substitua o token por um falsificado. A chave pública pra validar fica no JWKS do tenant:
https://apis.cativalab.digital/tenant/api/v2/sso/{customerName}/jwks
A discovery doc aponta pra esse JWKS — bibliotecas OIDC client baixam e cacheiam a chave automaticamente.

Node.js com jose

import { createRemoteJWKSet, jwtVerify } from 'jose';

const issuer = `https://apis.cativalab.digital/tenant/api/v2/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 com PyJWT

import jwt
from jwt import PyJWKClient

issuer = f'https://apis.cativalab.digital/tenant/api/v2/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
    )

Sessão e renovação de token

O access_token retornado pelo SSO da Cativa é de curta duração (configurado no OAuth App, default 1h). Escolha a estratégia que se encaixa:
  1. Renovar com refresh_token (recomendado) — toda troca de authorization_code já devolve um refresh_token (opaco). Troque-o por um access_token novo quando o atual expirar:
    curl -X POST https://apis.cativalab.digital/tenant/api/v2/sso/{customerName}/token \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "grant_type=refresh_token" \
      -d "refresh_token=SEU_REFRESH_TOKEN" \
      -d "client_id=SEU_CLIENT_ID" \
      -d "client_secret=SEU_CLIENT_SECRET"
    
    A resposta traz um access_token novo e um refresh_token novo — os tokens são rotacionados, então guarde o refresh novo e descarte o antigo (ele é invalidado no uso).
  2. Re-login silencioso quando expira — quando expires_in chegar a 0, redirecione o usuário pro /authorize de novo. Se ele ainda tem sessão na Cativa, o IdP devolve um novo code sem pedir senha de novo (single sign-on).
  3. Trate o token como sessão atrelada ao seu app — guarde apenas a identidade (sub, email) na sua sessão e gere o seu próprio JWT/cookie. O access_token da Cativa é usado só no momento do login.

Logout

Hoje não há um endpoint OIDC end_session dedicado pra apps OAuth externos. O fluxo recomendado é:
  1. No seu lado: descarte a sessão local (apague cookie/session/Keychain), redirecione o usuário pra um destino “deslogado”.
  2. Opcional, se quiser deslogar também da comunidade Cativa: redirecione o usuário pra https://{customerName}.cativa.digital/logout (substituindo {customerName} pelo subdomínio do tenant).
Endpoint OIDC standard end_session para parceiros externos está em desenvolvimento. Quando disponível, esta página será atualizada com cURL e exemplo de redirect com post_logout_redirect_uri.

Erros comuns

A redirect_uri enviada pra /authorize (e re-confirmada no /token) precisa ser idêntica a uma das URIs cadastradas no OAuth App do Console. Compare com cuidado: trailing slash, http vs https, e maiúsculas/minúsculas em paths importam.
O code retornado pelo /authorize tem TTL curto (alguns minutos). Se sua troca demora — porque o backend redireciona pra outra rota antes, ou porque você está depurando manualmente — o code expira. Sempre faça a troca imediatamente no callback.
O code é single-use. Se seu callback é chamado duas vezes (ex: usuário deu reload na tela de callback), a segunda troca falha com esse erro. Isso é esperado — apenas redirecione pra home da SPA quando você já tiver os tokens em sessão.
Verifique:
  • Você não confundiu client_id e client_secret.
  • O secret não foi rotacionado no Console (gerar novo invalida o anterior).
  • Não tem espaço/quebra de linha extra na variável de ambiente (acontece com cat .env quando o arquivo veio de Windows).
O code_verifier enviado no /token não bate com o code_challenge enviado no /authorize. Causas comuns: você gerou um novo verifier antes do callback (perdeu o original), está fazendo SHA-256 sobre bytes diferentes (UTF-8 vs ASCII), ou está aplicando base64 padrão em vez de base64url.
O access_token expirou ou está com formato errado. Cheque o Authorization: Bearer <access_token> (não use o id_token aqui — /userinfo valida o access_token). Se o token está válido mas você ainda recebe 401, valide o iss do access_token (deve ser https://apis.cativalab.digital/tenant/api/v2/sso/{customerName}).
Confirme:
  • algorithms: ['ES256'] (a Cativa assina com ES256, não RS256).
  • issuer exatamente https://apis.cativalab.digital/tenant/api/v2/sso/{customerName} (sem trailing slash).
  • audience é o seu client_id.
  • Sua biblioteca está buscando o JWKS do issuer correto (e cacheando, pra não bater no endpoint a cada login).

Próximos passos

Badges como permissão

Como o sub do id_token aparece na comunidade e como badges controlam o que esse usuário pode acessar.

Quick Start: API Key

Para chamadas server-to-server (sem usuário interativo), use uma API Key em vez de OAuth.

App de exemplo (GitHub)

Implementações de referência prontas pra rodar (Node + Express). A pasta login-com-cativa implementa exatamente este fluxo: discovery, PKCE, troca de token e validação via JWKS.