Identidade (OIDC / OAuth 2.1)

https://auth.asender.net · OAuth 2.1 + PKCE · 13 operações · openapi.yaml

Identidade — OIDC / OAuth 2.1

O Authorization Server do Asender. É daqui que sai o token que todas as outras APIs exigem: seu app não guarda a senha de ninguém nem valida um segundo fator — ele delega o login para auth.asender.net pelo fluxo padrão OpenID Connect / OAuth 2.1 + PKCE e recebe de volta dois tokens: um que prova quem logou (o ID token) e um que abre as APIs (o access token).

É só isto que é público. Esta página cobre os 6 endpoints de protocolo que um integrador usa. O resto do serviço de identidade (usuários, sessões, 2FA, autorização por ferramenta) é control plane interno, reservado aos serviços da casa — não é superfície de integração e não se documenta aqui.

Antes de começar: registre um client

Um client é o seu app do ponto de vista do login. Cada client tem um client_id e uma allowlist EXATA de redirect_uri — a comparação é caractere a caractere, sem curinga, porque é ela que impede que um code de login seja desviado para um endereço que não é seu.

O registro do client (id, redirect_uris, post-logout, tipo) é feito pela equipe da plataforma. Peça com: os redirect_uri exatos, se é público ou confidencial, e os post_logout_redirect_uri.

Discovery: descubra os endpoints, não os crave

O primeiro request de um client bem-feito é o de discovery. Em vez de cravar URLs, leia-as de:

curl 'https://auth.asender.net/.well-known/openid-configuration'
{
  "issuer": "https://auth.asender.net",
  "authorization_endpoint": "https://auth.asender.net/authorize",
  "token_endpoint": "https://auth.asender.net/token",
  "userinfo_endpoint": "https://auth.asender.net/userinfo",
  "jwks_uri": "https://auth.asender.net/.well-known/jwks.json",
  "end_session_endpoint": "https://auth.asender.net/logout",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["openid", "profile", "email", "offline_access"]
}

Toda biblioteca OIDC séria consome esse documento — aponte a sua para o issuer https://auth.asender.net e ela se autoconfigura. O documento é cacheável (Cache-Control: public, max-age=300); só muda em deploy ou rotação de chave.

Escopos

escopoo que traz
openidobrigatório — é o que faz o fluxo ser OIDC; é sempre garantido
profileo name do usuário no ID token / /userinfo
emailo email e o email_verified
offline_accessum refresh_token — peça só se o app precisa agir com a sessão fora da presença do usuário

O fluxo, passo a passo (Authorization Code + PKCE)

1. Gere o par PKCE

Antes de mandar o usuário para o login, gere um code_verifier aleatório (43–128 caracteres, base64url) e o seu code_challenge:

code_verifier  = base64url(random 32 bytes)
code_challenge = base64url( SHA-256( code_verifier ) )

Guarde o code_verifier do lado do app (na sessão do browser, por exemplo). Gere também um state aleatório (anti-CSRF) e, opcionalmente, um nonce (anti-replay do ID token). state e nonce vêm de um CSPRNG, nunca de Math.random().

2. Mande o browser para /authorize

Redirecione o usuário (navegação de browser, não fetch) para:

https://auth.asender.net/authorize
  ?response_type=code
  &client_id=app-exemplo
  &redirect_uri=https://app.exemplo.com.br/callback
  &scope=openid%20profile%20email%20offline_access
  &state=xyz123
  &nonce=n-0S6_WzA2Mj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256

O servidor valida o client_id e a redirect_uri, reaproveita a sessão do usuário no domínio de auth (ou o manda logar e volta) e redireciona de volta ao seu redirect_uri:

https://app.exemplo.com.br/callback?code=cod_9f8e7d…&state=xyz123

Confira o state antes de qualquer coisa: se não for o que você enviou, aborte. Se o login falhar depois da validação do redirect, a volta traz ?error=…&error_description=…&state=… em vez do code.

