# 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.

- Endereço oficial: https://pradev.com.br/guias/token-de-redefinicao-de-senha
- Tipo: Guia
- Publicado em: 2026-10-01
- Atualizado em: 2026-10-01
- Autor: Leonardo Gaertner (https://pradev.com.br/sobre)
- Ferramentas: [Gerador de SHA256](https://pradev.com.br/gerador-de-sha256)

## 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](https://pradev.com.br/gerador-de-sha256) é 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](https://pradev.com.br/guias/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.

```javascript
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;
}
```

_JavaScript_

## 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.

```python
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"]
```

_Python_

```php
<?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'];
}
```

_PHP_

## 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.

## 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.

## Fontes

- [OWASP: Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html)

## Guias relacionados

- [Como guardar senhas: por que não usar SHA-256 e o que usar](https://pradev.com.br/guias/como-guardar-senhas-com-hash): Por 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)](https://pradev.com.br/guias/como-gerar-senha-segura-em-codigo): 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.
