Skip to content

Password reset emails: token design and delivery checklist

Reset emails are a top account takeover vector. Design single-use, short-lived tokens, avoid account enumeration and make the email arrive fast.

Koltrix Team4 min read
A silver padlock against a plain background
Photo by Zaqy Al Fattah on Unsplash
On this page(5 sections)
  1. Token design
  2. Make tokens unguessable
  3. Store only a hash
  4. Expire quickly
  5. Single use, and invalidate on success
  6. Bind the token to the account
  7. Flow design
  8. Avoid account enumeration
  9. Rate-limit requests
  10. Do not trust the Host header
  11. Keep the token out of logs and referrers
  12. Consider link scanners
  13. Handle lost access to the mailbox
  14. The email itself
  15. Content
  16. Delivery speed
  17. Notification after the change
  18. A review checklist
  19. Key takeaways

A password reset email is a key to the account, delivered by a channel you do not control, to a mailbox that may or may not still belong to your user. Most account takeovers that go through email exploit a weakness in how the reset token was designed or how the flow behaves, not in the email itself.

Token design

Make tokens unguessable

Generate reset tokens with a cryptographically secure random number generator and enough entropy that guessing is hopeless. 128 bits or more is a common choice.

import secrets, hashlib

token = secrets.token_urlsafe(32)            # ~256 bits, URL-safe
token_hash = hashlib.sha256(token.encode()).hexdigest()
# store token_hash with user_id, created_at, expires_at; email the raw token

Never derive tokens from predictable values such as user IDs, timestamps or email addresses, even with hashing.

Store only a hash

Treat reset tokens like passwords in your database. Store a hash, not the raw token. If the database leaks, outstanding reset links remain unusable. Because the token already has high entropy, a fast hash such as SHA-256 is adequate here; a slow password hash is not required.

Expire quickly

Reset links should expire within a short window. OWASP's guidance treats short lifetimes as essential, and many applications use somewhere between 15 minutes and an hour. Long-lived reset links sit in mailboxes, backups and forwarded threads.

Single use, and invalidate on success

A token must stop working the moment it is used. When the password is changed:

  • Mark that token used.
  • Invalidate every other outstanding reset token for that account.
  • Revoke existing sessions, or at least offer to, so an attacker who already had access is signed out.

Bind the token to the account

Look up the account by the token's hash. Never accept a user ID or email from the URL or form alongside the token and trust it; a token for one account must not reset another.

Flow design

Avoid account enumeration

The reset request page should give the same response whether or not the address has an account:

If an account exists for that address, we have sent a password reset link.

Keep response timing similar too: perform the lookup and enqueue the email asynchronously so a real account does not respond measurably slower than a missing one.

Rate-limit requests

Limit reset requests per account and per client IP. This stops attackers from flooding a user's inbox, and from using your reset form to send unwanted mail to arbitrary addresses, which also harms your sending reputation.

Do not trust the Host header

A well-known class of bug builds the reset URL from the incoming request's Host header. An attacker who sends a reset request with a manipulated Host can cause your application to email a link pointing at their domain. If the victim clicks it, the token goes to the attacker. Build reset links from a configured, trusted base URL.

Keep the token out of logs and referrers

  • Do not log full reset URLs.
  • On the reset page, set Referrer-Policy: no-referrer (or a strict policy) so the token is not leaked to third-party resources the page loads.
  • Avoid loading third-party analytics scripts on the reset page entirely.
  • After the token is validated, consider exchanging it for a short-lived server-side session and redirecting to a URL without the token.

Corporate email security tools often fetch links in incoming mail to check them. If merely loading the reset URL consumes the token or changes the password, scanners will break your flow. Loading the link should show a form; the reset should happen only when the user submits it with a POST.

Handle lost access to the mailbox

Reset by email assumes the user still controls their address. People leave jobs, let personal domains lapse, and lose access to old accounts. If the recovery path is "email only," an attacker who registers a lapsed domain can receive reset emails for every account tied to addresses at that domain. Mitigations include prompting users to confirm their email periodically, offering a second recovery factor for high-value accounts, and treating a recovery request after a long period of inactivity with extra scrutiny.

The email itself

Content

  • State plainly what happened: someone requested a password reset for this account.
  • Include the link as a single, clearly labeled button and the same URL as plain text.
  • Say how long the link is valid.
  • Tell the user what to do if they did not request it: nothing, and their password is unchanged.
  • Do not include the current password, a new password, or any account secrets.

Delivery speed

Users request a reset and wait. A reset email that takes several minutes generates repeat requests and support tickets. Send reset emails through a dedicated, high-priority path:

  • Separate queue from bulk or marketing mail.
  • Sending identity with good reputation and full SPF, DKIM and DMARC alignment.
  • Monitoring of time from request to delivery acceptance.

Notification after the change

After a successful reset, send a confirmation to the account's address saying the password was changed, with a way to report it if it was not them. If the email address itself was changed, notify the old address too.

A review checklist

Area Check
Token Random, at least 128 bits, stored hashed
Lifetime Expires in minutes to an hour
Use Single use; all tokens invalidated on success
Sessions Existing sessions revoked or offered for revocation
Enumeration Identical responses and timing for unknown addresses
Rate limits Per account and per IP
URL building Trusted configured base URL, never the Host header
Leakage No token in logs; strict referrer policy; no third-party scripts
Scanners GET shows a form; POST performs the reset
Delivery Dedicated fast path; monitored latency
Aftermath Confirmation email after the change

Key takeaways

  • Reset tokens should be random, hashed in storage, short-lived and single-use.
  • Identical responses prevent account enumeration; rate limits prevent inbox flooding.
  • Build links from a trusted base URL, never from the request's Host header.
  • Make the GET request harmless so link scanners do not consume tokens.
  • Deliver reset emails quickly on a dedicated path, and confirm every successful change.

Start with Koltrix

Your domain, one inbox, and an API that sends.

A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.

SharePost on XLinkedIn