Recuperar, redefinir e trocar senha — API
Cobre os três fluxos de senha do Portal 2.0: esqueci minha senha (gera um token e envia e-mail), redefinir com o token (troca a senha usando o link recebido) e trocar a senha estando autenticado (exige a senha atual). Os dois primeiros são públicos; o terceiro exige um access token válido.
Funcionamento
Esqueci a senha (POST /authentication/password/forgot): busca o usuário pelo e-mail
normalizado (ou pelo e-mail de recuperação, se destination: "recovery"); se existir e tiver
senha cadastrada, gera um token de recuperação, invalida qualquer desafio de reset anterior ainda
não usado, salva o hash do novo token e envia um e-mail com o link. A resposta é sempre
genérica de sucesso, exista ou não a conta (anti-enumeração) — inclusive se o envio do e-mail
falhar (a falha só é registrada em log).
Redefinir com o token (POST /authentication/password/reset): valida o formato do token e a
política de senha antes de tocar no banco; localiza o desafio pelo hash do token, confere que
não expirou, troca a senha (bcrypt), zera o contador de falhas de login da conta e invalida o
token. Em sucesso, envia um e-mail de confirmação (best-effort).
Trocar a senha autenticado (PATCH /authentication/password): exige a senha atual e a
confere antes de trocar; também zera o contador de falhas de login, mas não envia e-mail de
confirmação.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| POST | /v1/authentication/password/forgot | Solicita recuperação: gera token e envia e-mail |
| POST | /v1/authentication/password/reset | Redefine a senha usando o token do e-mail |
| PATCH | /v1/authentication/password | Troca a senha do usuário autenticado |
Versão: v1
Swagger:
POST /authentication/password/forgotPOST /authentication/password/resetPATCH /authentication/password
Rota (Dev): http://localhost:3000/v1/authentication/password...
Lógica de decisão das três rotas:
Permissões
| Rota | Guards | Acesso |
|---|---|---|
POST /authentication/password/forgot | nenhum (rate limit) | Público |
POST /authentication/password/reset | nenhum (rate limit) | Público, mediante posse do token |
PATCH /authentication/password | IdentityOAuthAccessTokenGuard | Usuário autenticado (sobre si mesmo) |
Nenhuma dessas rotas usa IdentityOriginGuard.
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Content-Type | Sim | application/json |
Authorization | Sim (PATCH /password) | Bearer <access_token> |
Path parameters
Nenhum.
Query parameters
Nenhum.
Body
Forgot — IdentityPasswordResetRequest:
json{ "email": "usuario@exemplo.com", "client_id": "web-portal", "destination": "primary" }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
email | string | Sim | @IsEmail |
client_id | string | Sim | @Length(1, 128) |
destination | "primary" | "recovery" | Não | @IsIn — para qual e-mail enviar o link |
Reset — IdentityPasswordResetConfirmation:
json{ "token": "reset-token-recebido-por-email", "new_password": "NovaSenha123!", "client_id": "web-portal" }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
token | string | Sim | @Length(1, 512) |
new_password | string | Sim | @Length(8, 128) no DTO — a política real (8 a 16 caracteres, ver Regras) é aplicada depois |
client_id | string | Sim | @Length(1, 128) |
Trocar autenticado — IdentityPasswordChangeRequest:
json{ "current_password": "SenhaAtual123!", "new_password": "NovaSenha123!" }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
current_password | string | Sim | @IsString (sem limite de tamanho no DTO) |
new_password | string | Sim | @IsString (a política de senha é aplicada dentro do serviço, não no DTO) |
Nenhuma das três rotas tem campo de confirmação/repetição de senha — se o front pedir para repetir, essa checagem é só de interface, não é revalidada no backend.
Response
Forgot — 200 (sempre, exista ou não a conta):
json{ "message": "If an account with that email exists, a password reset link has been sent." }
Reset — 200: { "message": "Password reset completed." }
Trocar autenticado — 200: { "message": "Password changed." }
Erros
| Rota | Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|---|
| forgot | (validação de payload) | BAD_REQUEST | 400 | email inválido |
| forgot | ThrottlerException | RATE_LIMIT_EXCEEDED | 429 | limite excedido |
| reset | (validação de formato) | IDENTITY_RECOVERY_TOKEN_INVALID | 400 | token não tem o formato esperado (base64url, 43 caracteres) |
| reset | (política de senha) | INVALID_PASSWORD | 400 | new_password fora da política (ver Regras) |
| reset | IdentityInvalidRecoveryTokenError | IDENTITY_INVALID_RECOVERY_TOKEN | 401 | token bem formado mas inexistente, expirado ou já usado |
| trocar autenticado | IdentityInvalidOAuthAccessTokenError | IDENTITY_INVALID_OAUTH_ACCESS_TOKEN | 401 | access token ausente/inválido |
| trocar autenticado | IdentityInvalidCredentialsError | IDENTITY_INVALID_CREDENTIALS | 401 | current_password não confere |
| trocar autenticado | (política de senha) | INVALID_PASSWORD | 400 | new_password fora da política |
O token inválido/expirado no reset responde 401, não 404. A mensagem devolvida é
"Authentication required"(herdada deUnauthenticatedError) — não existe a string "Invalid or expired reset token" no código atual.
Regras de negócio
| ID | Regra | Comportamento |
|---|---|---|
| RN-01 | Forgot é anti-enumeração total | usuário inexistente, sem senha cadastrada, ou sem e-mail de recuperação confirmado → sempre 200 genérico |
| RN-02 | Token de reset é aleatório e guardado por hash | randomBytes(32) codificado em base64url (43 caracteres); persiste o SHA-256 do token, nunca o valor puro |
| RN-03 | Token de reset expira por TTL | IDENTITY_PASSWORD_RECOVERY_TTL_MINUTES (default 60 min) |
| RN-04 | Política de senha: 8 a 16 caracteres | mínimo 8, minúscula + maiúscula + dígito + especial (@$!%*?&) — há teto de 16, não é ilimitado |
| RN-05 | Falha no envio do e-mail não quebra o forgot | erro só é logado; resposta continua 200 |
| RN-06 | Reset e troca autenticada zeram o bloqueio de login | failedLoginAttempts, lockedUntil e lastFailedLoginAt voltam a 0/null nos dois casos |
| RN-07 | Reset envia e-mail de confirmação pós-troca | best-effort (falha só é logada); e-mail: "Sua senha da Mobilemed foi alterada" |
| RN-08 | Trocar autenticado não envia e-mail de confirmação | diferente do reset — o serviço de troca autenticada não dispara e-mail nenhum |
| RN-09 | Trocar autenticado exige a senha atual | senha atual errada → 401 IDENTITY_INVALID_CREDENTIALS, sem contar como tentativa de bloqueio de login |
| RN-10 | Sem confirmação de senha no backend | repetir a senha é validação apenas de interface, não existe campo equivalente no backend |
Compliance
| Órgão / norma | Exigência | Como a rota atende |
|---|---|---|
| LGPD | minimização; anti-enumeração; não logar e-mail/token em claro | forgot não revela existência da conta; token nunca aparece em log ou resposta de erro |
| HIPAA | senha forte; token de uso único e expirável | política de senha aplicada por Value Object; token com hash + TTL + invalidação no uso |
| ANVISA (indireto) | redefinição de credencial controlada e auditável | evento de auditoria auth.password.changed (tanto por recuperação quanto por troca autenticada) e auth.password.reset_requested |
Variáveis de ambiente
| Variável | Uso | Default |
|---|---|---|
IDENTITY_PASSWORD_RECOVERY_TTL_MINUTES | validade do token de reset | 60 min (máx. 1440) |
IDENTITY_PASSWORD_RECOVERY_URL | URL base do link enviado no e-mail | http://localhost:4200/password/reset |
IDENTITY_PASSWORD_HASH_SALT_ROUNDS | custo do bcrypt para o hash da senha | 12 (mín. 4, máx. 15) — é configurável, não fixo |
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 — cada forgot gera um novo token; reset/troca consomem o token/verificam a senha atual a cada chamada |
| Rate limit | forgot/reset: 5 requisições / 60s (IP + identidade); PATCH /password não tem @Throttle próprio |
| Cache | Não |
| Auditoria | Sim — auth.password.reset_requested, auth.password.changed |
Divergências e lacunas confirmadas
- Existem dois
errorCodeparecidos e fáceis de confundir:IDENTITY_RECOVERY_TOKEN_INVALID(400, formato do token) eIDENTITY_INVALID_RECOVERY_TOKEN(401, token não encontrado/expirado/ já usado). - Um erro de configuração da URL do e-mail (
IDENTITY_EMAIL_ACTION_URL_INVALID, documentado como 500 no Swagger do forgot) é hoje capturado pelo própriotry/catchde envio de e-mail e nunca chega ao cliente — divergência de documentação, não de comportamento observável. - Não existem testes unitários dedicados aos casos de senha; a cobertura vem de um único e2e
(
identity-authentication-flow.e2e.spec.ts).
Fora do escopo desta página
O mesmo controller expõe POST /authentication/email/verify e
POST /authentication/recovery-email/confirm — confirmação de verificação de e-mail e de troca
do e-mail de recuperação, respectivamente. Ambos são fluxos de token público muito parecidos com
o reset de senha (mesmo Value Object de token, mesmo erro 401 em caso de token inválido/expirado),
mas tratam de identidade, não de senha. A confirmar — responsável: time de Identity; data: 24/09/2026.: se vale a pena uma página própria para esses dois endpoints.
Relacionado
- 🖥️ Tela:
A confirmar — responsável: time de frontend; data: 24/09/2026.(não localizada neste levantamento, que cobriu apenas o backend) - 📂 Módulo: Authentication