Como fazer o token de "esqueci minha senha" com segurança
O link de "esqueci minha senha" dá acesso à conta de quem o tiver, então o token precisa ser imprevisível, valer por pouco tempo e servir uma única vez. Este guia mostra como gerar esse token, por que guardar só o hash dele no banco, como conferir validade e uso, e os detalhes que costumam vazar o link, com exemplos em JavaScript, Python e PHP.
O que o token precisa ter
- Imprevisível: gerado pelo gerador aleatório criptográfico, com pelo menos 128 bits. Os exemplos usam 256 bits.
- Validade curta: de 15 minutos a 1 hora.
- Uso único: depois de trocar a senha, o token não vale mais.
- No banco, só o hash: se o banco vazar, os tokens guardados não servem para nada.
Por que aqui o SHA-256 basta
Para senhas, um hash rápido como o SHA-256 é uma má ideia, porque senhas são fáceis de adivinhar e testar em massa. O token é diferente: 256 bits aleatórios não se adivinham, então um hash rápido já impede que quem leia o banco use os tokens. O banco guarda o hash, e o link do e-mail leva o token.
Para guardar senhas, veja como guardar senhas com hash.
Em JavaScript (Node.js)
Na conferência, calcule o hash do token recebido e busque o registro por ele (WHERE hash = ?). Se não existir, estiver expirado ou já tiver sido usado, recuse. Ao trocar a senha, marque o registro como usado na mesma transação.
import { createHash, randomBytes } from 'node:crypto';
const VALIDADE_MS = 30 * 60 * 1000; // 30 minutos
const hashDoToken = (token) => createHash('sha256').update(token).digest('hex');
// O token vai no link do e-mail; no banco fica só o hash dele
function criarTokenDeRedefinicao(usuarioId, agora = Date.now()) {
const token = randomBytes(32).toString('base64url'); // 256 bits aleatórios
const registro = {
usuarioId,
hash: hashDoToken(token),
expiraEm: agora + VALIDADE_MS,
usadoEm: null,
};
return { token, registro };
}
// Busca pelo hash (WHERE hash = ?) e confere validade e uso antes de trocar a senha
function registroValido(registro, agora = Date.now()) {
return Boolean(registro) && registro.usadoEm === null && agora <= registro.expiraEm;
} Em Python e PHP
No Python, secrets.token_urlsafe(32) já gera 256 bits em um texto seguro para URL. No PHP, random_bytes é a fonte criptográfica.
import hashlib
import secrets
import time
VALIDADE_S = 30 * 60 # 30 minutos
def hash_do_token(token: str) -> str:
return hashlib.sha256(token.encode()).hexdigest()
def criar_token_de_redefinicao(usuario_id: int, agora: float | None = None):
agora = time.time() if agora is None else agora
token = secrets.token_urlsafe(32) # vai no link do e-mail
registro = {
"usuario_id": usuario_id,
"hash": hash_do_token(token),
"expira_em": agora + VALIDADE_S,
"usado_em": None,
}
return token, registro # no banco, só o registro (com o hash)
def registro_valido(registro, agora: float | None = None) -> bool:
agora = time.time() if agora is None else agora
return bool(registro) and registro["usado_em"] is None and agora <= registro["expira_em"] <?php
const VALIDADE_S = 30 * 60; // 30 minutos
function hashDoToken(string $token): string
{
return hash('sha256', $token);
}
function criarTokenDeRedefinicao(int $usuarioId, ?int $agora = null): array
{
$agora ??= time();
$token = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='); // vai no e-mail
$registro = [
'usuarioId' => $usuarioId,
'hash' => hashDoToken($token),
'expiraEm' => $agora + VALIDADE_S,
'usadoEm' => null,
];
return [$token, $registro]; // no banco, só o registro
}
function registroValido(?array $registro, ?int $agora = null): bool
{
return $registro !== null
&& $registro['usadoEm'] === null
&& ($agora ?? time()) <= $registro['expiraEm'];
} Os detalhes que vazam o link
- Resposta igual para e-mail cadastrado ou não ("se o e-mail existir, enviamos um link"). Senão, o formulário revela quem tem conta.
- Na página de redefinição, use
Referrer-Policy: no-referrer. Sem isso, o endereço com o token pode ir no cabeçalho Referer para scripts e links de terceiros. - Não registre a URL completa em logs, nem em ferramentas de análise.
- Limite os pedidos por e-mail e por IP, para ninguém lotar a caixa de alguém.
- Ao trocar a senha, invalide os outros tokens do usuário e encerre as sessões abertas.
Achou um erro neste guia? Avise por e-mail.
Ferramentas para usar junto
Perguntas frequentes
Quanto tempo deve valer o link de redefinição de senha?
Pouco: de 15 minutos a 1 hora é o mais comum. Quanto mais tempo o link vale, mais tempo ele fica útil para quem conseguir acesso ao e-mail.
Preciso guardar o token de redefinição no banco?
Guarde só o hash dele, com a validade e se já foi usado. Na conferência, calcule o hash do token recebido e busque por ele. Assim, um vazamento do banco não entrega links válidos.
Posso usar um UUID como token de redefinição?
Um UUID v4 tem 122 bits aleatórios e, se gerado com um gerador criptográfico, é difícil de adivinhar. Mesmo assim, prefira um token próprio com 256 bits, como os deste guia, e nunca use UUID v1 ou v7, que têm partes previsíveis.
Por que a mensagem não diz se o e-mail está cadastrado?
Para não revelar quem tem conta no site. Com mensagens diferentes, qualquer pessoa descobre se um e-mail está cadastrado só usando o formulário.
Outros guias
- Como guardar senhas: por que não usar SHA-256 e o que usarPor que MD5 e SHA-256 não servem para senhas e como guardar senhas com scrypt, bcrypt ou Argon2 em Node.js, Python e PHP, com sal e código testado.
- Como gerar senha segura em código (sem Math.random)Por que Math.random não serve para senhas, como evitar o viés do módulo e como gerar senhas seguras em JavaScript, Python e PHP, com código testado.