Skip to content

Fluxo de Redefinição de Senha ​

Implementando uma redefinição de senha segura baseada em token: solicitar → verificar → concluir.

Visão Geral ​

Um fluxo de redefinição de senha tem três etapas:

  1. O usuário solicita uma redefinição — um token com tempo limitado é gerado e enviado (ex.: por e-mail).
  2. O usuário verifica se o token ainda é válido antes de apresentar o formulário de redefinição.
  3. O usuário submete uma nova senha — o token é consumido e a senha é atualizada.

Schema do Banco de Dados ​

sql
CREATE TABLE password_resets (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER NOT NULL,
    token_hash TEXT    NOT NULL UNIQUE,
    used_at    TEXT,
    expires_at TEXT    NOT NULL,
    created_at TEXT    NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

token_hash armazena o hash SHA-256 do token bruto. O token bruto nunca é armazenado no banco de dados.

Geração e Armazenamento do Token ​

Gere o token bruto com random_bytes, depois armazene apenas o hash SHA-256:

php
$rawToken  = bin2hex(random_bytes(32)); // entropia de 256 bits, 64 chars hex
$tokenHash = hash('sha256', $rawToken);

$this->repo->createReset($userId, $tokenHash, $expiresAt, $now);

// Retornar $rawToken ao usuário (via e-mail ou resposta da API)

Ao verificar, faça hash do token recebido da mesma forma:

php
$tokenHash = hash('sha256', $rawToken);
$reset     = $this->repo->findByTokenHashOrNull($tokenHash);

Armazenar um hash significa que uma violação do banco não expõe tokens de redefinição utilizáveis — um atacante precisaria reverter SHA-256 em um valor aleatório de 256 bits, o que é computacionalmente inviável.

Prevenção de Enumeração de Usuário ​

POST /password-reset deve sempre retornar 202, mesmo para endereços de e-mail desconhecidos:

php
$user = $this->repo->findUserByEmail($email);

// Sempre 202 — não revelar se o e-mail está registrado
if ($user === null) {
    return $this->json->create(['status' => 'pending'], 202);
}

// ... gerar token para usuário real
return $this->json->create(['status' => 'pending', 'token' => $rawToken], 202);

Retornar 404 para e-mails desconhecidos permitiria que um atacante enumerasse contas registradas sondando endereços de e-mail.

Uso Único ​

Defina used_at quando a redefinição for concluída. Rejeite qualquer token que tenha used_at IS NOT NULL:

php
if ($reset->isUsed()) {
    return $this->problems->create($request, 'conflict', 'Reset token has already been used.', 409, '');
}

$this->repo->markUsed($tokenHash, $now);
php
public function isUsed(): bool
{
    return $this->usedAt !== null;
}

Expiração ​

Aplique a expiração tanto no GET (verificação de status) quanto no POST (conclusão). Sempre verifique a expiração antes de verificar isUsed():

php
if ($reset->isExpired($now)) {
    return 410; // Gone — distinto de "não encontrado" (404) e "usado" (409)
}
if ($reset->isUsed()) {
    return 409;
}

410 (Gone) distingue "expirado" de "usado" (409), fornecendo ao usuário informação acionável.

Invalidação de Token Antigo ​

Quando um usuário solicita uma nova redefinição, invalide todos os tokens não usados anteriores para aquele usuário:

php
$this->executor->execute(
    "UPDATE password_resets SET used_at = ? WHERE user_id = ? AND used_at IS NULL",
    [$now, $userId],
);

Sem isso, um usuário que perdeu um e-mail de redefinição e solicita um novo teria dois tokens válidos em circulação simultaneamente — ambos poderiam ser usados para redefinir a senha.

Sanitização da Resposta ​

GET /password-reset/{token} não deve expor user_id ou token_hash na resposta:

php
public function toArray(): array
{
    return [
        'id'         => $this->id,
        'expires_at' => $this->expiresAt,
        'created_at' => $this->createdAt,
    ];
}

Expor user_id vincularia o token de redefinição a um ID de conta de usuário, o que é desnecessário já que o token em si é a credencial de autorização.

Propriedades de Segurança ​

PropriedadeImplementação
Entropia do tokenbin2hex(random_bytes(32)) — 256 bits
Armazenamento do tokenApenas hash SHA-256 — token bruto nunca no banco
Enumeração de usuárioSempre 202 de POST /password-reset
Expiração1 hora; verificada no GET e POST
Uso únicoused_at definido na conclusão; 409 em reutilização
Invalidação de token antigoTokens não usados anteriores marcados como usados na nova solicitação
Vazamento na respostauser_id e token_hash excluídos de todas as respostas
Hashing de senhaArgon2id

Resumo de Rotas ​

MétodoCaminhoDescrição
POST/password-resetSolicitar uma redefinição (sempre 202)
GET/password-reset/{token}Verificar validade do token
POST/password-reset/{token}Concluir redefinição com nova senha

Publicado sob a licença MIT.