18.6. Password Reset Emails
Table of Contents
Why Password Reset Emails Matter
Password reset emails are a critical part of almost every web application. They sit at the intersection of user experience and security. When a user forgets a password, this is usually the only safe way to let them regain access.
A good password reset flow must:
- Be simple for the user.
- Be very hard to abuse.
- Protect both the user account and your system from attackers.
In this chapter you will focus specifically on the email part of a password reset, not the entire authentication system. You will connect what you already know about authentication, tokens, and sending emails, and see how to design a secure and reliable reset email flow.
Core idea: A password reset email is a temporary, one-time way to prove identity. It must be treated as carefully as login and password storage.
Typical Password Reset Flow
Before you design the email content or send anything, you need to understand the ideal flow at a high level. Most modern applications follow a similar pattern:
- User requests a reset
- User goes to "Forgot password" page.
- Enters their email address.
- Server handles the request
- Looks up the user by email.
- If user exists:
- Creates a secure, random reset token.
- Stores it with an expiration time.
- Sends a password reset email containing a link with that token.
- If user does not exist:
- Responds as if everything is fine, but does not reveal that the email is unknown.
- User receives the email
- Opens the email.
- Clicks the reset link.
- User sets a new password
- The link opens a reset page on your application.
- Backend verifies the token.
- If valid and not expired, user can set a new password.
- Token is invalidated after use.
- Confirmation
- Optionally, send a "your password was changed" notification email.
In this chapter, your focus is mainly on step 2 (sending the reset email) and step 3 (what the email should contain and how to use it).
Designing the Reset Email Content
The content of a password reset email must be:
- Clear to non-technical users.
- Difficult to misuse.
- Helpful for security if something suspicious is going on.
Essential elements
A good password reset email typically contains:
- Clear subject line
Examples:
- "Reset your password"
- "Password reset request"
- "Instructions to reset your password"
Avoid subject lines that are vague or that look like spam.
- Greeting
If you know the user name, use it:
- "Hi Alice,"
- "Hello John Doe,"
If you do not, a generic greeting is fine:
- "Hello,"
- "Hi there,"
- Short explanation
Explain why the user is receiving the email:
- "We received a request to reset the password for your account associated with this email address."
- The reset action (call to action)
This is usually a button or a link:
- "Click the button below to reset your password:"
- A visible URL for backup, in case buttons fail.
- Expiration information
Tell the user that the link will expire:
- "This link will expire in 30 minutes."
- "For your security, this password reset link is valid for 1 hour."
- What to do if they did not request it
Very important for security awareness:
- "If you did not request a password reset, you can safely ignore this email. Your password will not be changed."
- Support information
Optional but useful:
- "If you are having trouble, contact us at support@example.com."
Example plaintext email
Here is a simple plaintext password reset email:
Subject: Reset your password
Hi {{user_name}},
We received a request to reset the password for your account at {{app_name}}.
To reset your password, click the link below:
{{reset_link}}
For your security, this link will expire in 30 minutes and can be used only once.
If you did not request a password reset, you can safely ignore this email.
Your password will not be changed.
Best regards,
The {{app_name}} TeamExample HTML email with a button
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.5; color: #333;">
<p>Hi {{user_name}},</p>
<p>
We received a request to reset the password for your {{app_name}} account.
</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{reset_link}}"
style="background-color: #2563eb;
color: #ffffff;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
display: inline-block;">
Reset your password
</a>
</p>
<p>If the button above does not work, copy and paste this link into your browser:</p>
<p style="word-break: break-all;">
<a href="{{reset_link}}">{{reset_link}}</a>
</p>
<p>
For your security, this link will expire in 30 minutes and can be used only once.
</p>
<p>
If you did not request a password reset, please ignore this email.
Your password will not be changed.
</p>
<p>Best regards,<br>The {{app_name}} Team</p>
</body>
</html>Using both HTML and plaintext versions improves deliverability and user experience.
Reset Links and Tokens
The most important part of the email is the reset link, because it carries the token that lets the user set a new password.
A typical reset link looks like this:
https://example.com/reset-password?token=abc123...But the token inside this link must be carefully designed.
Requirements for a reset token
A secure password reset token should be:
- Random: Not predictable.
- Long enough: To make brute-force guessing impractical.
- Single use: Once used, it becomes invalid.
- Short lived: Expires after a limited time, such as 15 to 60 minutes.
- Bound to a user: It should match exactly one account.
Important rule: Never use simple data like user id or email alone in the reset link. Always use a cryptographically secure random token, and verify it on the server.
Token formats
Some common approaches:
| Approach | Description | Pros | Cons |
|---|---|---|---|
| Random string in DB | Random bytes stored hashed in database | Simple, secure, revocable | Requires DB lookup |
| Signed token (JWT) | Token with payload signed by server secret | No DB if purely stateless | Revocation is harder |
| Hybrid | Signed token + optional DB storage | Flexible, extra control | More complex implementation |
For beginners, the database approach with a random string is easier to implement correctly.
Example token generation (Python)
import secrets
def generate_reset_token() -> str:
# 32 bytes of randomness encoded as URL-safe base64
return secrets.token_urlsafe(32)You then store a hash of this token in the database, not the token itself, similar to password storage practices.
Backend Logic for Sending Reset Emails
Now connect the email content with your backend logic. At a high level, the backend needs to:
- Receive the password reset request.
- Generate and store the token with expiration.
- Build the reset link.
- Render an email template with the reset link.
- Send the email through SMTP or an email service.
Handling the "Forgot password" request
You might define an endpoint such as:
POST /auth/forgot-password
Content-Type: application/json
{
"email": "user@example.com"
}Backend logic in pseudocode:
function forgot_password(email):
user = find_user_by_email(email)
if user exists:
token = generate_reset_token()
hashed_token = hash(token)
store_reset_token(user_id=user.id, token_hash=hashed_token, expires_at=now + 30 minutes)
reset_link = "https://example.com/reset-password?token=" + url_encode(token)
send_reset_email(user.email, user.name, reset_link)
# Always respond with the same message
return { "message": "If an account with that email exists, we have sent a password reset link." }The important detail is that you do not reveal whether the email exists.
Building the reset link
You usually combine:
- Application base URL.
- Reset route path.
- The token as a query parameter or path parameter.
Examples:
https://app.example.com/reset-password?token={{token}}
https://app.example.com/reset-password/{{token}}You must not put any sensitive data such as passwords or hashes in this link.
Sending the email (simplified Python example)
Assuming you already have an send_email helper that can send HTML and plaintext:
from jinja2 import Template
RESET_EMAIL_SUBJECT = "Reset your password"
PLAINTEXT_TEMPLATE = Template("""
Hi {{ user_name }},
We received a request to reset the password for your {{ app_name }} account.
To reset your password, click the following link:
{{ reset_link }}
For your security, this link will expire in 30 minutes and can be used only once.
If you did not request a password reset, you can ignore this email.
Best regards,
The {{ app_name }} Team
""".strip())
def send_password_reset_email(user_email: str, user_name: str, reset_link: str) -> None:
app_name = "MyCoolApp"
text_body = PLAINTEXT_TEMPLATE.render(
user_name=user_name or "there",
app_name=app_name,
reset_link=reset_link,
)
# You could also render an HTML template here
send_email(
to=user_email,
subject=RESET_EMAIL_SUBJECT,
text_body=text_body,
html_body=None, # or your HTML content
)This keeps your email logic separate from your password reset logic, which makes it easier to test.
Security Best Practices for Password Reset Emails
This is the most critical part of this chapter. Small mistakes in password reset flows can compromise the entire application.
1. Do not reveal whether an email exists
When a user submits an email to reset their password, your response must be the same in both cases:
- Email is associated with an account.
- Email is not associated with any account.
For example:
{
"message": "If an account with that email exists, we have sent a password reset link."
}This prevents attackers from using the password reset form to enumerate valid email addresses.
2. Use secure random tokens
Use a cryptographically secure random number generator, not simple random functions meant for games or UI.
In Python, good choices are:
secrets.token_urlsafeos.urandomcombined with base64 encoding
Do not use random.random for security tokens.
3. Short expiration times
Tokens should have a limited lifetime. Common values:
| Token type | Typical lifetime |
|---|---|
| Password reset token | 15 minutes to 1 hour |
| Email verification token | 1 to 24 hours |
Shorter lifetimes reduce the risk if an email inbox is compromised or a link is leaked.
4. Single use tokens
Once the user successfully resets their password, you must:
- Delete the token from the database, or
- Mark it as used and reject further use.
Even if the token is still within its time window, it must not be accepted again.
Pseudocode:
function reset_password(token, new_password):
hashed_token = hash(token)
record = find_reset_token_record_by_hash(hashed_token)
if record is None:
return error("Invalid or expired token")
if record.expires_at < now:
delete_record(record)
return error("Invalid or expired token")
user = find_user_by_id(record.user_id)
user.password_hash = hash_password(new_password)
save_user(user)
delete_record(record) # invalidate token
return success()5. Avoid sensitive data in URLs
The token itself is already sensitive because it allows password change. You must not add extra sensitive information such as:
- Plain user ids.
- Password hashes.
- Email verification secrets.
If you include a user id for convenience, make sure it is not the only thing you check. The token must still be necessary and verified on the server.
6. Use HTTPS everywhere
Reset links must only be served over HTTPS. If users click a reset link that goes to HTTP, someone on the network could steal the token.
In your emails, always use https:// URLs in the reset link.
7. Notification for password change
Consider sending a separate email when the password actually changes:
Subject: Your password was changed
Hi {{user_name}},
The password for your {{app_name}} account was changed.
If you made this change, you can ignore this email.
If you did not change your password, please reset your password immediately
and contact our support team.This helps users detect suspicious activity.
8. Limit password reset requests
To reduce abuse, you can:
- Limit how often a user can request a reset.
- Rate limit by IP address.
- Require a short cooldown period between emails to the same address.
Example rules:
- At most 3 reset emails per hour per user.
- At most 10 reset requests per IP per hour.
Rule of thumb: Treat the password reset flow as a login equivalent action. Any protection you add to login, you should also consider here.
Good UX Practices for Password Reset Emails
Security is critical, but user experience is also important. A confusing reset process will cause support requests and frustration.
Make the email recognizable
Use:
- Your application name clearly.
- A consistent sender address, for example
no-reply@example.comorsupport@example.com. - Branding elements in HTML emails if possible, like your logo or colors.
Users often ignore emails that look generic or suspicious.
Make the reset link obvious
The link or button should look clearly clickable. In HTML:
- Use a button style or a large colored link.
- Add enough spacing around it, especially on mobile.
In plaintext:
- Place the link on its own line.
- Avoid line breaks in the middle of the URL if possible.
Explain the next steps
Tell users what to expect:
- "After clicking this link, you will be able to choose a new password."
- "If you close this window, you can always request a new reset email."
This removes confusion and helps non-technical users.
Handle expired links gracefully
If a user clicks an expired link, example behavior:
- Show a clear message: "This link has expired."
- Provide a button to request a new reset email directly.
- Do not show technical error messages.
Example UX on the web page:
This password reset link has expired.
For security reasons, password reset links are only valid for 30 minutes.
You can request a new link by clicking the button below.
[ Request new password reset link ]Implementation Example: Combining the Pieces
To see how everything fits together, consider a simplified Python style example that uses functions you might implement in a web backend.
Data model example
You might have a password_reset_tokens table like:
| Column | Type | Description |
|---|---|---|
| id | integer (PK) | Internal id |
| user_id | integer (FK) | Id of the user |
| token_hash | string | Hash of the token |
| expires_at | datetime | Expiration time |
| created_at | datetime | Creation time |
You store a hash of the token, not the plain token.
Forgot password endpoint (pseudocode)
from datetime import datetime, timedelta
RESET_TOKEN_TTL_MINUTES = 30
def request_password_reset(email: str):
user = find_user_by_email(email)
if user is not None:
token = generate_reset_token()
token_hash = hash_token(token)
expires_at = datetime.utcnow() + timedelta(minutes=RESET_TOKEN_TTL_MINUTES)
save_reset_token(user_id=user.id, token_hash=token_hash, expires_at=expires_at)
reset_link = f"https://app.example.com/reset-password?token={token}"
send_password_reset_email(user_email=user.email, user_name=user.name, reset_link=reset_link)
# Always same response
return {
"message": "If an account with that email exists, we have sent a password reset link."
}Reset password endpoint (pseudocode)
def perform_password_reset(token: str, new_password: str):
token_hash = hash_token(token)
record = find_reset_token_by_hash(token_hash)
if record is None:
return error_response("Invalid or expired reset link", status_code=400)
if record.expires_at < datetime.utcnow():
delete_reset_token(record.id)
return error_response("Invalid or expired reset link", status_code=400)
user = find_user_by_id(record.user_id)
user.password_hash = hash_password(new_password)
save_user(user)
delete_reset_token(record.id)
# Optionally notify user
send_password_changed_notification(user.email, user.name)
return { "message": "Password has been reset successfully." }This example leaves out details that are covered in other chapters, such as:
- How to hash passwords correctly.
- How to implement
send_email. - How to handle errors in a specific framework.
The goal here is to understand the specific responsibilities connected to the password reset emails themselves.
Common Mistakes and How to Avoid Them
Finally, look at typical mistakes related to password reset emails and how to prevent them.
| Mistake | Risk | Better approach |
|---|---|---|
| Revealing if email exists | Account enumeration by attackers | Always return generic message |
| Long lived tokens (days or weeks) | Higher chance of token theft use | Use short lifetimes (15 to 60 minutes) |
| Reusing the same token multiple times | Anyone with link can change repeatedly | Invalidate after first successful use |
| Storing tokens in plaintext | DB leak lets attacker reset any password | Store only token hashes |
| Using HTTP links in emails | Token can be intercepted on network | Use HTTPS URLs only |
| No rate limiting on reset requests | Easy abuse for spam or enumeration | Add per user and per IP limits |
| Cryptographically weak random generator | Predictable tokens | Use dedicated secure random functions |
| Complex or unclear email text | Users ignore or misunderstand email | Short, clear language and simple instructions |
If you avoid these common mistakes, your password reset email flow will already be much safer than many real-world applications.
By now you should be able to:
- Design clear and user friendly password reset emails.
- Build secure reset links that use random tokens.
- Connect your backend logic with email sending for password reset flows.
- Apply key security and UX best practices specific to password reset emails.
Views: 5
KAHIBARO