Análise do Fluxo de Recuperação de Senha
Escopo: Backend coezzion-service-auth — endpoints, fluxo, credenciais, segurança e falhas
Contexto cliente: coezzion_vendas_app/docs/recover.md
Data: 2026-07-06
1. Resumo Executivo
O fluxo de recuperação de senha permite que um usuário redefina sua senha sem autenticação JWT, protegido por x-api-key. O processo segue quatro etapas:
- Consultar métodos de verificação disponíveis (SMS e/ou e-mail mascarados)
- Enviar código OTP de 6 dígitos pelo método escolhido
- Validar o código e obter token temporário de troca de senha
- Definir nova senha com o token obtido
O app Flutter (Coezzion Vendas) consome esses endpoints via interceptors que injetam tokens do Firebase Remote Config. O backend persiste estado transitório em cache distribuído e delega envio de mensagens ao Message Service e persistência de senha ao Admin Service.
2. Contexto do Cliente (Flutter)
Conforme documentado em recover.md, o app possui dois pontos de entrada para o mesmo fluxo:
| Entrada | Condição |
|---|---|
| Botão "Esqueci minha senha" | enable_forgot_password == true (Remote Config) |
| Atualização obrigatória pós-login | auth.needChangePassword == true na resposta de autenticação |
Headers HTTP no app
| Header | Interceptor | Paths | Valor (Remote Config) |
|---|---|---|---|
x-api-key | VerifyMethodsInterceptor | recover-methods, verify-recover-method, verify-code-recover, change-password, endpoints legados | recover_methods_token |
x-api-key | RecoverPasswordInterceptor | paths com cellphone | recover_password_token |
Authorization | ChangePasswordInterceptor | fluxos autenticados de atualização de contato | Bearer customizado |
Regras do app (complementares ao backend)
- Timer de reenvio de código: 2 minutos (client-side)
- Limite de tentativas SMS: 3 em 1 hora (client-side,
PhoneCodeVerificationCounterService) - Validação de senha no app: mínimo 6 chars, 1 letra, 1 número
3. Arquitetura Backend
Arquivo central: src/Auth.API/Controllers/PasswordController.cs
4. Endpoints
4.1 Fluxo principal
| # | Método | Endpoint | Action | Service | Proteção |
|---|---|---|---|---|---|
| 1 | POST | /api/auth/recover-methods/{cpf} | GetRecoverMethodsTypes | UserService.GetRecoverMethods | [UseApiKey("SendMessage")] |
| 2 | POST | /api/auth/verify-recover-method | VerifyRecoverMethod | VerifyCodeService.VerifyRecoverMethodAsync | [UseApiKey("SendMessage")] |
| 3 | POST | /api/auth/verify-code-recover | VerifyCodeRecover | VerifyCodeService.VerifyVerificationRecoverCodeAsync | [UseApiKey("SendMessage")] |
| 4 | POST | /api/auth/change-password | ChangePassword | UserService.ChangePassword | [UseApiKey("SendMessage")] |
Request / Response
1. recover-methods/{cpf}
- Request: corpo vazio, CPF na rota
- Response sucesso:
{
"isSuccess": true,
"success": [
{ "type": "Sms", "maskedValue": "(11) *****-**34" },
{ "type": "Email", "maskedValue": "u***@email.com" }
]
}
- Response erro (CPF inexistente):
{ "error": { "title": "CPF não cadastrado.", "message": "CPF não cadastrado.", "needConfirm": false } } - Response erro (sem contatos confirmados):
{ "error": { "title": "Sem dados verificados", "message": "Não temos meios confirmados para recuperar a senha.", "needConfirm": true } }
2. verify-recover-method
- Request:
{ "cpf": "12345678900", "type": "Sms" }
type: enumContactType—EmailouSms- Response sucesso:
{ "isSuccess": true, "success": { "title": "Enviado com sucesso!", "message": "Código de verificação enviado com sucesso!" } }
3. verify-code-recover
- Request:
{ "cpf": "12345678900", "type": "Sms", "verificationCode": "123456" }
- Response sucesso:
{
"isSuccess": true,
"success": {
"userCpf": "12345678900",
"contact": "11999999999",
"token": "a1b2c3d4e5f6...",
"expirationDate": "2026-07-06T12:03:00"
}
}
4. change-password
- Request:
{ "token": "<32 chars>", "cpf": "12345678900", "password": "novaSenha1" }
- Response sucesso:
{ "isSuccess": true, "success": { "title": "Senha alterada com sucesso!", "message": "A partir de agora, utilize sua nova senha para fazer login." } }
4.2 Endpoints auxiliares e legados
| Método | Endpoint | Auth | Uso |
|---|---|---|---|
| POST | /api/auth/verify-phone | x-api-key SendMessage | Envio OTP legado (usuário informa telefone) |
| POST | /api/auth/verify-email | x-api-key SendMessage | Envio OTP legado (usuário informa e-mail) |
| POST | /api/auth/verify-code | x-api-key SendMessage | Validação OTP legado |
| POST | /api/auth/change-cellphone | JWT Bearer | Atualiza telefone após validação |
| POST | /api/auth/change-email | JWT Bearer | Atualiza e-mail após validação |
| POST | /api/auth/{cpf}/cellphone | x-api-key GetCellPhoneMasked | Retorna telefone mascarado (legado, app não usa) |
5. Fluxo Detalhado (Backend)
Etapa 1 — Listar métodos (GetRecoverMethods)
Arquivo: UserService.cs (L268-332)
ExistsUser(cpf)— verifica existênciaGetCpfAuthAsync(cpf)— busca dados do usuário- Adiciona SMS se
CellPhoneConfirmed && CellPhonepreenchido - Adiciona Email se
EmailConfirmed && Emailpreenchido - Mascara valores antes de retornar
Etapa 2 — Enviar código (VerifyRecoverMethodAsync)
Arquivo: VerifyCodeService.cs (L77-103, L251-301)
GetContactData(type, cpf)— busca telefone ou e-mail do banco- Verifica throttle: bloqueia se código anterior ainda válido (expiração 2 min)
GenerateCodeAsync()— OTP de 6 dígitos viaRandomNumberGenerator- Salva em cache: chave
VerificationCodeModelSms-{cpf}ouVerificationCodeModelEmail-{cpf}, TTL 3 min - Envia via
MessageService→api/message/send-sms-passwordousend-email-password
Etapa 3 — Validar código (VerifyVerificationRecoverCodeAsync)
Arquivo: VerifyCodeService.cs (L105-176, L229-239)
- Resolve contato cadastrado automaticamente (cliente não informa telefone/e-mail)
- Valida OTP no cache OU aceita código via
VerifyCodeEasy(ver seção 9) - Gera token:
Guid.NewGuid().ToString("N")(32 caracteres) - Salva em
VerificationCodeModelInCache-{cpf}, expiração 3 min (TTL cache 5 min) - Remove OTP do cache
Etapa 4 — Trocar senha (ChangePassword)
Arquivo: UserService.cs (L46-118)
- Busca
VerificationCodeModelInCache-{cpf}no cache - Valida: token existe, igual ao informado, não expirado
PasswordHasher.Hash(password)→AdminService.UpdatePasswordAsync- Remove token do cache (uso único)
6. Diagrama de Sequência
7. Credenciais e Autenticação
7.1 API Key (UseApiKeyAttribute)
Arquivo: UseApiKeyAttribute.cs
| ConfigKey | Endpoints | appsettings |
|---|---|---|
SendMessage | recover-methods, verify-recover-method, verify-code-recover, change-password, verify-phone, verify-email, verify-code | ApiKeys.SendMessage |
GetCellPhoneMasked | {cpf}/cellphone | ApiKeys.GetCellPhoneMasked |
Produção (appsettings.Production.json):
"ApiKeys": {
"GetCellPhoneMasked": "__KEY_GET_CELL_PHONE_MASKED__",
"SendMessage": "__KEY_SEND_MESSAGE__"
}
Development (appsettings.Development.json):
"ApiKeys": {
"GetCellPhoneMasked": "",
"SendMessage": ""
}
Em Development, header
x-api-keyvazio corresponde à chave vazia configurada — bypass de autenticação em ambiente local.
7.2 Tokens transitórios (cache)
| Artefato | Chave no cache | TTL cache | Expiração lógica | Uso |
|---|---|---|---|---|
| OTP SMS | VerificationCodeModelSms-{cpf} | 3 min | 2 min | Validação em verify-code-recover |
| OTP Email | VerificationCodeModelEmail-{cpf} | 3 min | 2 min | Validação em verify-code-recover |
| Token de troca | VerificationCodeModelInCache-{cpf} | 5 min | 3 min | change-password (uso único) |
7.3 JWT Bearer
Endpoints change-cellphone e change-email exigem [Authorize(JwtBearer)]. O fluxo principal de recover não usa JWT.
8. Mecanismos de Segurança
8.1 Implementados
| Mecanismo | Detalhe | Arquivo |
|---|---|---|
| API Key | Header x-api-key obrigatório em endpoints públicos | UseApiKeyAttribute.cs |
| OTP criptográfico | RandomNumberGenerator para 6 dígitos | VerificationCodeService.cs |
| Throttle de reenvio | Bloqueia novo envio enquanto OTP anterior válido | VerifyCodeService.VerifyContact |
| Token de uso único | Removido do cache após change-password | UserService.ChangePassword |
| Contato do banco | verify-code-recover resolve contato cadastrado, não aceita arbitrário | VerifyCodeService.GetContactData |
| Mascaramento | Telefone e e-mail mascarados em recover-methods | UserService.GetRecoverMethods |
| Hash de senha | PasswordHasher.Hash antes de persistir | UserService.ChangePassword |
| Validação de senha | FluentValidation: 6-16 chars, letra + número | ChangePasswordValidator.cs |
| Validação change-cellphone | Token cache + contato correspondente | UserService.ChangeCellPhone |
8.2 Ausentes ou fracos
| Lacuna | Impacto |
|---|---|
| Sem rate limit de tentativas de OTP no backend | Brute force de 6 dígitos (1M combinações) |
| Sem lockout por CPF após N falhas | Ataque persistente sem penalidade server-side |
| Enumeração de CPF | Respostas distintas revelam CPFs cadastrados |
| API keys no Remote Config do app | Extraíveis via engenharia reversa |
| Sem validators para recover input models | CPF/tipo/código não validados por FluentValidation |
| Inconsistência de contato confirmado | verify-recover-method ignora flags *Confirmed |
Backdoor VerifyCodeEasy | Bypass total de OTP (ver seção 9.1) |
ChangeEmail sem validação de token cache | Alteração de e-mail sem prova de posse (ver seção 9.2) |
9. Falhas e Vulnerabilidades
9.1 CRÍTICA — Backdoor VerifyCodeEasy
Arquivo: VerifyCodeService.cs (L115-121, L216-227)
if (VerifyCodeEasy(input.VerificationCode))
{
verificationModel = CreateVerificationCodeModel(input.ContactType);
verificationModel.UserCpf = input.Cpf;
verificationModel.Contact = input.Contact;
}
A função VerifyCodeEasy gera um código previsível baseado na data atual:
código = (mês + 2)(ano % 100 + 2)(dia + 2)
Exemplo: em 06/07/2026 → (07+2)(26+2)(06+2) = 092808
Impacto:
- Bypassa validação de OTP em
verify-codeeverify-code-recover - Com
x-api-keyválida + CPF conhecido, atacante obtém token de troca de senha - Permite reset de senha de qualquer usuário sem acesso ao SMS/e-mail
Vetor de ataque:
POST /api/auth/verify-code-recover
{ "cpf": "<vítima>", "type": "Sms", "verificationCode": "092808" }
→ retorna token válido para change-password
9.2 ALTA — ChangeEmail sem validação de token
Arquivo: UserService.cs (L182-221)
O bloco de validação do token no cache está comentado/removido:
//await _cacheService.RemoveAsync(key);
Comparando com ChangeCellPhone (L120-180) que valida token, expiração e correspondência de contato, ChangeEmail apenas chama UpdateEmailAsync com JWT.
Impacto: usuário autenticado pode alterar e-mail sem ter validado posse do contato via OTP.
9.3 MÉDIA — Enumeração de usuários
Arquivo: UserService.GetRecoverMethods (L272-273)
- CPF inexistente:
"CPF não cadastrado."comneedConfirm: false - CPF existente sem contatos:
"Sem dados verificados"comneedConfirm: true - CPF existente com contatos: lista de métodos mascarados
Atacante com x-api-key pode enumerar CPFs válidos.
9.4 MÉDIA — Sem proteção contra brute force de OTP
O backend não conta tentativas falhas de verify-code-recover. O limite de 3 SMS/hora existe apenas no app (PhoneCodeVerificationCounterService), não no servidor.
Com 1M combinações possíveis e janela de 2 minutos de validade do OTP, um atacante automatizado pode tentar múltiplos códigos por requisição.
9.5 MÉDIA — Inconsistência de contato confirmado
| Etapa | Verifica *Confirmed? |
|---|---|
GetRecoverMethods | Sim |
VerifyRecoverMethod | Não — envia para qualquer contato no banco |
Se um contato existir no banco mas não estiver confirmado, recover-methods não o exibe, porém chamada direta a verify-recover-method pode enviar OTP.
9.6 MÉDIA — Fluxo legado sem validação de posse
verify-phone e verify-email aceitam qualquer telefone/e-mail informado pelo cliente. O teste VerifyPhoneAsync_InvalidCellPhone_Error está marcado como Skip = "Logica removida" em VerifyPhoneServiceTest.cs.
Impacto: no sub-fluxo de atualização de contato do app, OTP pode ser enviado para contato não vinculado ao CPF.
9.7 BAIXA — API keys vazias em Development
appsettings.Development.json com SendMessage: "" — qualquer requisição com header vazio é aceita.
9.8 BAIXA — API keys embutidas no app
Tokens recover_methods_token e recover_password_token no Firebase Remote Config são extraíveis do binário do app.
9.9 BAIXA — Logging incorreto
Em ValidateVerificationCode (L203-207), o log de mismatch de contato registra input.Cpf em vez de input.Contact:
_logger.LogError(
"Codigo não pertence ao contato informado [{Cpf} | {ContactSave} | {ContactSend}]",
input.Cpf, verificationModel.Contact, input.Cpf); // deveria ser input.Contact
9.10 Observações de implementação
| Item | Detalhe |
|---|---|
| Método HTTP | recover-methods documentado como GET, implementado como POST |
| Nomes duplicados | PasswordController tem 4 métodos chamados VerifyCode |
| Copy-paste | ChangeEmail retorna mensagens de erro referindo "senha" |
| TODO pendente | ChangePassword: "converter cpf para um token para não expor o cpf" nos logs |
| Validators ausentes | Sem VerifyRecoverMethodValidator nem VerifyCodeRecoverValidator |
10. Diagrama de Pontos de Falha
11. Validações (FluentValidation)
| Input Model | Validator | Campos validados |
|---|---|---|
ChangePasswordInputModel | ChangePasswordValidator | CPF válido, token 32 chars, senha 6-16 chars com letra e número |
VerifyCodeInputModel | VerifyCodeValidator | Código 6 dígitos, CPF, telefone ou e-mail |
VerifyPhoneInputModel | VerifyPhoneValidator | CPF, telefone 11-13 dígitos |
VerifyRecoverMethodInputModel | Nenhum | — |
VerifyCodeRecoverInputModel | Nenhum | — |
12. Integrações Externas
Message Service
Arquivo: MessageService.cs
| Canal | Endpoint interno |
|---|---|
| SMS | POST api/message/send-sms-password |
POST api/message/send-email-password |
Admin Service
Arquivo: AdminService.cs
| Operação | Método |
|---|---|
| Atualizar senha | UpdatePasswordAsync(cpf, passwordHash) |
| Atualizar telefone | UpdateCellPhoneAsync(cpf, cellPhone) |
| Atualizar e-mail | UpdateEmailAsync(cpf, email) |
13. Cobertura de Testes
| Componente | Status |
|---|---|
ChangePasswordValidator | Testado (ChangePasswordValidatorTest.cs) |
UserService.ChangePassword | Testado (UserServiceTest.cs) |
UserService.GetCellPhoneMasked | Testado (UserServiceTest.cs) |
VerifyCodeService (recover) | Testes comentados (VerifyPhoneServiceTest.cs — arquivo inteiro comentado) |
GetRecoverMethods | Sem testes |
VerifyCodeEasy | Sem testes, sem flag de ambiente |
14. Recomendações
Apenas documentação — nenhuma alteração de código foi realizada nesta análise.
| Prioridade | Ação |
|---|---|
| P0 | Remover VerifyCodeEasy ou restringir a ambiente de dev com flag explícita e testes |
| P0 | Restaurar validação de token cache em ChangeEmail (paridade com ChangeCellPhone) |
| P1 | Implementar rate limiting server-side em verify-code-recover (tentativas por CPF/IP) |
| P1 | Unificar verificação de *Confirmed entre GetRecoverMethods e VerifyRecoverMethod |
| P1 | Uniformizar resposta de recover-methods para CPF inexistente vs sem contatos (anti-enumeração) |
| P2 | Criar validators para VerifyRecoverMethodInputModel e VerifyCodeRecoverInputModel |
| P2 | Restaurar validação de contato cadastrado em verify-phone/verify-email |
| P2 | Reativar e atualizar testes de VerifyCodeService |
| P3 | Não usar chaves vazias em Development; exigir valor ou desabilitar endpoints |
| P3 | Avaliar rotação de API keys e não depender exclusivamente de Remote Config no app |
| P3 | Corrigir logging em ValidateVerificationCode |
15. Arquivos Relacionados
| Camada | Arquivo | Responsabilidade |
|---|---|---|
| Controller | src/Auth.API/Controllers/PasswordController.cs | Endpoints HTTP |
| Service | src/Auth.API/Application/Services/Implementations/VerifyCodeService.cs | OTP, validação, backdoor |
| Service | src/Auth.API/Application/Services/Implementations/UserService.cs | recover-methods, change-password |
| Service | src/Auth.API/Application/Services/Implementations/VerificationCodeService.cs | Geração e envio de OTP |
| Filtro | src/Auth.API/Filters/UseApiKeyAttribute.cs | Autenticação x-api-key |
| Validator | src/Auth.API/Application/Validators/ChangePasswordValidator.cs | Regras de senha |
| Entity | src/Auth.API/Data/Entities/VerificationCodeModel.cs | Modelos de cache |
| Integration | src/Auth.API/Integration/MessageService.cs | SMS e e-mail |
| Integration | src/Auth.API/Integration/AdminService.cs | Persistência de senha |
| Config | src/Auth.API/appsettings.Production.json | API keys produção |
| Config | src/Auth.API/appsettings.Development.json | API keys development |
| Cliente | coezzion_vendas_app/docs/recover.md | Documentação do consumo Flutter |