The “Forgot Password” feature is one of the most common — and most attacked — endpoints in any web application. If it’s poorly implemented, it opens the door to account takeover, email enumeration, brute force, and even credential stuffing attacks.
In this guide, we’ll walk through best practices for implementing a secure password reset flow, explain why each matters, and share practical code snippets you can adapt.
1. Never Reveal Whether an Account Exists
Attackers love using the reset form to enumerate valid emails or usernames.
Always respond with a generic message, regardless of whether the email exists:
“If an account exists with this email, a password reset link has been sent.”
This prevents attackers from confirming which accounts are valid.
2. Use Strong, Single-Use Reset Tokens
- Generate tokens with cryptographically secure randomness (e.g., 32+ bytes).
- Store only a hash of the token in your database.
- Mark tokens as single-use and invalidate them after reset.
⚠️ Do not store raw tokens in your database — if your DB is leaked, attackers could use them directly.
3. Short Token Lifetimes
Password reset links should not live forever. A good practice is to expire them after 15–60 minutes.
For high-security systems (like banking), even 10–15 minutes is acceptable.
4. Always Use HTTPS
Reset links often contain sensitive tokens. These must be delivered only over TLS (HTTPS).
Never send or accept reset tokens over plain HTTP.
5. Rate Limit & Throttle
Attackers may abuse the reset flow with automated scripts. Defend against this with:
- Per-account limits (e.g., 3 reset requests per hour).
- Per-IP limits (e.g., 50 requests per hour).
- Token verification throttling (limit failed attempts before locking).
You can also add a CAPTCHA for suspicious patterns.
6. Constant-Time Token Verification
When comparing token hashes, always use a constant-time comparison function.
This prevents timing attacks that can leak information about valid tokens.
7. Invalidate Active Sessions After Reset
When a password is reset:
- Log out all existing sessions.
- Rotate authentication tokens or API keys. This ensures attackers cannot stay logged in after a compromise.
8. Notify Users of All Reset Activity
Send emails for:
- When a reset request is made.
- When the password is successfully changed.
Include timestamp, IP, and device info where possible. This way, users can spot suspicious activity early.
9. Enforce Strong New Passwords
After verification:
- Require strong passwords (length, complexity, no dictionary words).
- Prevent reusing the last N passwords.
- Use a strong hash algorithm (e.g., bcrypt, argon2) for storage.
10. Consider MFA for Sensitive Accounts
For privileged or high-value accounts, you may require multi-factor authentication (MFA) before allowing a password reset.
Alternatively, require re-enrollment in MFA after a reset.
11. Secure the Reset Page
- Use POST (not GET) when submitting new passwords (prevents CSRF issues).
- Set session cookies with
Secure
,HttpOnly
, andSameSite=strict
. - Don’t store reset tokens in local storage or logs.
12. Secure Token Design
Most systems embed reset tokens in the URL like:
https://example.com/reset-password?token=<raw_token>
This is fine if TLS is enforced. But remember:
- Tokens may leak via logs or referer headers.
- For extra security, you can use a short link and store the actual token server-side.
Example: Secure Token Workflow
Here’s a practical workflow:
- User submits email at
/forgot-password
. - Server generates a cryptographically strong token.
- Store only the token hash with expiry and single-use flag.
- Email the user a reset link containing the raw token.
- When the user clicks the link, verify the hash in constant time.
- If valid, allow password reset, invalidate sessions, and mark token as used.
Example Implementation (Python-style pseudocode)
import os, hmac, hashlib
from secrets import token_urlsafe
from datetime import datetime, timedelta
SECRET_KEY = os.environ['RESET_SECRET']
TOKEN_BYTES = 32
TOKEN_TTL = timedelta(minutes=30)
def create_reset_token(user_id):
raw = token_urlsafe(TOKEN_BYTES)
token_hash = hmac.new(SECRET_KEY.encode(), raw.encode(), hashlib.sha256).hexdigest()
expires_at = datetime.utcnow() + TOKEN_TTL
db.insert("reset_tokens", {
"user_id": user_id,
"token_hash": token_hash,
"expires_at": expires_at,
"used": False
})
return raw # send this in email link
def verify_token(user_id, provided_raw):
token_hash = hmac.new(SECRET_KEY.encode(), provided_raw.encode(), hashlib.sha256).hexdigest()
record = db.get_reset_token(user_id)
if not record or record["used"] or record["expires_at"] < datetime.utcnow():
return False
if not hmac.compare_digest(record["token_hash"], token_hash):
return False
db.update("reset_tokens", record["id"], {"used": True})
return True
Final Checklist
✅ Don’t leak account existence
✅ Strong, hashed, single-use tokens
✅ Short expiry (15–60 mins)
✅ HTTPS only
✅ Rate limiting & CAPTCHA
✅ Constant-time token checks
✅ Invalidate sessions on reset
✅ Notify users of reset activity
✅ Strong new passwords & password history
✅ Audit & monitor unusual activity
🔐 Bottom line:
Your “Forgot Password” endpoint is often the weakest link in account security. By following these best practices, you make it significantly harder for attackers to hijack accounts, while still keeping the reset process smooth for real users.
Top comments (0)