> ## 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.

# Implementar Login com a Cativa

> Guia completo: SPA com PKCE, backend Node/Express, mobile com deep link. Inclui validação de id_token via JWKS e tratamento de erros.

O [Quick Start](/pt-BR/get-started/quickstart-sign-in) 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.

<Info>
  Prefere aprender por exemplo? O repo [`cativa-sso`](https://github.com/cativalab/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.
</Info>

## 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](https://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.

<Note>
  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`).
</Note>

## Decida qual fluxo usar

<Tabs>
  <Tab title="SPA com PKCE">
    **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`.

    ```js theme={null}
    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`

    ```js theme={null}
    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.

    ```js theme={null}
    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');
    ```

    <Warning>
      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).
    </Warning>

    ### 4. Pegue o perfil e faça login local

    ```js theme={null}
    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.
  </Tab>

  <Tab title="Backend Node/Express">
    **Quando usar:** seu app tem backend (Express, Fastify, NestJS, Django, Rails, ASP.NET) e você prefere manter o `client_secret` server-side e usar **sessão httpOnly cookie**. Esse é o padrão mais seguro e o que recomendamos quando há backend.

    ### 1. Inicie o login

    ```js theme={null}
    import express from 'express';
    import crypto from 'crypto';

    const app = express();
    app.use(require('express-session')({
      secret: process.env.SESSION_SECRET,
      resave: false,
      saveUninitialized: false,
      cookie: { httpOnly: true, secure: true, sameSite: 'lax' }
    }));

    function base64UrlEncode(buf) {
      return buf.toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
    }

    app.get('/login', (req, res) => {
      const verifier = base64UrlEncode(crypto.randomBytes(48));
      const challenge = base64UrlEncode(
        crypto.createHash('sha256').update(verifier).digest()
      );
      const state = crypto.randomBytes(16).toString('hex');

      req.session.pkceVerifier = verifier;
      req.session.oauthState = state;

      const params = new URLSearchParams({
        client_id: process.env.CATIVA_CLIENT_ID,
        redirect_uri: process.env.CATIVA_REDIRECT_URI,
        response_type: 'code',
        scope: 'openid profile email',
        code_challenge: challenge,
        code_challenge_method: 'S256',
        state
      });

      res.redirect(
        `https://apis.cativalab.digital/tenant/api/v2/sso/${process.env.CATIVA_CUSTOMER}/authorize?${params}`
      );
    });
    ```

    ### 2. Receba o callback e troque o code

    ```js theme={null}
    app.get('/callback', async (req, res) => {
      const { code, state } = req.query;

      if (!code || state !== req.session.oauthState) {
        return res.status(400).send('Estado OAuth inválido');
      }

      const tokenRes = await fetch(
        `https://apis.cativalab.digital/tenant/api/v2/sso/${process.env.CATIVA_CUSTOMER}/token`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({
            grant_type: 'authorization_code',
            code,
            client_id: process.env.CATIVA_CLIENT_ID,
            client_secret: process.env.CATIVA_CLIENT_SECRET,
            redirect_uri: process.env.CATIVA_REDIRECT_URI,
            code_verifier: req.session.pkceVerifier
          })
        }
      );

      if (!tokenRes.ok) {
        const err = await tokenRes.json();
        return res.status(400).send(`OAuth error: ${err.error_description}`);
      }

      const tokens = await tokenRes.json();
      // { access_token, token_type, expires_in, id_token }

      // Pegue o perfil do usuário
      const userRes = await fetch(
        `https://apis.cativalab.digital/tenant/api/v2/sso/${process.env.CATIVA_CUSTOMER}/userinfo`,
        { headers: { Authorization: `Bearer ${tokens.access_token}` } }
      );
      const user = await userRes.json();

      // Persista o usuário na sessão
      req.session.user = user;
      req.session.cativa = {
        accessToken: tokens.access_token,
        idToken: tokens.id_token,
        expiresAt: Date.now() + tokens.expires_in * 1000
      };
      delete req.session.pkceVerifier;
      delete req.session.oauthState;

      res.redirect('/');
    });
    ```

    ### 3. Proteja rotas autenticadas

    ```js theme={null}
    function requireAuth(req, res, next) {
      if (!req.session.user) return res.redirect('/login');
      next();
    }

    app.get('/dashboard', requireAuth, (req, res) => {
      res.render('dashboard', { user: req.session.user });
    });
    ```
  </Tab>

  <Tab title="Mobile com deep link">
    **Quando usar:** app nativo iOS/Android. O fluxo é o mesmo OAuth + PKCE da SPA, mas o redirect URI é um **custom scheme** (ex: `meuapp://callback`) ou um **App Link / Universal Link** (ex: `https://meuapp.com/oauth/callback`).

    ### 1. Configure o deep link

    * **iOS**: cadastre o scheme em `Info.plist` (`CFBundleURLSchemes`) ou configure Associated Domains pra Universal Links.
    * **Android**: declare um `intent-filter` com `data android:scheme="meuapp"` na Activity, ou um App Link com `android:autoVerify="true"`.

    Depois, registre esse URI no **OAuth App** do Console (`meuapp://callback`).

    ### 2. Use AppAuth (recomendado)

    Não recomendamos implementar o fluxo OAuth manualmente em mobile — use [AppAuth-iOS](https://github.com/openid/AppAuth-iOS) ou [AppAuth-Android](https://github.com/openid/AppAuth-Android) que já faz PKCE, gerencia o `ASWebAuthenticationSession`/Custom Tab e valida o `id_token`.

    ### iOS (Swift)

    ```swift theme={null}
    import AppAuth

    let issuer = URL(string: "https://apis.cativalab.digital/tenant/api/v2/sso/\(customerName)")!

    OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { config, error in
      guard let config = config else { return }

      let request = OIDAuthorizationRequest(
        configuration: config,
        clientId: clientId,
        clientSecret: clientSecret,
        scopes: [OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail],
        redirectURL: URL(string: "meuapp://callback")!,
        responseType: OIDResponseTypeCode,
        additionalParameters: nil
      )

      // Apresenta o ASWebAuthenticationSession e completa PKCE automaticamente
      currentAuthorizationFlow = OIDAuthState.authState(
        byPresenting: request,
        presenting: viewController
      ) { authState, error in
        guard let authState = authState else { return }
        let accessToken = authState.lastTokenResponse?.accessToken
        // Persista authState no Keychain
      }
    }
    ```

    ### Android (Kotlin)

    ```kotlin theme={null}
    val issuer = Uri.parse("https://apis.cativalab.digital/tenant/api/v2/sso/$customerName")

    AuthorizationServiceConfiguration.fetchFromIssuer(issuer) { config, ex ->
      if (config == null) return@fetchFromIssuer

      val request = AuthorizationRequest.Builder(
        config,
        clientId,
        ResponseTypeValues.CODE,
        Uri.parse("meuapp://callback")
      )
        .setScopes("openid", "profile", "email")
        .build()

      val authService = AuthorizationService(context)
      val intent = authService.getAuthorizationRequestIntent(request)
      startActivityForResult(intent, RC_AUTH)
    }
    ```

    Trate o callback no `onActivityResult` e troque o code por tokens via `authService.performTokenRequest`.

    <Note>
      AppAuth lê o discovery doc e configura PKCE/JWKS sozinho — você só precisa do `issuer` (a URL `https://apis.cativalab.digital/tenant/api/v2/sso/{customerName}`) e do `clientId`/`clientSecret` registrados no Console.
    </Note>
  </Tab>
</Tabs>

## 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`

```js theme={null}
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`

```python theme={null}
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:

   ```bash theme={null}
   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).

