Mục tiêu: chỉ chủ email thật mới đặt lại được mật khẩu, và không rò rỉ tài khoản nào tồn tại.
Luồng:
1. User nhập email → luôn trả lời cùng một thông báo ("nếu email tồn tại, chúng tôi đã gửi link") bất kể có tồn tại hay không → chống user enumeration.
2. Tạo token ngẫu nhiên đủ dài (vd 32 byte từ CSPRNG). Lưu hash của token trong DB (không lưu token thô), kèm user_id và expires_at (15–60 phút).
3. Gửi token qua link email; token chỉ dùng một lần (single-use) — dùng xong hoặc đổi pass xong thì xóa/vô hiệu.
4. Khi user mở link + đặt pass mới: tra hash, kiểm tra chưa hết hạn và chưa dùng, cập nhật mật khẩu (hash bằng bcrypt/argon2), rồi vô hiệu token + thu hồi các session đang mở.
import { randomBytes, createHash } from "crypto"
const sha256 = (s) => createHash("sha256").update(s).digest("hex")
// request: email the raw token, store only its hash
const token = randomBytes(32).toString("hex")
await db.resetTokens.insert({ userId, tokenHash: sha256(token), expiresAt: Date.now() + 30 * 60_000 })
// verify: look up by hash, check unused + not expired, then single-use delete
const row = await db.resetTokens.findByHash(sha256(input))
if (!row || row.used || row.expiresAt < Date.now()) throw new Error("invalid token")Bẫy hay gặp: token đoán được, không hết hạn, dùng lại nhiều lần, hoặc lộ "email không tồn tại" ở bước 1.
Nên rate-limit cả bước gửi link.