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

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

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/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á.

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
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 /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 é 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`.
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'