3. Troque o code por tokens em /token

Do backend (ou BFF), troque o code pelos tokens, provando o PKCE com o code_verifier que você guardou. Corpo x-www-form-urlencoded:

curl -X POST 'https://auth.asender.net/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'code=cod_9f8e7d…' \
  -d 'redirect_uri=https://app.exemplo.com.br/callback' \
  -d 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \
  -d 'client_id=app-exemplo'

Resposta 200 (Cache-Control: no-store):

{
  "access_token": "eyJhbGciOiJSUzI1Ni…",
  "token_type": "Bearer",
  "expires_in": 900,
  "id_token": "eyJhbGciOiJSUzI1Ni…",
  "refresh_token": "rft_1a2b3c…",
  "scope": "openid profile email offline_access"
}

O redirect_uri aqui tem de ser idêntico ao do passo 2 (o servidor amarra o code àquele valor — é o que barra a injeção de code). O refresh_token só vem se o scope pediu offline_access. Client confidencial acrescenta a autenticação:

  -u 'app-exemplo:CLIENT_SECRET'      # HTTP Basic (client_secret_basic)

Os dois tokens, e o que fazer com cada um

O /token devolve dois JWT RS256, e a diferença entre eles é de segurança, não de detalhe:

Validar o ID token (faça no client)

Não confie num token só porque ele chegou. Valide, nesta ordem:

  1. Assinatura — pegue a chave do JWKS (/.well-known/jwks.json) cujo kid casa com o header do token; verifique a assinatura RS256. Rejeite qualquer alg fora de RS256 (nunca aceite none).
  2. iss — exatamente https://auth.asender.net.
  3. aud — exatamente o seu client_id.
  4. exp / nbf — dentro da validade.
  5. nonce — igual ao que você enviou no /authorize.
