Skip to main content

Autorizar um client OAuth — API

Endpoint de autorização OAuth2/OIDC. O GET redireciona o navegador para a experiência de login/consentimento externa, preservando os parâmetros OAuth recebidos. O POST (com um access token de usuário) e o POST /oauth/authorize/session (chamado por um serviço interno com X-Service-Token) emitem o authorization_code de fato, via PKCE. O código gerado é trocado depois em POST /oauth/token.

Funcionamento​

GET /oauth/authorize: valida o pedido (client, redirect_uri, escopos) sem autenticar ninguém, resolve o nome do client e redireciona (303) para a URL de início da experiência de autorização do navegador (IDENTITY_OAUTH_BROWSER_AUTHORIZATION_START_URL), repassando todos os parâmetros originais mais client_name. Não lê cookie nem sessão — é apenas o ponto de entrada.

POST /oauth/authorize: exige um access token de usuário válido (Authorization: Bearer). Valida o client (ativo, com grant authorization_code), o redirect_uri (contra as URLs cadastradas do client) e os escopos pedidos; confirma que o usuário do token está ativo e que a sessão associada ainda está ativa; então emite o authorization_code (PKCE, S256 obrigatório).

POST /oauth/authorize/session: variante para uso interno (BFF), autenticada por X-Service-Token em vez de um access token de usuário. Recebe uma decision (approve ou deny) e, quando approve, os IDs de sessão/usuário diretamente no corpo — não precisa de um access token de usuário. deny responde com error: access_denied sem emitir nenhum código.

SSO/prompt continuam sem implementação. Não há leitura de sessão de navegador nem parâmetro prompt em nenhum DTO desta rota — a pendência de SSO "sessão válida → autoriza sem pedir login de novo" segue em aberto.

Endpoints​

MétodoRotaDescrição
GET/v1/oauth/authorizeRedireciona para a experiência de login/consentimento
POST/v1/oauth/authorizeEmite o authorization_code usando o access token do usuário
POST/v1/oauth/authorize/sessionEmite (ou nega) o authorization_code a partir de uma sessão, via serviço interno

Versão: v1

Swagger: Identity — OAuth authorization · Rota (Dev): http://localhost:3000/v1/oauth/authorize

Lógica de decisão das três rotas:

Permissões​

RotaGuardsAcesso
GET /oauth/authorizenenhumPúblico
POST /oauth/authorizeIdentityOAuthAccessTokenGuardUsuário autenticado (usa o próprio access token)
POST /oauth/authorize/sessionIdentityOAuthBffServiceTokenGuardServiço interno com X-Service-Token válido

Headers​

HeaderObrigatórioDescrição
AuthorizationSim (POST /oauth/authorize)Bearer <access_token>
X-Service-TokenSim (POST /oauth/authorize/session)Token de serviço configurado (identity.internalApi.bffServiceToken)

Path parameters​

Nenhum.

Query parameters​

GET /oauth/authorize (todos repassados ao redirect):

NomeTipoObrigatórioValidação
response_typestringSim@IsIn(['code'])
client_idstringSim@IsUUID
redirect_uristringSim@IsUrl({ require_tld: false })
code_challengestringSim@Length(43, 128)
code_challenge_methodstringSim@IsIn(['S256'])
scopestringNão@Length(1, 1024)
statestringNão@Length(1, 512)
noncestringNão@Length(1, 255)

Body​

POST /oauth/authorize — IdentityOAuthAuthorizationRequest:

json
{
"client_id": "mobile-app",
"redirect_uri": "https://app.exemplo.com/callback",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
"scope": "openid profile",
"state": "random-state",
"nonce": "random-nonce"
}
CampoTipoObrigatórioValidação
client_idstringSim@Length(1, 64) — não exige UUID aqui (diferente da query do GET)
redirect_uristringSim@IsUrl
code_challengestringSim@Length(43, 128)
code_challenge_methodstringSim@IsIn(['S256'])
scopestringNão@Length(1, 1024)
statestringNão@Length(1, 512)
noncestringNão@Length(1, 255)

POST /oauth/authorize/session: os mesmos campos acima, mais:

CampoTipoObrigatórioValidação
decision"approve" | "deny"Sim@IsIn
identity_session_idstringSó quando decision: "approve"@ValidateIf + @IsUUID
user_idstringSó quando decision: "approve"@ValidateIf + @IsUUID

Response​

GET — 303 redirect:

text
Location: {IDENTITY_OAUTH_BROWSER_AUTHORIZATION_START_URL}?client_id=...&client_name=...&redirect_uri=...&code_challenge=...&response_type=...&state=...

