KAHIBARO
Discord Login Register

18.6. Password Reset Emails

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:

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:

  1. User requests a reset
    • User goes to "Forgot password" page.
    • Enters their email address.
  2. 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.
  3. User receives the email
    • Opens the email.
    • Clicks the reset link.
  4. 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.
  5. 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:

Essential elements

A good password reset email typically contains:

  1. Clear subject line

Examples:

Avoid subject lines that are vague or that look like spam.

  1. Greeting

If you know the user name, use it:

If you do not, a generic greeting is fine:

  1. Short explanation

Explain why the user is receiving the email:

  1. The reset action (call to action)

This is usually a button or a link:

  1. Expiration information

Tell the user that the link will expire:

  1. What to do if they did not request it

Very important for security awareness:

  1. Support information

Optional but useful:

Example plaintext email

Here is a simple plaintext password reset email:

text
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}} Team

Example HTML email with a button

html
<!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:

text
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:

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:

ApproachDescriptionProsCons
Random string in DBRandom bytes stored hashed in databaseSimple, secure, revocableRequires DB lookup
Signed token (JWT)Token with payload signed by server secretNo DB if purely statelessRevocation is harder
HybridSigned token + optional DB storageFlexible, extra controlMore complex implementation

For beginners, the database approach with a random string is easier to implement correctly.

Example token generation (Python)
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:

  1. Receive the password reset request.
  2. Generate and store the token with expiration.
  3. Build the reset link.
  4. Render an email template with the reset link.
  5. Send the email through SMTP or an email service.

Handling the "Forgot password" request

You might define an endpoint such as:

http
POST /auth/forgot-password
Content-Type: application/json
{
  "email": "user@example.com"
}

Backend logic in pseudocode:

text
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:

Examples:

text
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:

python
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:

For example:

json
{
  "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:

Do not use random.random for security tokens.

3. Short expiration times

Tokens should have a limited lifetime. Common values:

Token typeTypical lifetime
Password reset token15 minutes to 1 hour
Email verification token1 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:

Even if the token is still within its time window, it must not be accepted again.

Pseudocode:

text
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:

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:

text
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:

Example rules:

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:

Users often ignore emails that look generic or suspicious.

Make the reset link obvious

The link or button should look clearly clickable. In HTML:

In plaintext:

Explain the next steps

Tell users what to expect:

This removes confusion and helps non-technical users.

Handle expired links gracefully

If a user clicks an expired link, example behavior:

Example UX on the web page:

text
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:

ColumnTypeDescription
idinteger (PK)Internal id
user_idinteger (FK)Id of the user
token_hashstringHash of the token
expires_atdatetimeExpiration time
created_atdatetimeCreation time

You store a hash of the token, not the plain token.

Forgot password endpoint (pseudocode)

python
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)

python
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:

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.

MistakeRiskBetter approach
Revealing if email existsAccount enumeration by attackersAlways return generic message
Long lived tokens (days or weeks)Higher chance of token theft useUse short lifetimes (15 to 60 minutes)
Reusing the same token multiple timesAnyone with link can change repeatedlyInvalidate after first successful use
Storing tokens in plaintextDB leak lets attacker reset any passwordStore only token hashes
Using HTTP links in emailsToken can be intercepted on networkUse HTTPS URLs only
No rate limiting on reset requestsEasy abuse for spam or enumerationAdd per user and per IP limits
Cryptographically weak random generatorPredictable tokensUse dedicated secure random functions
Complex or unclear email textUsers ignore or misunderstand emailShort, 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:

Views: 5

Comments

Please login to add a comment.

Don't have an account? Register now!