Email security Required
Make sign-in and reset links single-use and short-lived
A link that signs someone in or resets a password works once, expires within an hour, and holds a long random token. A sign-in code expires within 15 minutes.
How to do it
Make the token at least 128 random bits from a secure random source. Store only a hash of the token, tied to the account and the request. Mark it used on first success. A typed sign-in code dies after 5 wrong tries, and the person asks for a new one. Open a page with a button instead of acting on the first visit, because mail scanners open links on their own. Send Referrer-Policy: no-referrer on that page so the token never leaks. A link only proves access to the inbox. NIST SP 800-63B forbids email as an authentication factor, so ask for the second factor too where the account has one.
Example
GET /reset?token=… → shows "Set a new password" form, changes nothing
POST /reset → checks token, sets password, marks token used
import { randomBytes, createHash } from 'node:crypto';
const token = randomBytes(32).toString('base64url');
const tokenHash = createHash('sha256').update(token).digest('hex');
await db.saveResetToken({ userId, tokenHash, expiresAt: Date.now() + 60 * 60 * 1000 });
import hashlib, secrets, time
token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(token.encode()).hexdigest()
db.save_reset_token(user_id=user_id, token_hash=token_hash, expires_at=time.time() + 60 * 60)
How to check
Use a reset link twice. The second use fails. Wait past the expiry with a fresh link, and it fails too. Fetching the link with curl changes nothing.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers email playbook: https://agentwranglers.se/playbook/email/email-security/sign-in-links.