<Note>
  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`.
</Note>

## Erros comuns

<AccordionGroup>
  <Accordion title="`error: invalid_request, error_description: redirect_uri não autorizada`">
    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.
  </Accordion>

  <Accordion title="`error: invalid_grant, error_description: Código de autorização expirado`">
    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.
  </Accordion>

  <Accordion title="`error: invalid_grant, error_description: Código de autorização já utilizado`">
    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.
  </Accordion>

  <Accordion title="`error: invalid_client, error_description: client_secret inválido`">
    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).
  </Accordion>

  <Accordion title="`error: invalid_grant, error_description: Verificação PKCE falhou`">
    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**.
  </Accordion>

  <Accordion title="`401 Unauthorized` no /userinfo">
    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}`).
  </Accordion>

  <Accordion title="O usuário consegue logar mas o `id_token` não passa na minha validação JWKS">
    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).
  </Accordion>
</AccordionGroup>

## Próximos passos

<CardGroup cols={2}>
  <Card title="Badges como permissão" icon="shield-check" href="/pt-BR/concepts/badges-as-permissions">
    Como o `sub` do id\_token aparece na comunidade e como badges controlam o que esse usuário pode acessar.
  </Card>

  <Card title="Quick Start: API Key" icon="terminal" href="/pt-BR/get-started/quickstart-api-key">
    Para chamadas server-to-server (sem usuário interativo), use uma API Key em vez de OAuth.
  </Card>

  <Card title="App de exemplo (GitHub)" icon="github" href="https://github.com/cativalab/cativa-sso">
    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.
  </Card>
</CardGroup>
