# Para quem desenvolve

> Como ligar um sistema à Central de Identidade para login (OpenID Connect) e como usar a API
> da central para gerir pessoas.

A Central de Identidade é um provedor **OpenID Connect** (OIDC): o padrão aberto de login que
fica por cima do OAuth2. Em vez de guardar senhas, o seu sistema manda a pessoa para a central,
ela entra lá, e o sistema recebe de volta quem ela é. Qualquer biblioteca cliente de OIDC da sua
linguagem serve, e uma de OAuth2 puro também (veja o checklist abaixo); esta página diz o que a
central espera e onde ela foge do comum.

**Antes de integrar, leia também o [Checklist de integração](/docs/integration.md)**: o que o
seu sistema precisa fazer, além do protocolo.

## Antes de começar: o client

Para a central aceitar o seu sistema, ele precisa de um **client**: o cadastro do sistema na
central, com um identificador (`client_id`). Quem cadastra é um administrador global. Leve para
ele:

- **Tipo**: **confidencial**, se o sistema guarda um segredo no servidor (o caso comum de um
  backend), ou **público**, se não tem onde guardar (um app que roda só no navegador ou no
  celular). Essa escolha não muda depois.
- **Redirect URIs**: os endereços para onde a central devolve a pessoa depois do login.
- **Post-logout redirect URIs**, se o sistema for usar a saída pela central (veja
  [Sair](#sair)).
- **Scopes** de que o sistema precisa (veja [Quem é a pessoa](#quem-é-a-pessoa-scopes-e-claims)).
- **Grants**: `authorization_code` e `refresh_token` para login; `client_credentials` só se o
  sistema for chamar a API em nome próprio (veja [API](#api)).

Um client confidencial recebe um **secret**, a senha do sistema. **Ele aparece uma vez só**, para
o administrador, no momento do cadastro: guarde na hora, num cofre de segredos.

## Login

### Endpoints do login

| O quê | Caminho |
|---|---|
| Discovery | `GET /.well-known/openid-configuration` |
| Autorização | `GET /authorize` |
| Token | `POST /token` |
| UserInfo | `GET` ou `POST /userinfo` |
| Chaves públicas (JWKS) | `GET /jwks` |
| Saída | `GET /end-session` |

O documento de discovery tem esses endereços e o `issuer`, mas **não lista os scopes e claims**:
eles estão na seção [Quem é a pessoa](#quem-é-a-pessoa-scopes-e-claims), abaixo.

### O fluxo

A central só aceita o fluxo **authorization code com PKCE**, e o PKCE é **obrigatório para todo
client, inclusive os confidenciais**. PKCE é a prova de que quem troca o código pelo token é o
mesmo sistema que pediu o login: o sistema gera um segredo aleatório (`code_verifier`), manda o
hash dele no pedido de login (`code_challenge`) e o segredo original na troca.

1. O sistema manda a pessoa para `/authorize`:

   ```
   https://home-hml.vetorzero.com.br/authorize
     ?response_type=code
     &client_id=meu-sistema
     &redirect_uri=https://meu-sistema.exemplo.com.br/callback
     &scope=openid profile email
     &state=<aleatório>
     &nonce=<aleatório>
     &code_challenge=<BASE64URL(SHA256(code_verifier))>
     &code_challenge_method=S256
   ```

   **Mande `code_challenge_method=S256` sempre.** Sem ele, a biblioteca do servidor assume
   `plain`, que a central recusa.

2. A pessoa entra com a conta Google (ou passa direto, se já tiver entrado) e volta para a
   `redirect_uri` com `code` e `state`. **Não existe tela de consentimento**: todos os sistemas
   ligados à central são da própria empresa.

3. O sistema troca o código por tokens:

   ```sh
   curl -X POST "https://home-hml.vetorzero.com.br/token" \
     -u 'meu-sistema:SECRET' \
     -d grant_type=authorization_code \
     -d code=CODE \
     -d redirect_uri=https://meu-sistema.exemplo.com.br/callback \
     -d code_verifier=CODE_VERIFIER
   ```

   Um client confidencial se autentica com HTTP Basic (como acima) ou com `client_id` e
   `client_secret` no corpo. Um client público manda só o `client_id` no corpo.

4. A resposta traz `access_token`, `id_token` e `refresh_token`. O `id_token` já tem as claims
   dos scopes pedidos; confira nele o `nonce` que você mandou.

### Tokens

| Token | Validade |
|---|---|
| Código de autorização | 10 minutos |
| Access token | 1 hora |
| ID token | 1 hora |
| Refresh token | 1 mês |

- O access token e o ID token são **JWT assinados com RS256**. As chaves públicas estão em
  `/jwks`.
- **Cada refresh token vale uma vez só**: ao usá-lo, você recebe um novo e o antigo é revogado.
  Guarde sempre o último.
- A central **não tem endpoint de revogação nem de introspecção** de token.

### Regras de redirect URI

- Tem que ser `https`. A exceção é `http` em `localhost` ou `127.0.0.1`, para desenvolvimento.
- A comparação é **exata**, caractere por caractere. Em `http://127.0.0.1` a porta é ignorada;
  em `http://localhost`, **a porta precisa bater**.

## Quem é a pessoa: scopes e claims

Cada scope libera um conjunto de claims (os dados da pessoa). O scope `openid` entra sempre.

| Scope | Claim | Tipo | O que é |
|---|---|---|---|
| `openid` | `sub` | string | Identificador da pessoa, como `vlidc_usr_…` |
| `profile` | `name` | string | Nome |
| `profile` | `picture` | string ou null | Foto da conta Google principal |
| `email` | `email` | string | Email principal |
| `email` | `email_verified` | bool | Se o email principal foi confirmado |
| `verified_emails` | `verified_emails` | string[] | Todos os emails confirmados |
| `employment` | `employer` | string ou null | Domínio da empresa que contratou a pessoa; null para freelancer |
| `employment` | `served_organizations` | string[] | Domínios das empresas que ela atende |
| `slack` | `slack` | objeto ou null | Conta no Slack: `{"user_id": "U…", "team_id": "T…"}` |
| `access` | `groups` | objeto | Grupos da pessoa: slug do grupo → caminho até ele, `{}` sem grupo |
| `access` | `permissions` | string[] | Permissões efetivas da pessoa; vazio sem nenhuma |

As claims vêm no ID token e também em `/userinfo`.

O scope `slack` é opcional e só serve a quem precisa falar com a pessoa no Slack.

O scope `access` entrega a autorização da pessoa. Ele é opcional e **precisa ser pedido**:

```json
{
  "groups": { "animacao_2d": ["producao", "2d", "animacao_2d"] },
  "permissions": ["something.read", "another_thing"]
}
```

- `groups`: cada chave é o slug de um grupo **a que a pessoa foi atribuída diretamente**, e o valor
  é o caminho de slugs da raiz até ele, incluindo ele. Para saber se a pessoa "está em X", procure
  a chave; para "está sob X" (X ou qualquer subgrupo), confira o prefixo do caminho. Sem grupo, vem
  `{}`, nunca `[]`.
- `permissions`: todas as permissões da pessoa, já somadas as dos grupos dela e as de todos os
  subgrupos. **Vêm todas, não só as do seu sistema**: o catálogo é global, e o seu sistema escolhe
  quais conhece.
- **Use os slugs como chave**: o nome do grupo pode mudar. Um administrador pode trocar um slug,
  e aí o seu sistema deixa de reconhecê-lo até ser atualizado.
- Mudanças na estrutura só aparecem no próximo token (ou chamada a `/userinfo` com um token novo).

**Identifique a pessoa pelo `sub`, nunca pelo email.** Isso e o resto do que o seu sistema
precisa fazer com essas claims, incluindo como associar usuários que já existiam, estão no
[Checklist de integração](/docs/integration.md).

### /userinfo

`/userinfo` devolve as mesmas claims a partir de um access token. O token vai **só no header**
`Authorization: Bearer <token>`, tanto em `GET` quanto em `POST`; não há parâmetro no corpo.

```sh
curl "https://home-hml.vetorzero.com.br/userinfo" -H "Authorization: Bearer ACCESS_TOKEN"
```

Um token de `client_credentials` é recusado aqui (403): ele não representa uma pessoa.

## Sair

**Sair da central não tira a pessoa do seu sistema**, e sair do seu sistema não tira ela da
central. Cada lado tem a sua sessão.

Para encerrar também a sessão na central, mande a pessoa para `/end-session`, com os parâmetros
na query string:

```
https://home-hml.vetorzero.com.br/end-session
  ?client_id=<o client_id do seu sistema>
  &id_token_hint=<o id_token da pessoa>
  &post_logout_redirect_uri=https://meu-sistema.exemplo.com.br/
  &state=<opcional>
```

- A `post_logout_redirect_uri` precisa estar cadastrada no client. Ela também exige
  `id_token_hint` ou `client_id`.
- A saída é direta quando vem o `client_id` do seu sistema junto com um `id_token_hint` emitido
  para ele, ou junto com uma `post_logout_redirect_uri` cadastrada. Nos outros casos (por exemplo,
  só `id_token_hint`, sem `client_id`), a central mostra antes uma tela de confirmação ("Sair da
  Central de Identidade?", com "Sair" e "Cancelar").

## Pessoas desativadas

Quando um administrador desativa alguém, a central revoga os tokens dessa pessoa: `/userinfo` e
o refresh passam a recusar. **Um JWT que o seu sistema valida sozinho, só com a chave pública,
continua válido até expirar** (no máximo 1 hora). Se o seu sistema precisa reagir na hora,
consulte `/userinfo` ou faça refresh em vez de confiar só na validade do JWT.

## API

A API em `/api/v1` permite que outro sistema gerencie pessoas: listar, consultar, cadastrar,
editar, desativar e cuidar de emails. A especificação completa, em OpenAPI, está em
[`/api/doc`](https://home-hml.vetorzero.com.br/api/doc) (para ler no navegador) e em
[`/api/doc.json`](https://home-hml.vetorzero.com.br/api/doc.json).

### Acesso

Toda chamada leva um access token no header `Authorization: Bearer`. Há dois jeitos de obter um:

- **Em nome do próprio sistema** (`client_credentials`): o sistema age como ele mesmo, com os
  papéis que o client recebeu. Exige client confidencial.

  ```sh
  curl -X POST "https://home-hml.vetorzero.com.br/token" -u 'meu-sistema:SECRET' -d grant_type=client_credentials
  ```

- **Em nome de uma pessoa**: o access token do login dela. A chamada vale com os papéis da
  pessoa.

Nos dois casos, **o client precisa ter "Acesso à API" liberado** por um administrador global,
que também define os IPs de onde ele pode chamar (sem nenhum IP, qualquer um é aceito). Leituras (`GET` e
`QUERY`) exigem o papel de leitor (de pessoas ou de diretório, conforme o recurso); escritas exigem o
administrador correspondente. Um client que só consulta deve receber só o leitor. As chamadas ficam registradas na
auditoria da central, com o client de origem.

**O bearer é o access token, não o secret.** Mandar o secret no lugar dá 401.

### Testar a API pelo `/api/doc`

O botão "Authorize" do [`/api/doc`](https://home-hml.vetorzero.com.br/api/doc) oferece três esquemas:

- `client`: você informa o client id e o secret, e a página busca o token (`client_credentials`).
- `user`: abre o login da central e volta com o token da pessoa logada, que age com os papéis dela
  (leitor ou administrador, conforme a chamada). Papel de client não vale aqui, só em
  `client_credentials`.
- `bearer`: para um access token que veio de outro lugar.

Para usar o `user`, um administrador global cadastra um **client público**, só com o grant
`authorization_code`, tendo `https://home-hml.vetorzero.com.br/api/doc/oauth2-redirect` como redirect URI, e libera o
acesso à API para ele, sem papéis. Quem testa digita o client id no "Authorize"; ele não vem
preenchido. A redirect URI tem que bater exatamente com a cadastrada, inclusive a porta quando o
host é `localhost`.

### Endpoints da API

| Método | Caminho | O quê |
|---|---|---|
| `GET` | `/api/v1/users?limit=&offset=` | Lista pessoas (filtros opcionais: `email`, `ownsEmail`, `active`, `claimable`, `employer`, `served`) |
| `QUERY` | `/api/v1/users/email-owners` | Descobre o dono de vários emails de uma vez |
| `POST` | `/api/v1/users` | Cadastra uma pessoa (`name`, `email`; opcionais `active`, `invite`, `employment` e `slack`) |
| `GET` | `/api/v1/users/{id}` | Consulta uma pessoa |
| `PUT` | `/api/v1/users/{id}` | Troca nome, email e conta no Slack |
| `PUT` | `/api/v1/users/{id}/employment` | Troca empregador, empresas atendidas |
| `POST` | `/api/v1/users/{id}/emails` | Adiciona um email (a pessoa confirma pelo link, ou já confirmado com `verified`) |
| `DELETE` | `/api/v1/users/{id}/emails/{address}` | Remove um email |
| `POST` | `/api/v1/users/{id}/deactivate` | Desativa |
| `POST` | `/api/v1/users/{id}/reactivate` | Reativa |

O `{id}` é o mesmo `sub` do login. Empresas são identificadas pelo domínio. Uma pessoa
cadastrada pela API recebe o convite por email, como no cadastro pela tela, a menos que você
mande `"invite": false`. Não há exclusão de pessoa: o equivalente é desativar.

A resposta traz `picture`: a URL da foto da conta Google principal, a mesma do claim `picture`
(a imagem fica no Google, não é copiada). É `null` sem conta Google vinculada ou sem foto.

No `PUT /api/v1/users/{id}`, `email` é livre só enquanto a pessoa não tem nenhum email
confirmado (convite pendente). Depois, só aceita um email confirmado dela; qualquer outro dá 422.

Em `slack`, a pessoa tem `{userId, teamId}` ou `null`: os dois campos vêm juntos ou nenhum.
`userId` começa com `U` ou `W` e `teamId` com `T`. O mesmo par não pode estar em duas pessoas
(409). No `PUT /api/v1/users/{id}`, `slack` é obrigatório (`null` desvincula); no
`POST /api/v1/users`, é opcional.

Em `POST /api/v1/users`, `employment` define o vínculo já na criação, no mesmo formato do corpo de
`PUT /api/v1/users/{id}/employment` (`employer`, `servedOrganizations`). Sem `employment`, a pessoa nasce sem vínculo. Empresa desconhecida dá 422. A central
não deduz o empregador pelo domínio do email: quem cadastra informa.

Em `POST /api/v1/users/{id}/emails`, `"verified": true` faz você atestar o endereço no lugar da
pessoa: ele entra já confirmado (ou, se estava pendente, passa a confirmado) e nenhum email é
enviado. `reason` é obrigatório nesse caso, sem ele a resposta é 422, e vai para a auditoria junto
com o seu client. Se o endereço já está confirmado nessa pessoa, nada muda. Se está confirmado em
outra pessoa, dá 409. Sem `verified`, a pessoa recebe o link e confirma sozinha. Use só quando o
sistema de origem é confiável quanto àquele endereço: quem tem o papel de administrador de pessoas
pode confirmar um email que não controla.

### Receber webhooks

Em vez de consultar a API de tempos em tempos, o sistema pode ser avisado de cada mudança. Um
administrador global informa a "URL de webhook" no client (seção "API", que exige o acesso à API).
O sistema passa a receber o que os papéis dele deixam ver, com a mesma regra da API: eventos de
pessoas pedem o leitor de pessoas, de clients o administrador global, e o resto do diretório
(empresas, apps, grupos, permissões) o leitor de diretório. **Mudança de papel de
uma pessoa nunca é enviada.** Um client desativado não recebe nada, e um webhook novo começa do
momento em que é criado, sem histórico.

Cada aviso é um **Security Event Token** (SET, [RFC 8417](https://www.rfc-editor.org/rfc/rfc8417)):
um JWT entregue por `POST` ([RFC 8935](https://www.rfc-editor.org/rfc/rfc8935)) com
`Content-Type: application/secevent+jwt`, e o JWT inteiro como corpo.

**Como verificar.** O JWT é assinado (RS256) com a mesma chave do login: valide a assinatura pelo
JWKS da central. O header tem `typ: secevent+jwt`. Confira `iss` (o `issuer` da central) e `aud`
(o seu client id). **Não há `exp`**: o aviso é um fato, e uma retentativa pode chegar dias depois.

| Claim | O quê |
|---|---|
| `jti` | Id do evento, igual em todas as retentativas. Use para deduplicar. |
| `iat` | Quando o aviso foi assinado. |
| `toe` | Quando a ação aconteceu (segundos Unix). |
| `events` | Objeto com um tipo de evento como chave e `{"subject": {"type", "id"}}` como valor. |
| `urn:vl-idc:claim:own` | `true` só quando a mudança foi feita pelo seu próprio client via API; ausente nos demais. |

O tipo é `urn:vl-idc:event:<ação>` (texto fixo, compare como string), com a ação igual à da
auditoria, como `user.updated`, `user.email_verified`, `client.updated`, `group.created` ou
`organization.created`. O `subject` traz o tipo do alvo (`user`, `client`, `organization`, `app`,
`group` ou `permission`) e o mesmo id que a API usa para ele. **O aviso não traz
dados da pessoa nem quem agiu**: para saber o estado, consulte a API com o seu token. A claim
`own` permite pular essa consulta quando foi o seu próprio sistema que acabou de escrever. Ela não
evita loops por si: escrita sem mudança não gera evento.

Exemplo de `events` decodificado:

```json
{"urn:vl-idc:event:user.updated": {"subject": {"type": "user", "id": "vlidc_usr_…"}}}
```

**O que responder.** Qualquer resposta **2xx** confirma o aviso (a RFC pede 202, mas 200 serve).
Qualquer outra coisa, ou erro de rede, ou mais de 5 segundos sem resposta, conta como falha;
redirecionamentos não são seguidos. A central **não** lê o corpo da resposta.

**Ordem e retentativas.** Os avisos de um client saem em ordem. Uma falha segura a fila: o mesmo
aviso volta com espera crescente (10s, 20s, 40s… até 1 hora entre tentativas). Depois de cerca de
24h falhando, o webhook é **pausado** e nada mais é enviado até um administrador global usar
"Retomar agora" na tela do client; a entrega recomeça de onde parou, sem perder aviso. Em
produção, a URL tem que ser `https` e não pode apontar para rede privada.

**Testar.** Na tela do client, "Enviar evento de verificação" manda na hora um SET de tipo
`https://schemas.openid.net/secevent/ssf/event-type/verification`, com `events` vazio de dados, e
mostra se o receptor respondeu 2xx. Ele não entra na fila nem na auditoria.

### Achar uma pessoa pelo email

Para achar o `id` de alguém a partir do email que seu sistema tem, use os filtros de
`GET /api/v1/users`. Todos são opcionais, combináveis, de valor exato e sem diferenciar maiúsculas
de minúsculas; `total`, `next` e `prev` respeitam o filtro.

- `ownsEmail=` acha **quem é dono do endereço**: o email principal ou um email secundário
  confirmado. Dá no máximo uma pessoa. Email não confirmado nunca casa, porque pode se repetir
  entre pessoas. **É o filtro certo para associar usuários.**
- `email=` casa só o email principal.
- `active=true` ou `active=false` separa quem está ativo de quem foi desativado.
- `claimable=true` ou `claimable=false` separa quem ainda não foi assumido por ninguém no login.
- `employer=` filtra pelo domínio da empresa contratante (`vetorzero.com.br`), ou `employer=none`
  para quem não tem empregador (freelancers).
- `served=` filtra pelo domínio de uma organização que a pessoa atende.

Domínio e id bem formados mas que não existem dão lista vazia: aqui é filtro, não referência
(diferente do corpo de `PUT /api/v1/users/{id}/employment`, onde seria 422). Valor vazio ou
malformado (domínio que não é um hostname, id fora do formato) dá 422, não "todos". Não há filtro
por papel.

Para vários emails, `QUERY /api/v1/users/email-owners` aplica a regra do `ownsEmail` em lote. O
método `QUERY` (RFC 10008) lê sem alterar nada, como o `GET`, mas leva a lista no corpo:

```json
{"emails": ["ada@example.com", "ninguem@example.com"]}
```

São de 1 a 100 emails; fora disso, ou com um endereço malformado, a resposta é 422. A resposta
traz `items`, um objeto com cada email **exatamente como você mandou** e a pessoa dona dele, ou
`null` se ninguém é dona:

```json
{"items": {"ada@example.com": {"id": "…", "name": "Ada Lovelace"}, "ninguem@example.com": null}}
```

Os dois exigem o papel de leitor de pessoas. A documentação OpenAPI em `/api/doc` segue a
versão 3.2, a primeira que descreve o `QUERY`; ferramentas mais antigas podem não reconhecê-lo.

### Importar pessoas de outro sistema

Para trazer uma base existente, use um client por script de importação: a auditoria registra o
client de origem, e assim dá para saber de onde cada pessoa veio.

- `POST /api/v1/users` com `"active": false` cria quem já saiu da empresa: a pessoa nasce
  desativada, sem convite por email, e continua reclamável. Sem `active`, vale `true`.
- `"invite": false` cadastra a pessoa ativa sem mandar o convite por email, útil para importar quem
  ainda não deve ser avisado. Sem `invite`, vale `true` e o convite sai (para quem nasce ativo).
  A auditoria registra "Usuário criado sem convite".
- `employment` traz o vínculo de cada pessoa na mesma chamada.
- Para saber antes quem já existe, consulte por email (veja
  [Achar uma pessoa pelo email](#achar-uma-pessoa-pelo-email)).
- Email já usado dá 409 com `user_id` no corpo, o `id` de quem o possui (como email principal ou
  como email secundário confirmado). Nada é alterado. Se nome ou estado divergem, o script corrige
  com `PUT /api/v1/users/{id}` ou `POST /api/v1/users/{id}/deactivate`. O mesmo vale para
  um `PUT /api/v1/users/{id}` que bate no email de outra pessoa.
- `POST /api/v1/users/{id}/emails` com `"verified": true` traz os emails secundários já confirmados.

O repositório traz dois exemplos completos (Python, só biblioteca padrão), com um client
próprio: `scripts/import-users-from-ferias/`, que importa as pessoas do ferias.

### Convenções

- **Os nomes da API estão em inglês** (campos, mensagens de erro).
- **Não há `PATCH`.** O `PUT` manda o estado inteiro do recurso: toda chave é obrigatória,
  inclusive as que aceitam `null`. Chave faltando dá erro.
- **`Idempotency-Key`** (opcional, só em `POST`): repetir a mesma chamada com a mesma chave
  devolve a resposta guardada, com o header `Idempotent-Replayed: true`, em vez de repetir o
  efeito. Só respostas 2xx são guardadas, por pelo menos 7 dias. A mesma chave com outro corpo dá
  422; uma chamada enquanto a primeira ainda roda dá 409.
- **Coleção sem ordem** (como o resultado de `email-owners`) sai como objeto indexado por uma
  chave útil, não como lista; vazia, é `{}`. Lista só quando a ordem importa.
- **Paginação**: `limit` de 1 a 100 (padrão 50) e `offset`. A resposta traz `items`, `total` e
  os links `next` e `prev`.
- **Erros** seguem o RFC 9457 (`application/problem+json`), com `title`, `status` e, quando
  ajuda, `detail`. Erro de validação é 422, com a lista em `violations`; recurso inexistente é
  404; conflito é 409. Email já usado traz `user_id`, o id de quem o possui.
- **Limite de uso**: a API conta as chamadas **por client**, e a conta vale também para o token de
  uma pessoa obtido por esse client. O limite é alto, só para um programa em loop não derrubar a
  central: uma integração normal não chega nele, e a rajada de um script cabe. Passou do limite, a
  resposta é 429 (problem details) com o header `Retry-After`, em segundos até poder tentar de
  novo. Espere esse tempo antes de repetir. Todo endpoint da API pode responder 429; o `/token`
  não entra na conta.

## Chamada entre sistemas

Um sistema (o **consumidor**) pode chamar a API de outro sistema (o **destino**) em nome de si
mesmo, sem pessoa no meio. O consumidor pede um token à central com `client_credentials` e o
destino confere o token sozinho, sem falar com a central a cada chamada.

### Como o consumidor pede o token

O client do consumidor precisa ser confidencial e ter o grant `client_credentials`. No `/token`,
o parâmetro `audience` diz para qual sistema o token vale: é o **client id do destino**. Vários
destinos vão separados por espaço.

```sh
curl -X POST "https://home-hml.vetorzero.com.br/token" -u 'consumidor:SECRET' \
  -d grant_type=client_credentials -d audience=destino
```

- O `aud` do access token passa a ter **só** os ids pedidos. O consumidor deixa de constar nele.
- Cada id tem que ser de um client que existe e está ativo. Senão, 400 `invalid_target`.
- `audience` só vale no `client_credentials`. Em outro grant, 400 `invalid_request`.
- Sem `audience`, o `aud` é o próprio client que pediu, como sempre.
- **Guarde o token em cache até perto do `exp`** e peça outro só então. Não há refresh token no
  `client_credentials`: quando vence, é só pedir de novo.

### Como o destino valida o token

O access token é um JWT assinado com RS256. O destino confere, sempre, com uma biblioteca de JWT:

1. **Assinatura**, com as chaves de `/jwks`. Guarde-as em cache e busque de novo se a
   assinatura não bater.
2. **`exp` e `nbf`**: o token não pode estar vencido nem ser de uso futuro.
3. **`iss`** igual ao `issuer` de `/.well-known/openid-configuration`. Token de outro emissor é
   recusado.
4. **`aud`** contém o **próprio client id** do destino. Token feito para outro sistema é recusado.
5. **`client_id` diferente do próprio client id** do destino. O `client_id` é o client que pediu
   o token. Um token que o próprio destino pediu (por exemplo, o login de uma pessoa nele) tem
   `aud` e `client_id` iguais a ele, e não é uma chamada de outro sistema: recuse.

O `client_id` identifica quem está chamando.

**Por enquanto, mantenha também uma lista dos `client_id` aceitos.** Hoje qualquer client da
central pode pedir um token com o seu id no `audience`: o `aud` impede só que o token seja usado
em outro destino. Sem a lista, qualquer sistema da central entra. Quando a central passar a
controlar quem pode pedir token para quem (acompanhe em
[#123](https://github.com/vetorzero/vl-idc/issues/123)), a lista deixa de ser necessária.

## Quando algo dá errado

**`invalid_request` "PKCE is required"** no `/authorize`: faltou o `code_challenge`. Se ele foi,
confira o `code_challenge_method=S256`.

**`invalid_client` no `/token`**: secret errado, client desativado ou, no `client_credentials`,
chamada de um IP fora da lista do client.

**`invalid_target` no `/token`**: algum id do `audience` não é de um client existente e ativo.

**`invalid_request` no `/token` citando `application/x-www-form-urlencoded`**: o corpo foi enviado
em outro formato, como JSON. O `/token` só aceita formulário.

**`invalid_request` no `/token` citando `audience`**: o parâmetro só vale com `client_credentials`.

**Página "Não deu pra entrar" no `/authorize`**: erro que a central não
pode devolver ao seu sistema (client desconhecido ou desativado, `redirect_uri` ausente ou fora do
cadastro) aparece como página na própria central, com o código do erro e, em "Detalhes pra quem
integra", o `client_id` e a `redirect_uri` recebidos. Quando é a `redirect_uri`, ela provavelmente
não bate exatamente com a cadastrada: atenção à barra no fim e à porta do `localhost`. A página
não lista as `redirect_uri` cadastradas; confira no cadastro do client.

**Página "Não deu pra sair" no `/end-session`**: o pedido de saída veio inválido. A página traz a
mensagem da central sobre o parâmetro.

**`access_denied` no `/authorize` durante um teste**: o administrador está impersonando outra
pessoa na central. Impersonando, a central não deixa entrar em nenhum sistema em produção. Nos
ambientes de teste (com faixa de segurança no topo das telas) isso é permitido.

**403 em `/userinfo`**: o token é de `client_credentials`, que não representa uma pessoa.

**401 na API**: falta o bearer, o bearer é o secret em vez do access token, ou o client está
desativado, sem acesso à API ou chamando de um IP não liberado. O `detail` diz qual.

**429 na API**: o client passou do limite de uso. Espere o `Retry-After` e confira se não há um
loop ou paralelismo sem controle chamando a API.
