Emitir, renovar e inspecionar tokens — API
POST /oauth/token é o endpoint OAuth2 de tokens: o campo grant_type decide a operação —
trocar um authorization_code (PKCE) por tokens, autenticar máquina-a-máquina
(client_credentials) ou rotacionar um refresh_token. POST /oauth/introspect permite a um
client OAuth consultar se um access token ainda está ativo (RFC 7662).
Funcionamento
POST /oauth/token: valida o corpo (grant_type restrito a três valores + client_id) e
roteia por grant_type:
authorization_code— confere o código pelo hash (não consumido, não expirado), oclient_ide oredirect_uri, e o PKCE (SHA-256(code_verifier)em base64url deve igualar ocode_challenge, comparado comtimingSafeEqual). Emiteaccess_tokensempre;id_tokenapenas se o escopoopenidfoi pedido;refresh_tokenapenas se o client suporta o grantrefresh_token. O código é marcado como consumido (uso único).refresh_token— confere o refresh token pelo hash e peloclient_id; se já foi usado antes (reuso detectado) ou a sessão associada não está mais ativa, revoga toda a família de refresh tokens daquela sessão. Em sucesso, emite novoaccess_tokene rotaciona orefresh_token(mesma família); não reemiteid_token.client_credentials— confereclient_id+client_secret(hash comparado comtimingSafeEqual) e que o client tem um vínculo organizacional; emite umaccess_tokende máquina — nunca emiterefresh_tokenpara este grant.
POST /oauth/introspect: autentica o client chamador com client_id/client_secret e só
depois avalia o token informado, pelo mesmo caminho usado para autenticar qualquer requisição
(verificação de assinatura RS256 + status da sessão). Qualquer falha nessa verificação — token
inválido, expirado, sessão revogada — responde 200 { active: false }, sem erro HTTP. Só
funciona para tokens do tipo access token (JWT); um refresh token apresentado aqui sempre volta
active: false, porque não é um JWT verificável por assinatura.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| POST | /v1/oauth/token | Emite ou renova tokens (authorization_code, refresh_token, client_credentials) |
| POST | /v1/oauth/introspect | Consulta se um access token está ativo |
Versão: v1
Swagger:
Rota (Dev): http://localhost:3000/v1/oauth/token
Lógica de decisão por grant_type e da introspecção:
Permissões
| Rota | Guards | Acesso |
|---|---|---|
POST /oauth/token | nenhum | Público (a segurança vem do PKCE, do client_secret ou do refresh token) |
POST /oauth/introspect | nenhum (autenticação de client dentro do use-case) | Público, mediante client_id/client_secret válidos |
Não existe rota dedicada de revogação (/oauth/token/revoke, RFC 7009). O que existe é
DELETE /oauth/client-sessions/current, que revoga a sessão OAuth associada ao access token do
bearer atual — não recebe um token arbitrário no corpo, então não é equivalente ao RFC 7009. Essa
rota não está documentada em detalhe aqui (fora do escopo desta página).
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Content-Type | Sim | application/json |
Path parameters
Nenhum.
Query parameters
Nenhum.
Body
authorization_code — IdentityOAuthTokenRequest:
json{"grant_type": "authorization_code","client_id": "mobile-app","code": "opaque-authorization-code","code_verifier": "<verificador original em texto>","redirect_uri": "https://app.exemplo.com/callback"}
client_credentials:
json{"grant_type": "client_credentials","client_id": "ai-service","client_secret": "segredo-do-client","scope": "reports:read"}
refresh_token:
json{ "grant_type": "refresh_token", "client_id": "mobile-app", "refresh_token": "opaque-refresh-token" }
| Campo | Obrigatório | Validação |
|---|---|---|
grant_type | Sim | @IsIn(['authorization_code', 'client_credentials', 'refresh_token']) — só estes três valores; qualquer outro é rejeitado com 400 pelo ValidationPipe, antes de chegar ao controller |
client_id | Sim | @IsString (sem formato fixo) |
code, redirect_uri | Exigidos via @ValidateIf quando grant_type=authorization_code | — |
code_verifier | Exigido pelo caso de uso quando grant_type=authorization_code | regex ^[A-Za-z0-9\-._~]{43,128}$ |
client_secret | Exigido via @ValidateIf quando grant_type=client_credentials | — |
refresh_token | Exigido via @ValidateIf quando grant_type=refresh_token | — |
scope | Não | opcional em qualquer grant |
POST /oauth/introspect — IdentityOAuthIntrospectionRequest:
json{ "client_id": "ai-service", "client_secret": "segredo-do-client", "token": "access-token-jwt" }
| Campo | Obrigatório | Validação |
|---|---|---|
client_id | Sim | @IsString |
client_secret | Sim | @IsString |
token | Sim | @IsString |
token_type_hint | Não | @IsIn(['access_token', 'refresh_token']) — aceito no DTO mas ignorado pela implementação; qualquer token que não seja um JWT de access token válido responde active: false |
Response
200 — authorization_code / refresh_token (IdentityOAuthTokenResponse):
json{ "access_token": "eyJ...", "id_token": "eyJ...", "refresh_token": "opaque...", "token_type": "Bearer", "expires_in": 3600 }
client_credentials: sóaccess_token+token_type+expires_in— semrefresh_tokene semid_token.refresh_token:access_token+refresh_tokennovos — semid_token.
200 — POST /oauth/introspect, token ativo:
json{ "active": true, "sub": "8f2a...", "client_id": "web-portal", "scope": "openid profile", "iss": "http://localhost:3000", "aud": "identity-api", "iat": 1750000000, "exp": 1750003600, "token_type": "Bearer" }
200 — token inativo/inválido: { "active": false }
Erros
| Rota | Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|---|
/token | (validação de payload) | BAD_REQUEST | 400 | grant_type fora da lista fechada, ou campos do grant faltando |
/token (authorization_code) | (Value Objects) | IDENTITY_OAUTH_AUTHORIZATION_CODE_INVALID / IDENTITY_OAUTH_PKCE_VERIFIER_INVALID | 400 | code/code_verifier mal formados |
/token (authorization_code) | IdentityOAuthAuthorizationCodeRejectedError | IDENTITY_OAUTH_AUTHORIZATION_CODE_REJECTED | 400 | código inexistente/expirado/já usado, client_id/redirect_uri não batem, ou PKCE falhou |
/token (client_credentials) | IdentityOAuthClientCredentialsRejectedError | IDENTITY_OAUTH_CLIENT_CREDENTIALS_REJECTED | 401 | client_secret errado, client sem vínculo organizacional, ou escopo não permitido |
/token (refresh_token) | (repositório de refresh token) | IDENTITY_INVALID_REFRESH_TOKEN | 401 | token inexistente/expirado/reusado/sessão inativa (reuso e sessão inativa também revogam a família inteira) |
/introspect | IdentityInvalidOAuthClientCredentialsError | IDENTITY_INVALID_OAUTH_CLIENT_CREDENTIALS | 401 | client_id/client_secret do chamador não conferem |
Um grant_type fora da lista (invalid_grant, por exemplo) é barrado pelo ValidationPipe com
400 BAD_REQUEST genérico — não chega a existir um branch de "grant desconhecido" alcançável
via HTTP nesta rota.
Regras de negócio
| ID | Regra | Comportamento |
|---|---|---|
| RN-01 | Só 3 grants são aceitos pelo DTO | authorization_code, client_credentials, refresh_token — não há mais mobile_credentials/verify_qrcode_login neste endpoint (o QR-code tem fluxo próprio, ver Login por QR-code) |
| RN-02 | PKCE obrigatório no authorization_code | comparação timingSafeEqual do SHA-256(code_verifier) contra o code_challenge salvo |
| RN-03 | authorization_code é de uso único | consumido na troca; reuso → 400 IDENTITY_OAUTH_AUTHORIZATION_CODE_REJECTED |
| RN-04 | refresh_token é de uso único, com rotação | reuso do mesmo token revoga toda a família (proteção contra roubo de token) |
| RN-05 | client_credentials nunca emite refresh_token | por design — um client máquina não deve manter uma credencial de longa duração renovável |
| RN-06 | id_token só sai com escopo openid | tanto na troca de código quanto — não se aplica ao refresh, que nunca reemite id_token |
| RN-07 | Access/ID token têm TTL fixo de 3600s | constante no código, não é variável de ambiente |
| RN-08 | Introspecção nunca propaga erro de token inválido | qualquer motivo de invalidade vira {active:false} com HTTP 200; só a autenticação do client chamador pode gerar 401 |
| RN-09 | Introspecção só reconhece access tokens | token_type_hint é aceito mas ignorado; um refresh token sempre volta active:false |
Compliance
| Órgão / norma | Exigência | Como a rota atende |
|---|---|---|
| OAuth2 / OIDC (RFC 6749, 7636, 7662) | PKCE, código de uso único, rotação de refresh, introspecção | implementados conforme descrito acima |
| LGPD | não logar segredos | client_secret, code_verifier e tokens não aparecem em log |
| HIPAA | trilha de emissão/revogação de credenciais | reuso de refresh token gera evento de revogação de família |
Variáveis de ambiente
| Variável | Uso | Default |
|---|---|---|
IDENTITY_OAUTH_AUTHORIZATION_CODE_LIFETIME_SECONDS | TTL do authorization_code | 600s (máx. 3600) |
IDENTITY_REFRESH_TOKEN_LIFETIME_DAYS | validade do refresh_token | 30 dias (máx. 365) |
IDENTITY_SESSION_LIFETIME_DAYS | validade da sessão de client M2M (reaproveitada em client_credentials) | 30 dias |
IDENTITY_OAUTH_ISSUER / IDENTITY_OAUTH_AUDIENCE | claims iss/aud assinadas e verificadas | http://localhost:3000 / identity-api |
O TTL do access/id token (3600s) não é configurável — é uma constante no código
(identity-oauth-token-signer.service.ts).
Serviços consumidores
Reportado, não verificado neste levantamento (origem: card Bitrix 296, F-S01): o serviço de IA
da plataforma (mm-pacs-mobilemed-ai-api, repositório não disponível neste workspace) consumiria
este endpoint via client_credentials/refresh_token, com cache Redis (TTL 280s), padrão
single-flight e retry com backoff (até 4 tentativas, 1s/2s/4s). Não foi possível confirmar esse
comportamento no código deste repositório — registrado apenas como contexto reportado pela origem
citada.
Tempo médio de resposta
A confirmar — responsável: time de Identity; data: 24/09/2026. Sem medição publicada.
Requisitos não funcionais
| Requisito | Definição |
|---|---|
| Idempotência | Não — authorization_code e refresh_token são consumidos a cada troca |
| Rate limit | Não identificado @Throttle específico nestas duas rotas neste levantamento |
| Cache | Não, no código deste repositório (ver nota de serviços consumidores acima para um cache externo reportado) |
| Auditoria | Revogação de família de refresh token é auditada; não identificado evento de auditoria dedicado à emissão comum |
Divergências e lacunas confirmadas
- Não existe
/oauth/token/revoke(RFC 7009); a revogação disponível (DELETE /oauth/client-sessions/current) tem semântica diferente (revoga a sessão do bearer atual, não um token arbitrário informado no corpo). - Não existem specs unitários dedicados para os casos de uso de troca de código, refresh,
client_credentialsou introspecção — a cobertura vem de dois e2e (identity-oauth-discovery.e2e.spec.ts,identity-oauth-token-validation.e2e.spec.ts).
Relacionado
- 📂 Módulo: Authentication
- 🔑 Rota anterior: Autorizar aplicação (OAuth) — gera o
authorization_codetrocado aqui - 🔎 Descoberta OIDC — o
token_endpointe ointrospection_endpointsão anunciados lá