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: 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).
- Scopes de que o sistema precisa (veja Quem é a pessoa).
- Grants:
authorization_codeerefresh_tokenpara login;client_credentialssó se o sistema for chamar a API em nome próprio (veja 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, 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.
-
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=S256Mande
code_challenge_method=S256sempre. Sem ele, a biblioteca do servidor assumeplain, que a central recusa. -
A pessoa entra com a conta Google (ou passa direto, se já tiver entrado) e volta para a
redirect_uricomcodeestate. Não existe tela de consentimento: todos os sistemas ligados à central são da própria empresa. -
O sistema troca o código por tokens:
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_VERIFIERUm client confidencial se autentica com HTTP Basic (como acima) ou com
client_ideclient_secretno corpo. Um client público manda só oclient_idno corpo. -
A resposta traz
access_token,id_tokenerefresh_token. Oid_tokenjá tem as claims dos scopes pedidos; confira nele ononceque 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 éhttpemlocalhostou127.0.0.1, para desenvolvimento. - A comparação é exata, caractere por caractere. Em
http://127.0.0.1a porta é ignorada; emhttp://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:
{
"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
/userinfocom 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.
/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.
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_uriprecisa estar cadastrada no client. Ela também exigeid_token_hintouclient_id. - A saída é direta quando vem o
client_iddo seu sistema junto com umid_token_hintemitido para ele, ou junto com umapost_logout_redirect_uricadastrada. Nos outros casos (por exemplo, sóid_token_hint, semclient_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 (para ler no navegador) e em
/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.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 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ó emclient_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):
um JWT entregue por POST (RFC 8935) 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:
{"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=trueouactive=falsesepara quem está ativo de quem foi desativado.claimable=trueouclaimable=falsesepara quem ainda não foi assumido por ninguém no login.employer=filtra pelo domínio da empresa contratante (vetorzero.com.br), ouemployer=nonepara 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:
{"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:
{"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/userscom"active": falsecria quem já saiu da empresa: a pessoa nasce desativada, sem convite por email, e continua reclamável. Semactive, valetrue."invite": falsecadastra a pessoa ativa sem mandar o convite por email, útil para importar quem ainda não deve ser avisado. Seminvite, valetruee o convite sai (para quem nasce ativo). A auditoria registra "Usuário criado sem convite".employmenttraz o vínculo de cada pessoa na mesma chamada.- Para saber antes quem já existe, consulte por email (veja Achar uma pessoa pelo email).
- Email já usado dá 409 com
user_idno corpo, oidde quem o possui (como email principal ou como email secundário confirmado). Nada é alterado. Se nome ou estado divergem, o script corrige comPUT /api/v1/users/{id}ouPOST /api/v1/users/{id}/deactivate. O mesmo vale para umPUT /api/v1/users/{id}que bate no email de outra pessoa. POST /api/v1/users/{id}/emailscom"verified": truetraz 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. OPUTmanda o estado inteiro do recurso: toda chave é obrigatória, inclusive as que aceitamnull. Chave faltando dá erro. Idempotency-Key(opcional, só emPOST): repetir a mesma chamada com a mesma chave devolve a resposta guardada, com o headerIdempotent-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:
limitde 1 a 100 (padrão 50) eoffset. A resposta trazitems,totale os linksnexteprev. - Erros seguem o RFC 9457 (
application/problem+json), comtitle,statuse, quando ajuda,detail. Erro de validação é 422, com a lista emviolations; recurso inexistente é 404; conflito é 409. Email já usado trazuser_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/tokennã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.
curl -X POST "https://home-hml.vetorzero.com.br/token" -u 'consumidor:SECRET' \
-d grant_type=client_credentials -d audience=destino
- O
auddo 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. audiencesó vale noclient_credentials. Em outro grant, 400invalid_request.- Sem
audience, oaudé o próprio client que pediu, como sempre. - Guarde o token em cache até perto do
expe peça outro só então. Não há refresh token noclient_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:
- Assinatura, com as chaves de
/jwks. Guarde-as em cache e busque de novo se a assinatura não bater. expenbf: o token não pode estar vencido nem ser de uso futuro.issigual aoissuerde/.well-known/openid-configuration. Token de outro emissor é recusado.audcontém o próprio client id do destino. Token feito para outro sistema é recusado.client_iddiferente do próprio client id do destino. Oclient_idé o client que pediu o token. Um token que o próprio destino pediu (por exemplo, o login de uma pessoa nele) temaudeclient_idiguais 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), 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.