curl 'https://auth.asender.net/.well-known/jwks.json'
{ "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "2026-08a", "n": "0vx7…", "e": "AQAB" } ] }

Cacheie o JWKS e recarregue quando aparecer um kid desconhecido — é assim que uma rotação de chave funciona sem downtime: a chave nova entra no JWKS antes de ser usada, e a antiga fica até os tokens que ela assinou expirarem. Uma boa biblioteca OIDC faz tudo isto para você a partir do issuer.

Claims disponíveis no ID token: sub, iss, aud, exp, iat, nonce, auth_time, email, name, tenant. O sub é o ID interno imutável do usuário — é por ele que você o identifica no seu lado, nunca pelo e-mail (uma pessoa pode ter vários e-mails, e o primário pode mudar).

/userinfo — o perfil a partir do access token

Alternativa (ou complemento) ao ID token para hidratar o perfil:

curl 'https://auth.asender.net/userinfo' \
  -H 'Authorization: Bearer eyJhbGciOiJSUzI1Ni…'
{ "sub": "usr_01J…", "email": "pessoa@empresa.com.br", "email_verified": true, "name": "Fulana" }

Bearer ausente, inválido, expirado — ou um ID token no lugar do access — devolve 401 invalid_token.

Manter a sessão viva: refresh token

Com offline_access, você recebe um refresh_token opaco. Quando o access expirar (expires_in), troque o refresh por tokens novos:

curl -X POST 'https://auth.asender.net/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  -d 'refresh_token=rft_1a2b3c…' \
  -d 'client_id=app-exemplo'

Rotação com detecção de reuso. Cada refresh é de uso único: usá-lo invalida aquele e devolve um novo — guarde sempre o mais recente. Se um refresh já usado for reapresentado (sinal clássico de token roubado), o servidor revoga a família inteira e responde 400 invalid_grant; a partir daí, o usuário precisa logar de novo. Trate invalid_grant no refresh como "recomece o fluxo do /authorize", não como erro a repetir.

Sair: logout

Para encerrar a sessão, mande o browser ao end_session_endpoint:

https://auth.asender.net/logout
  ?client_id=app-exemplo
  &post_logout_redirect_uri=https://app.exemplo.com.br/adeus
  &state=xyz123

O logout é server-side e irreversível (piso 17): revoga a sessão e a família de refresh, não só apaga o cookie — um refresh emitido antes não volta a valer. O redirecionamento só vai para o post_logout_redirect_uri se ele estiver na allowlist do client; senão, cai na tela de login.

Erros

/token e /userinfo usam o envelope OAuth ({ "error", "error_description" }). No /authorize, o erro volta por 302 com ?error= depois que a redirect_uri foi validada; antes disso (client/redirect desconhecidos) a falha é mostrada, não redirecionada — devolver a uma URI não registrada seria open-redirect.

errorondequando
invalid_request/authorize, /tokenfalta PKCE S256, parâmetro ou corpo inválido
unsupported_response_type/authorizeresponse_type ≠ code
invalid_client/authorize, /tokenclient_id desconhecido ou client não autenticado (401 no /token)
unsupported_grant_type/tokengrant_type fora de authorization_code/refresh_token
invalid_grant/tokencode/refresh inválido, expirado, já usado; redirect_uri/PKCE não conferem; refresh reusado (família revogada)
invalid_token/userinfoaccess token ausente/inválido, ou um ID token no lugar dele
server_error/authorize, /tokenfalha interna ao emitir

Checklist de integração segura

Referência

Toda operação registrada no código, com o que faz, o que recebe e o que devolve.

acoes-sensiveis

GET /acoes-sensiveis/{id} sem schema na spec

Consultar atende GET /v1/acoes-sensiveis/{id}: o solicitante ou um admin do tenant.

Exemplo

curl -X GET 'https://auth.asender.net/acoes-sensiveis/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'

Discovery

GET /.well-known/jwks.json

Chaves públicas (JWKS)

As chaves públicas RS256 do provedor. O client busca aqui para validar a assinatura do ID token (e de qualquer JWT deste AS), escolhendo a chave pelo kid do header do token. Cacheável; numa rotação, o JWKS traz a chave nova e a antiga convivem até os tokens velhos expirarem.

Onde é usadaverificação no client.

Respostas

códigosignificado
200O JWK Set.

Exemplo

curl -X GET 'https://auth.asender.net/.well-known/jwks.json' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /.well-known/oauth-authorization-server

Metadados do Authorization Server (RFC 8414)

O MESMO documento de /.well-known/openid-configuration (uma fonte só, nunca divergem), servido no caminho do RFC 8414 para clientes OAuth/MCP que não falam OIDC. Anuncia token_endpoint_auth_methods_supported (client_secret_basic, client_secret_post, none) e authorization_response_iss_parameter_supported. Anuncia registration_endpoint (RFC 7591, POST /register) quando OAUTH_DCR_ATIVO=true e client_id_metadata_document_supported: true (CIMD) quando OAUTH_CIMD_ATIVO=true — cada um some do documento se desligado.

Onde é usadaclientes MCP (spec 2025-11-25) descobrem o AS aqui a partir do authorization_servers do recurso (RFC 9728).

Respostas

códigosignificado
200Os metadados do Authorization Server.

Exemplo

curl -X GET 'https://auth.asender.net/.well-known/oauth-authorization-server' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /.well-known/openid-configuration

Metadados do provedor OIDC (discovery)

Anuncia os endpoints e as capacidades do Authorization Server. É o primeiro request de um client: ele se autoconfigura a partir daqui, em vez de ter as URLs cravadas. Só response_type=code, só PKCE S256, só RS256 — o perfil OAuth 2.1 da casa.

Onde é usadaos clients leem isto no bootstrap para se autoconfigurar. Cacheável (muda só em deploy).

Respostas

códigosignificado
200Os metadados do provedor.

Exemplo

curl -X GET 'https://auth.asender.net/.well-known/openid-configuration' \
  -H 'Authorization: Bearer SEU_TOKEN'

meta

GET /

Identidade do serviço.

Onde é usadarota raiz.
Efeitosescreve a resposta. Serve para saber QUAL binário está rodando quando o comportamento diverge do esperado — a primeira pergunta de todo diagnóstico de deploy.

Respostas

códigosignificado
200OK

Exemplo

curl -X GET 'https://auth.asender.net/' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /version

Versão e commit do binário em execução.

Onde é usadarota pública, usada por ops e por quem investiga incidente. Pública porque a primeira pergunta de todo incidente é "que versão está no ar?", e ela tem de ser respondível sem credencial — quem investiga muitas vezes ainda não tem uma. O que sai é só isso: nunca configuração, nunca endereço interno.

Respostas

códigosignificado
200OK

Exemplo

curl -X GET 'https://auth.asender.net/version' \
  -H 'Authorization: Bearer SEU_TOKEN'

OAuth 2.1

GET /authorize

Início do login (Authorization Code + PKCE)

O endpoint para onde o app manda o browser iniciar o login. Valida o client_id e a redirect_uri (allowlist EXATA do client) e exige PKCE S256. Se houver sessão viva no domínio de auth, emite um authorization code de uso único e devolve por 302 ao redirect_uri (?code=…&state=…). Sem sessão, redireciona para a tela de login e, após autenticar, volta para cá.

client_id em forma de URL (CIMD, ADR-0130 D5). Se o client_id é uma URL https (porta 443, sem credencial nem fragmento), o AS busca o documento JSON de metadados naquela URL (só IP público; sem redirect; 5 s; ≤ 64 KiB; client_id do documento igual à URL), cria/atualiza o client (origem=cimd, público, não verificado, só escopos de leitura) e segue o fluxo. Cache de 5 min a 24 h (Cache-Control: max-age); se a rebusca falhar por rede, usa a cópia de até 7 dias. Documento inválido, destino interno ou domínio bloqueado => 400 invalid_client (sem redirect). Muitas buscas do mesmo IP => 429.

Apps de terceiro (ADR-0128): o scope é validado contra o catálogo e contra o que o app pode pedir (invalid_scope por redirect); na 1ª vez o usuário passa pela tela de consentimento (302 para <authfront>/consentir?pedido=…), onde escolhe UMA conta; nas seguintes, com sessão viva e um consentimento que cobre os escopos, o code sai na hora (o "1 clique"). prompt=none devolve login_required / consent_required; prompt=consent força a tela; prompt=login força o login. Toda resposta ao app de terceiro (code ou erro) traz iss (RFC 9207). Recurso por audiência (RFC 8707): resource (repetível) escolhe a(s) API(s) que o token vai acessar; cada valor tem de estar na allowlist OIDC_RECURSOS_PERMITIDOS (padrão https://api.asender.net e https://api.asender.net/mcp), senão error=invalid_target por redirect (com state e iss). Sem resource o aud é o recurso público padrão.

Loopback (RFC 8252 §7.3): para client PÚBLICO, redirect_uri de loopback (http://127.0.0.1, http://[::1], http://localhost) casa com a cadastrada ignorando só a porta; esquema, host, caminho e query seguem exatos. Confidencial continua exato.

Este endpoint é uma navegação de browser, não uma chamada de API: a resposta normal é 302. Erros só voltam por 302 com ?error= DEPOIS que a redirect_uri foi validada; antes disso (client/redirect desconhecidos) a falha é mostrada, não redirecionada — devolver a uma URI não registrada seria open-redirect.

Onde é usadao browser é redirecionado para cá pelo client (app) que quer logar.

Parâmetros

nomeemobrigatóriodescrição
response_typequery simúnico valor aceito (OAuth 2.1: sem implicit)
client_idquery simid do client OAuth registrado
redirect_uriquery simtem de casar EXATAMENTE com uma URI registrada no client
scopequery nãolista separada por espaço; `openid` é sempre garantido; `offline_access` pede refresh token
statequery nãoopaco, devolvido intacto; use para amarrar a resposta ao request e barrar CSRF
noncequery nãoligado ao ID token (anti-replay); o client confere que o `nonce` do id_token é o que enviou
promptquery nãosó apps de terceiro: `none` (sem tela; `login_required`/`consent_required`), `consent` (força a tela), `login` (força o login). `none` não combina com outros
resourcequery nãoRFC 8707, repetível, só apps de terceiro: recurso(s) do token; fora da allowlist => `invalid_target`
code_challengequery simPKCE: base64url(SHA-256(code_verifier))
code_challenge_methodquery simúnico método aceito

Respostas

códigosignificado
302Redireciona ao `redirect_uri` com `code` e `state` (sucesso) ou com `error`/`error_description` e `state` (falha após validar o redirect); ou à tela de login quando não há sessão.
400`client_id` desconhecido ou `redirect_uri` não registrada — a falha é mostrada aqui, não redirecionada.

Exemplo

curl -X GET 'https://auth.asender.net/authorize' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /logout

Logout iniciado pelo app (end session)

O end_session_endpoint (RP-initiated logout). Revoga a sessão server-side (piso 17: irreversível — não só o cookie) e a família de refresh dela, limpa o cookie e redireciona. Vai para o post_logout_redirect_uri SOMENTE se ele estiver na allowlist do client; caso contrário, para a tela de login. É uma navegação de browser.

Onde é usadao client manda o browser para cá ao sair.

Parâmetros

nomeemobrigatóriodescrição
post_logout_redirect_uriquery nãodestino após sair; precisa estar registrado no client, senão é ignorado
client_idquery nãoo client dono da allowlist de post-logout
statequery nãoopaco, anexado ao `post_logout_redirect_uri` quando ele é aceito

Respostas

códigosignificado
302Redireciona ao `post_logout_redirect_uri` (se permitido) ou à tela de login.

Exemplo

curl -X GET 'https://auth.asender.net/logout' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /oauth/introspect

Introspecção de token (RFC 7662)

Diz se um token do próprio client está ativo. Só aceita client confidencial autenticado (Basic ou corpo): um client público não prova quem é. Token inválido, expirado, revogado, de outro client — ou de um grant que o usuário revogou — responde {"active": false} e nada mais. Access token de terceiro só é ativo enquanto o grant segue ativo.

Respostas

códigosignificado
200Estado do token.
400`invalid_request` — corpo inválido ou sem `token`.
401`invalid_client` — client não autenticado ou não confidencial.

Exemplo

curl -X POST 'https://auth.asender.net/oauth/introspect' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /oauth/revoke

Revoga um refresh token (RFC 7009)

O client revoga um refresh token seu. Token desconhecido, de outro client ou já revogado responde 200 igual (RFC 7009 §2.2 — não vaza se existe). O access token é stateless e vive no máximo 15 min: a revogação efetiva do acesso é a do refresh (ou do grant, na tela "Apps conectados"). O client se autentica por HTTP Basic (client_secret_basic) ou pelos campos client_id/client_secret do corpo.

Saídas200 vazio; 401 invalid_client se o client não se autentica.

Respostas

códigosignificado
200Revogado (ou nada a revogar).
400`invalid_request` — corpo inválido ou sem `token`.
401`invalid_client` — client não autenticado.

Exemplo

curl -X POST 'https://auth.asender.net/oauth/revoke' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /register

Registro dinâmico de client (RFC 7591)

Registra um client público sem cadastro prévio (clientes MCP como Claude, ChatGPT e Cursor que ainda não fazem CIMD). Corpo application/json (≤ 64 KiB) com os metadados do RFC 7591: redirect_uris (obrigatório; https ou http em 127.0.0.1, localhost, [::1], sem credencial nem fragmento, até 10), client_name, client_uri, logo_uri (https), grant_types (⊆ authorization_code, refresh_token), response_types (code), token_endpoint_auth_method (só none; outro valor, inclusive private_key_jwt, é invalid_client_metadata) e scope (o que não é identidade ou leitura é descartado; a resposta traz o scope efetivo).

O client nasce origem=dcr, publicado, não verificado (a tela de consentimento mostra o selo "Não verificado" e o domínio dos redirects). Resposta 201 com client_id (dcr_…, CSPRNG) e client_id_issued_at. Não há client_secret (cliente público) e não há registration_access_token nem registration_client_uri: a gestão do registro (RFC 7592) não é suportada — para mudar, registre de novo; a equipe suspende pelo backoffice. Limite de 10 registros por hora por IP (429 + Retry-After). Domínio de redirect_uris bloqueado => 400 invalid_redirect_uri. Desligado (OAUTH_DCR_ATIVO=false) => 404.

Respostas

códigosignificado
201Client registrado (sem segredo).
400`{"error":"invalid_redirect_uri"|"invalid_client_metadata","error_description":"…"}` (RFC 7591 §3.2.2).
404Registro dinâmico desligado.
429Limite de registros por hora do IP atingido (`Retry-After`).

Exemplo

curl -X POST 'https://auth.asender.net/register' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /token

Troca de code/refresh por tokens

Troca o authorization_code por tokens (provando o PKCE code_verifier) ou rotaciona um refresh_token. Chamado servidor-a-servidor (ou pelo BFF), com corpo application/x-www-form-urlencoded.

Autenticação do client. Client público (SPA/mobile) não tem segredo — o PKCE prova a posse. Client confidencial autentica por HTTP Basic (client_secret_basic) ou pelos campos client_id/client_secret do corpo.

Apps de terceiro (ADR-0128): client confidencial (segredo) ou público (sem segredo; PKCE; mandar client_secret => invalid_client; refresh de no máximo 30 dias). O access token sai com aud = os recursos concedidos (RFC 8707; sem resource, a API pública OIDC_RECURSO_PUBLICO), azp = o client, tenant = a conta do consentimento, scope = o escopo EFETIVO (pedido ∩ permitido no app ∩ concedido), ext: true (nunca vale como sessão) e jti; vive 15 min. O refresh é preso ao grant (não à sessão do navegador), vive o TTL do app e é revogado quando o usuário remove o app. Grant revogado ou usuário sem acesso à conta → invalid_grant.

Rotação de refresh com detecção de reuso. Cada uso do refresh o invalida e emite um novo. Reapresentar um refresh já usado revoga a família inteira e responde invalid_grant — o sinal de token roubado.

Onde é usadao client troca aqui, servidor-a-servidor (ou pelo BFF), o code/refresh por tokens.

Respostas

códigosignificado
200Tokens emitidos.
400`invalid_grant` (code/refresh inválido, expirado, já usado, ou redirect_uri/PKCE que não conferem; refresh reusado → família revogada; refresh de SESSÃO encerrada — revogada ou expirada — → família revogada, "sessão encerrada"), `invalid_request` (corpo inválido) ou `unsupported_grant_type`, ou `invalid_target` (apps de terceiro: `resource` fora do concedido ou da allowlist).
401`invalid_client` — client não autenticado.

Exemplo

curl -X POST 'https://auth.asender.net/token' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

OpenID Connect

GET /userinfo

Claims do usuário do access token

Devolve os claims do usuário dono do access token apresentado. Valida o Bearer por inteiro (RS256, iss, exp) e exige tok_use: "access" — um ID token NÃO é aceito aqui. Use para hidratar o perfil depois do login.

Onde é usadao client chama para hidratar o perfil. Sem token válido -> 401 invalid_token.

Respostas

códigosignificado
200Os claims do usuário.
401`invalid_token` — Bearer ausente, inválido, ou não é um access token.

Exemplo

curl -X GET 'https://auth.asender.net/userinfo' \
  -H 'Authorization: Bearer SEU_TOKEN'