Identidade (OIDC / OAuth 2.1)
https://auth.asender.net · OAuth 2.1 + PKCE · 8 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).
- Base URL:
https://auth.asender.net - Perfil: OAuth 2.1 — Authorization Code com PKCE
S256obrigatório. Sem implicit, sem password grant, semplain. - Assinatura: RS256. As chaves públicas ficam no JWKS; você valida os tokens sozinho, sem chamar o servidor a cada request.
É 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.
- Client público (SPA, mobile, CLI): não tem segredo. Quem prova que a troca do code é legítima é o PKCE. É o caso da maioria das integrações.
- Client confidencial (backend que guarda segredo com segurança): autentica no
/tokenpor HTTP Basic (client_secret_basic) ou pelos campos do corpo.
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
| escopo | o que traz |
|---|---|
openid | obrigatório — é o que faz o fluxo ser OIDC; é sempre garantido |
profile | o name do usuário no ID token / /userinfo |
email | o email e o email_verified |
offline_access | um 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:
- ID token (
id_token) — prova quem logou. É para o SEU app consumir: valide-o e leia os claims (sub,email,name). Nunca o mande para uma API como credencial. - Access token (
access_token) — é a credencial para chamar as APIs do Asender. Vai emAuthorization: Bearer …. Traztok_use: "access"; o/userinfoe as APIs recusam um ID token apresentado no lugar dele.
Validar o ID token (faça no client)
Não confie num token só porque ele chegou. Valide, nesta ordem:
- Assinatura — pegue a chave do JWKS (
/.well-known/jwks.json) cujokidcasa com o header do token; verifique a assinatura RS256. Rejeite qualqueralgfora deRS256(nunca aceitenone). iss— exatamentehttps://auth.asender.net.aud— exatamente o seuclient_id.exp/nbf— dentro da validade.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.
error | onde | quando |
|---|---|---|
invalid_request | /authorize, /token | falta PKCE S256, parâmetro ou corpo inválido |
unsupported_response_type | /authorize | response_type ≠ code |
invalid_client | /authorize, /token | client_id desconhecido ou client não autenticado (401 no /token) |
unsupported_grant_type | /token | grant_type fora de authorization_code/refresh_token |
invalid_grant | /token | code/refresh inválido, expirado, já usado; redirect_uri/PKCE não conferem; refresh reusado (família revogada) |
invalid_token | /userinfo | access token ausente/inválido, ou um ID token no lugar dele |
server_error | /authorize, /token | falha interna ao emitir |
Checklist de integração segura
- Cliente OIDC que consome o discovery e o JWKS, apontado para o
issuerhttps://auth.asender.net— não crave URLs nem chaves. - PKCE
S256sempre;stateenoncede CSPRNG, conferidos na volta. - Valide o ID token por inteiro (assinatura RS256 +
iss+aud+exp+nonce); rejeitealgfora de RS256. - Identifique o usuário pelo
sub, nunca pelo e-mail. - O access token chama as APIs; o ID token fica no app. Não troque os papéis.
- A troca do
codee orefreshmoram no backend/BFF; guarde o refresh mais recente e trateinvalid_grantcomo "refaça o login".
Referência
Toda operação registrada no código, com o que faz, o que recebe e o que devolve.
Discovery
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 é usada | verificação no client. |
|---|
Respostas
| código | significado |
|---|---|
200 | O JWK Set. |
Exemplo
curl -X GET 'https://auth.asender.net/.well-known/jwks.json' \
-H 'Authorization: Bearer SEU_TOKEN'
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 é usada | os clients leem isto no bootstrap para se autoconfigurar. Cacheável (muda só em deploy). |
|---|
Respostas
| código | significado |
|---|---|
200 | Os metadados do provedor. |
Exemplo
curl -X GET 'https://auth.asender.net/.well-known/openid-configuration' \
-H 'Authorization: Bearer SEU_TOKEN'
meta
Identidade do serviço.
| Onde é usada | rota raiz. |
|---|---|
| Efeitos | escreve 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ódigo | significado |
|---|---|
200 | OK |
Exemplo
curl -X GET 'https://auth.asender.net/' \
-H 'Authorization: Bearer SEU_TOKEN'
Versão e commit do binário em execução.
| Onde é usada | rota 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ódigo | significado |
|---|---|
200 | OK |
Exemplo
curl -X GET 'https://auth.asender.net/version' \
-H 'Authorization: Bearer SEU_TOKEN'
OAuth 2.1
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á.
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 é usada | o browser é redirecionado para cá pelo client (app) que quer logar. |
|---|
Parâmetros
| nome | em | obrigatório | descrição |
|---|---|---|---|
response_type | query | sim | único valor aceito (OAuth 2.1: sem implicit) |
client_id | query | sim | id do client OAuth registrado |
redirect_uri | query | sim | tem de casar EXATAMENTE com uma URI registrada no client |
scope | query | não | lista separada por espaço; `openid` é sempre garantido; `offline_access` pede refresh token |
state | query | não | opaco, devolvido intacto; use para amarrar a resposta ao request e barrar CSRF |
nonce | query | não | ligado ao ID token (anti-replay); o client confere que o `nonce` do id_token é o que enviou |
code_challenge | query | sim | PKCE: base64url(SHA-256(code_verifier)) |
code_challenge_method | query | sim | único método aceito |
Respostas
| código | significado |
|---|---|
302 | Redireciona 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'
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 é usada | o client manda o browser para cá ao sair. |
|---|
Parâmetros
| nome | em | obrigatório | descrição |
|---|---|---|---|
post_logout_redirect_uri | query | não | destino após sair; precisa estar registrado no client, senão é ignorado |
client_id | query | não | o client dono da allowlist de post-logout |
state | query | não | opaco, anexado ao `post_logout_redirect_uri` quando ele é aceito |
Respostas
| código | significado |
|---|---|
302 | Redireciona 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'
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.
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 é usada | o client troca aqui, servidor-a-servidor (ou pelo BFF), o code/refresh por tokens. |
|---|
Respostas
| código | significado |
|---|---|
200 | Tokens 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`. |
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
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 é usada | o client chama para hidratar o perfil. Sem token válido -> 401 invalid_token. |
|---|
Respostas
| código | significado |
|---|---|
200 | Os 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'