Skip to main content

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:

  1. Consultar métodos de verificação disponíveis (SMS e/ou e-mail mascarados)
  2. Enviar código OTP de 6 dígitos pelo método escolhido
  3. Validar o código e obter token temporário de troca de senha
  4. 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:

EntradaCondição
Botão "Esqueci minha senha"enable_forgot_password == true (Remote Config)
Atualização obrigatória pós-loginauth.needChangePassword == true na resposta de autenticação

Headers HTTP no app

HeaderInterceptorPathsValor (Remote Config)
x-api-keyVerifyMethodsInterceptorrecover-methods, verify-recover-method, verify-code-recover, change-password, endpoints legadosrecover_methods_token
x-api-keyRecoverPasswordInterceptorpaths com cellphonerecover_password_token
AuthorizationChangePasswordInterceptorfluxos autenticados de atualização de contatoBearer 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étodoEndpointActionServiceProteção
1POST/api/auth/recover-methods/{cpf}GetRecoverMethodsTypesUserService.GetRecoverMethods[UseApiKey("SendMessage")]
2POST/api/auth/verify-recover-methodVerifyRecoverMethodVerifyCodeService.VerifyRecoverMethodAsync[UseApiKey("SendMessage")]
3POST/api/auth/verify-code-recoverVerifyCodeRecoverVerifyCodeService.VerifyVerificationRecoverCodeAsync[UseApiKey("SendMessage")]
4POST/api/auth/change-passwordChangePasswordUserService.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: enum ContactTypeEmail ou Sms
  • 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étodoEndpointAuthUso
POST/api/auth/verify-phonex-api-key SendMessageEnvio OTP legado (usuário informa telefone)
POST/api/auth/verify-emailx-api-key SendMessageEnvio OTP legado (usuário informa e-mail)
POST/api/auth/verify-codex-api-key SendMessageValidação OTP legado
POST/api/auth/change-cellphoneJWT BearerAtualiza telefone após validação
POST/api/auth/change-emailJWT BearerAtualiza e-mail após validação
POST/api/auth/{cpf}/cellphonex-api-key GetCellPhoneMaskedRetorna telefone mascarado (legado, app não usa)

5. Fluxo Detalhado (Backend)

Etapa 1 — Listar métodos (GetRecoverMethods)

Arquivo: UserService.cs (L268-332)

  1. ExistsUser(cpf) — verifica existência
  2. GetCpfAuthAsync(cpf) — busca dados do usuário
  3. Adiciona SMS se CellPhoneConfirmed && CellPhone preenchido
  4. Adiciona Email se EmailConfirmed && Email preenchido
  5. Mascara valores antes de retornar

Etapa 2 — Enviar código (VerifyRecoverMethodAsync)

Arquivo: VerifyCodeService.cs (L77-103, L251-301)

  1. GetContactData(type, cpf) — busca telefone ou e-mail do banco
  2. Verifica throttle: bloqueia se código anterior ainda válido (expiração 2 min)
  3. GenerateCodeAsync() — OTP de 6 dígitos via RandomNumberGenerator
  4. Salva em cache: chave VerificationCodeModelSms-{cpf} ou VerificationCodeModelEmail-{cpf}, TTL 3 min
  5. Envia via MessageServiceapi/message/send-sms-password ou send-email-password

Etapa 3 — Validar código (VerifyVerificationRecoverCodeAsync)

Arquivo: VerifyCodeService.cs (L105-176, L229-239)

  1. Resolve contato cadastrado automaticamente (cliente não informa telefone/e-mail)
  2. Valida OTP no cache OU aceita código via VerifyCodeEasy (ver seção 9)
  3. Gera token: Guid.NewGuid().ToString("N") (32 caracteres)
  4. Salva em VerificationCodeModelInCache-{cpf}, expiração 3 min (TTL cache 5 min)
  5. Remove OTP do cache

Etapa 4 — Trocar senha (ChangePassword)

Arquivo: UserService.cs (L46-118)

  1. Busca VerificationCodeModelInCache-{cpf} no cache
  2. Valida: token existe, igual ao informado, não expirado
  3. PasswordHasher.Hash(password)AdminService.UpdatePasswordAsync
  4. Remove token do cache (uso único)

6. Diagrama de Sequência


7. Credenciais e Autenticação

7.1 API Key (UseApiKeyAttribute)

Arquivo: UseApiKeyAttribute.cs

ConfigKeyEndpointsappsettings
SendMessagerecover-methods, verify-recover-method, verify-code-recover, change-password, verify-phone, verify-email, verify-codeApiKeys.SendMessage
GetCellPhoneMasked{cpf}/cellphoneApiKeys.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-key vazio corresponde à chave vazia configurada — bypass de autenticação em ambiente local.

7.2 Tokens transitórios (cache)

ArtefatoChave no cacheTTL cacheExpiração lógicaUso
OTP SMSVerificationCodeModelSms-{cpf}3 min2 minValidação em verify-code-recover
OTP EmailVerificationCodeModelEmail-{cpf}3 min2 minValidação em verify-code-recover
Token de trocaVerificationCodeModelInCache-{cpf}5 min3 minchange-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

MecanismoDetalheArquivo
API KeyHeader x-api-key obrigatório em endpoints públicosUseApiKeyAttribute.cs
OTP criptográficoRandomNumberGenerator para 6 dígitosVerificationCodeService.cs
Throttle de reenvioBloqueia novo envio enquanto OTP anterior válidoVerifyCodeService.VerifyContact
Token de uso únicoRemovido do cache após change-passwordUserService.ChangePassword
Contato do bancoverify-code-recover resolve contato cadastrado, não aceita arbitrárioVerifyCodeService.GetContactData
MascaramentoTelefone e e-mail mascarados em recover-methodsUserService.GetRecoverMethods
Hash de senhaPasswordHasher.Hash antes de persistirUserService.ChangePassword
Validação de senhaFluentValidation: 6-16 chars, letra + númeroChangePasswordValidator.cs
Validação change-cellphoneToken cache + contato correspondenteUserService.ChangeCellPhone