POST /oauth/authorize e POST /oauth/authorize/session (approve) — 200:

json
{ "code": "opaque-authorization-code", "state": "random-state", "redirect_uri": "https://app.exemplo.com/callback" }

POST /oauth/authorize/session (deny) — 200:

json
{ "error": "access_denied", "redirect_uri": "https://app.exemplo.com/callback", "state": "random-state" }

O authorization_code é opaco (32 bytes aleatórios em base64url), tem uso único e TTL configurável (IDENTITY_OAUTH_AUTHORIZATION_CODE_LIFETIME_SECONDS); só o hash SHA-256 dele é persistido.

Erros​

Classe de erroerrorCodeStatusQuando ocorre
(validação de payload)BAD_REQUEST400corpo/query inválidos
IdentityOAuthAuthorizationDeniedErrorIDENTITY_OAUTH_AUTHORIZATION_DENIED400client inexistente/inativo, redirect_uri não cadastrada, escopo não permitido, usuário/sessão inativos
IdentityInvalidOAuthAccessTokenErrorIDENTITY_INVALID_OAUTH_ACCESS_TOKEN401access token ausente/inválido/expirado (POST /oauth/authorize)
(guard de serviço)IDENTITY_OAUTH_BFF_UNAUTHENTICATED401X-Service-Token ausente/inválido (POST /oauth/authorize/session)

O Swagger do GET também lista 404 IDENTITY_OAUTH_CLIENT_NOT_FOUND como exemplo, mas o comportamento real para client inexistente é sempre 400 IDENTITY_OAUTH_AUTHORIZATION_DENIED — divergência de documentação, não de comportamento.

Regras de negócio​

IDRegraComportamento
RN-01Apenas PKCE S256 é aceitooutro método → rejeitado na validação do DTO (@IsIn(['S256']))
RN-02client_id deve existir, estar ativo e suportar authorization_codesenão → 400 IDENTITY_OAUTH_AUTHORIZATION_DENIED
RN-03redirect_uri deve casar com as URLs cadastradas do clientdivergência → mesmo erro acima
RN-04scope pedido deve estar entre os escopos permitidos do clientinválido → mesmo erro acima
RN-05POST /oauth/authorize exige usuário e sessão ativos no momento da emissãoqualquer um inativo → 400
RN-06authorization_code é de uso únicoTTL configurável; consumido na troca em /oauth/token
RN-07deny em /authorize/session não emite códigoresposta access_denied, sem efeito colateral
RN-08SSO e prompt não implementadostoda autorização passa pela experiência externa de login; pendência confirmada, não resolvida nesta branch

Compliance​

Órgão / normaExigênciaComo a rota atende
OAuth2 / OIDCPKCE obrigatório (S256), redirect_uri exatovalidação de code_challenge_method e de redirect_uri contra cadastro
LGPDminimizaçãoapenas client_id/redirect_uri/escopos são processados; nenhum dado clínico envolvido
HIPAAautorização controlada e rastreávelclient e sessão validados antes da emissão do código

Variáveis de ambiente​

VariávelUsoDefault
IDENTITY_OAUTH_AUTHORIZATION_CODE_LIFETIME_SECONDSTTL do authorization_code600s (máx. 3600)
IDENTITY_OAUTH_BROWSER_AUTHORIZATION_START_URLdestino do redirect do GEThttp://localhost:3000/v1/auth/oauth/start
IDENTITY_OAUTH_ISSUERusado na montagem de URLs relacionadashttp://localhost:3000

Tempo médio de resposta​

A confirmar — responsável: time de Identity; data: 24/09/2026. Sem medição publicada.

Requisitos não funcionais​

RequisitoDefinição
IdempotênciaNão — cada POST aprovado gera um novo authorization_code
Rate limitNão identificado @Throttle específico nestas rotas neste levantamento
CacheNão
AuditoriaNão identificado evento de auditoria dedicado nesta rota neste levantamento

Divergências e lacunas confirmadas​

  • Não foi possível confirmar, neste levantamento, qual dos dois caminhos (POST /oauth/authorize com access token de usuário, ou POST /oauth/authorize/session com token de serviço) é o usado pelo front hoje para obter o primeiro authorization_code logo após o login — ver nota em Visão geral do módulo.
  • O GET /oauth/authorize não recebeu, neste levantamento, confirmação de teste automatizado cobrindo o redirect em si (o spec do controller cobre principalmente o POST).

Relacionado​