8.2 Ausentes ou fracos

LacunaImpacto
Sem rate limit de tentativas de OTP no backendBrute force de 6 dígitos (1M combinações)
Sem lockout por CPF após N falhasAtaque persistente sem penalidade server-side
Enumeração de CPFRespostas distintas revelam CPFs cadastrados
API keys no Remote Config do appExtraíveis via engenharia reversa
Sem validators para recover input modelsCPF/tipo/código não validados por FluentValidation
Inconsistência de contato confirmadoverify-recover-method ignora flags *Confirmed
Backdoor VerifyCodeEasyBypass total de OTP (ver seção 9.1)
ChangeEmail sem validação de token cacheAlteraçã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-code e verify-code-recover
  • Com x-api-key vá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." com needConfirm: false
  • CPF existente sem contatos: "Sem dados verificados" com needConfirm: 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

EtapaVerifica *Confirmed?
GetRecoverMethodsSim
VerifyRecoverMethodNã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

ItemDetalhe
Método HTTPrecover-methods documentado como GET, implementado como POST
Nomes duplicadosPasswordController tem 4 métodos chamados VerifyCode
Copy-pasteChangeEmail retorna mensagens de erro referindo "senha"
TODO pendenteChangePassword: "converter cpf para um token para não expor o cpf" nos logs
Validators ausentesSem VerifyRecoverMethodValidator nem VerifyCodeRecoverValidator

10. Diagrama de Pontos de Falha


11. Validações (FluentValidation)

Input ModelValidatorCampos validados
ChangePasswordInputModelChangePasswordValidatorCPF válido, token 32 chars, senha 6-16 chars com letra e número
VerifyCodeInputModelVerifyCodeValidatorCódigo 6 dígitos, CPF, telefone ou e-mail
VerifyPhoneInputModelVerifyPhoneValidatorCPF, telefone 11-13 dígitos
VerifyRecoverMethodInputModelNenhum
VerifyCodeRecoverInputModelNenhum

12. Integrações Externas

Message Service

Arquivo: MessageService.cs

CanalEndpoint interno
SMSPOST api/message/send-sms-password
EmailPOST api/message/send-email-password

Admin Service

Arquivo: AdminService.cs

OperaçãoMétodo
Atualizar senhaUpdatePasswordAsync(cpf, passwordHash)
Atualizar telefoneUpdateCellPhoneAsync(cpf, cellPhone)
Atualizar e-mailUpdateEmailAsync(cpf, email)

13. Cobertura de Testes

ComponenteStatus
ChangePasswordValidatorTestado (ChangePasswordValidatorTest.cs)
UserService.ChangePasswordTestado (UserServiceTest.cs)
UserService.GetCellPhoneMaskedTestado (UserServiceTest.cs)
VerifyCodeService (recover)Testes comentados (VerifyPhoneServiceTest.cs — arquivo inteiro comentado)
GetRecoverMethodsSem testes
VerifyCodeEasySem testes, sem flag de ambiente

14. Recomendações

Apenas documentação — nenhuma alteração de código foi realizada nesta análise.

PrioridadeAção
P0Remover VerifyCodeEasy ou restringir a ambiente de dev com flag explícita e testes
P0Restaurar validação de token cache em ChangeEmail (paridade com ChangeCellPhone)
P1Implementar rate limiting server-side em verify-code-recover (tentativas por CPF/IP)
P1Unificar verificação de *Confirmed entre GetRecoverMethods e VerifyRecoverMethod
P1Uniformizar resposta de recover-methods para CPF inexistente vs sem contatos (anti-enumeração)
P2Criar validators para VerifyRecoverMethodInputModel e VerifyCodeRecoverInputModel
P2Restaurar validação de contato cadastrado em verify-phone/verify-email
P2Reativar e atualizar testes de VerifyCodeService
P3Não usar chaves vazias em Development; exigir valor ou desabilitar endpoints
P3Avaliar rotação de API keys e não depender exclusivamente de Remote Config no app
P3Corrigir logging em ValidateVerificationCode

15. Arquivos Relacionados

CamadaArquivoResponsabilidade
Controllersrc/Auth.API/Controllers/PasswordController.csEndpoints HTTP
Servicesrc/Auth.API/Application/Services/Implementations/VerifyCodeService.csOTP, validação, backdoor
Servicesrc/Auth.API/Application/Services/Implementations/UserService.csrecover-methods, change-password
Servicesrc/Auth.API/Application/Services/Implementations/VerificationCodeService.csGeração e envio de OTP
Filtrosrc/Auth.API/Filters/UseApiKeyAttribute.csAutenticação x-api-key
Validatorsrc/Auth.API/Application/Validators/ChangePasswordValidator.csRegras de senha
Entitysrc/Auth.API/Data/Entities/VerificationCodeModel.csModelos de cache
Integrationsrc/Auth.API/Integration/MessageService.csSMS e e-mail
Integrationsrc/Auth.API/Integration/AdminService.csPersistência de senha
Configsrc/Auth.API/appsettings.Production.jsonAPI keys produção
Configsrc/Auth.API/appsettings.Development.jsonAPI keys development
Clientecoezzion_vendas_app/docs/recover.mdDocumentação do consumo Flutter