KAHIBARO
Discord Login Register

13.14. Password Reset

Why Password Reset Matters

Almost every real application needs a way for users to recover access to their accounts. People forget passwords, accounts get locked, emails change. Without a secure password reset flow, users get stuck or, worse, attackers can easily take over accounts.

So you must design a password reset system that is:

In this chapter we focus on the mechanics and security of password reset. We will not re-explain password hashing and storage, which are covered in the Password Hashing chapter. Here we assume you already store passwords securely, for example with bcrypt or Argon2.


Typical Password Reset Flow

Most sites follow a similar flow. Understanding this helps you design and implement your own.

  1. User clicks “Forgot password?”
  2. User enters their email (or username).
  3. Backend checks that account exists, and if so, creates a one-time reset token.
  4. Backend sends an email with a link that contains the token.
  5. User clicks the link, backend verifies the token and shows a “Set new password” form.
  6. User submits a new password.
  7. Backend verifies the token again, sets the new password, invalidates the token, and logs the user in or redirects to login.

You can think of the reset token as a temporary permission to change a specific account’s password.


Password Reset Token Design

What is a Reset Token?

A password reset token is a secret value that proves:

You never ask the user to type the token manually. It is embedded in a URL sent by email.

Example reset link:

text
https://example.com/reset-password?token=5aXszT0L8t0R4d0mkT2Y7w

The token must be:

Important rule: A password reset token must be treated like a password.
Never log it in plaintext, never expose how you generate it, and never reuse it.

Token Length and Randomness

Use a cryptographically secure random generator, not a normal random function.

In Python:

python
import secrets
token = secrets.token_urlsafe(32)

This produces a URL safe string with good entropy.

Approximate entropy:

128 bits is already extremely strong. 32 bytes is commonly used and very safe.

Why Use URL Safe Tokens?

Some characters are special in URLs. token_urlsafe uses only characters that are safe to put in a query string without extra encoding. This makes links easier to click and handle.


Stateless vs Stateful Reset Tokens

There are two main approaches to token design. Both are used in real applications.

Stateful Tokens (Database Stored)

With a stateful token, you:

  1. Generate a random token.
  2. Store it (or its hash) in your database with:
    • The user id it belongs to.
    • Expiration time.
    • Whether it was used already.
  3. Send the raw token in the email.
  4. When the user clicks the link, you look up the token in the database.

Table Example

You could have a table like:

ColumnTypeDescription
idUUID / integerInternal id
user_idUUID / integerWhich user this token belongs to
token_hashstringHash of the token (not token itself)
expires_atdatetimeWhen token becomes invalid
used_atdatetime / nullWhen it was used
created_atdatetimeAudit info
ip_addressstring / nullIP that requested the reset (optional)
user_agentstring / nullClient info (optional)

Notice we store token_hash instead of the raw token. That way if your database is compromised, attackers do not get instant reset tokens.

Example in Python:

python
import secrets
import hashlib
from datetime import datetime, timedelta
def create_reset_token(user_id, db_session):
    raw_token = secrets.token_urlsafe(32)
    token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
    expires_at = datetime.utcnow() + timedelta(hours=1)
    db_session.execute(
        """
        INSERT INTO password_reset_tokens (user_id, token_hash, expires_at)
        VALUES (:user_id, :token_hash, :expires_at)
        """,
        {"user_id": user_id, "token_hash": token_hash, "expires_at": expires_at},
    )
    db_session.commit()
    return raw_token

To verify:

python
def verify_reset_token(raw_token, db_session):
    token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
    row = db_session.execute(
        """
        SELECT * FROM password_reset_tokens
        WHERE token_hash = :token_hash
        """,
        {"token_hash": token_hash},
    ).fetchone()
    if row is None:
        return None  # invalid token
    if row.used_at is not None:
        return None  # already used
    if row.expires_at < datetime.utcnow():
        return None  # expired
    return row.user_id

Pros and Cons

ApproachProsCons
Stateful tokensEasy to revoke, invalidate, auditNeeds database reads and writes

Stateful tokens are common and relatively easy to implement.

Stateless Tokens (Signed Data, like JWT)

With stateless tokens, all data is encoded and cryptographically signed. The server does not have to store each token.

Example contents:

json
{
  "sub": "user-id-123",
  "type": "password_reset",
  "exp": 1712345678
}

You sign it with a secret key and send it as a compact string. On verification, you:

This approach is similar to JSON Web Tokens, but you can also design a custom format.

Pros and Cons

ApproachProsCons
Stateless tokensNo DB lookup needed for each verificationRevocation is harder, needs careful design

For many applications, stateful tokens are simpler and safer to start with. Stateless tokens are more advanced and must be designed carefully if you need revocation.


Token Lifetime and Expiration

If tokens live too long, an attacker who gains access to an old email can take over the account. If they are too short, real users may be frustrated.

Common lifetimes:

LifetimeUse case
15 minutesVery security sensitive apps
1 hourMost web apps, good balance
24 hoursVery user friendly, but more risk

Rule: Always store an explicit expiration time, and always check it before accepting a token.

You can store expires_at in the database for stateful tokens, or use exp for stateless tokens.


Token Reuse and Invalidation

A reset token should be single-use. After a successful reset, you must ensure it cannot be used again.

For stateful tokens:

  1. After verifying token:
    • Mark used_at with current time.
    • Or delete the row from the table.
  2. Make sure future checks verify used_at IS NULL.

For stateless tokens, you need a strategy, for example:

Example idea:

Creating the Reset Request Endpoint

Endpoint Design

Typically:

Request body:

json
{
  "email": "user@example.com"
}

Response:

json
{
  "message": "If an account with that email exists, we have sent a password reset link."
}

Why Hide the Existence of Accounts?

If you return different responses for existing and non-existing emails:

Protecting this information is a basic security practice.


Sending the Reset Email

Email Content

A simple reset email might look like:

Subject: Reset your password

Hi Alice,

We received a request to reset your password for your ExampleApp account.

Click this link to choose a new password:
https://example.com/reset-password?token=5aXszT0L8t0R4d0mkT2Y7w

This link will expire in 1 hour.

If you did not request this, you can safely ignore this email.

ExampleApp Security Team

Key elements:

Do Not Include Passwords in Email

Never send:

Always let the user choose a new password through a secure form.


Reset Form Endpoint

When the user clicks the link, they visit something like:

text
GET /reset-password?token=XYZ

Your frontend:

text
New password: [.................]
Confirm password: [.............]
[Submit]

On submit the frontend sends:

http
POST /reset-password
Content-Type: application/json
{
  "token": "XYZ",
  "new_password": "NewSecure#Password1"
}

Your backend will:

  1. Validate the body (presence of token and new password).
  2. Verify that the token is valid.
  3. Validate password strength.
  4. Hash the new password.
  5. Update the user record.
  6. Invalidate the token.
  7. Return success.

Integrating with Password Hashing

You already learned how to hash passwords when users register or change their passwords normally. The reset flow must use the same hashing function.

Example in Python with bcrypt using passlib:

python
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
    return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)

In the reset endpoint:

python
def reset_password(token: str, new_password: str, db_session):
    user_id = verify_reset_token(token, db_session)
    if user_id is None:
        raise InvalidTokenError()
    password_hash = hash_password(new_password)
    db_session.execute(
        "UPDATE users SET password_hash = :password_hash WHERE id = :user_id",
        {"password_hash": password_hash, "user_id": user_id},
    )
    # Invalidate token
    mark_token_used(token, db_session)
    db_session.commit()

Make sure:

Password Validation and Security

Password Strength Rules

You should define password rules that are not too weak but also not impossible to use. Examples:

Example simple rule:

Example rule:
A valid password must be at least 8 characters long and contain at least one letter and one digit.

You can implement this with regular expressions or manual checks.

In Python:

python
import re
def is_valid_password(pw: str) -> bool:
    if len(pw) < 8:
        return False
    if not re.search(r"[A-Za-z]", pw):
        return False
    if not re.search(r"[0-9]", pw):
        return False
    return True

You should perform the same validation on:

Prevent Reuse of Old Passwords (Optional)

Some systems require that the new password is not equal to the old one. You can:

python
    if new_password == old_password_hash:  # WRONG, they cannot be equal
        ...
python
    if verify_password(new_password, old_password_hash):
        # same password as before

This is more advanced, and many simple apps do not implement password history. Decide based on your security needs.


Security Protections Around Password Reset

Password reset is a powerful feature, so it is a common target for attackers. You must protect it against abuse.

Rate Limiting Reset Requests

Attackers might:

You can mitigate with rate limiting:

Example policies:

TargetLimit example
Per email3 reset emails per hour
Per IP address10 reset requests per 10 minutes

If the limit is hit, you can still respond with the generic message:

"If an account with that email exists, we have sent a password reset link."

But internally you do not send another email.

Throttling Token Verification

While verifying tokens, you should:

Example:

python
import time
def verify_reset_token_with_delay(token: str, db_session):
    user_id = verify_reset_token(token, db_session)
    if user_id is None:
        time.sleep(0.5)  # Add small delay
    return user_id

Be careful not to introduce big delays that can be used for DoS, but small, consistent delays can make brute-force less effective.

Secure Transport: Always Use HTTPS

Reset links contain secrets. If you send them over plain HTTP:

You must:

Avoid Leaking Tokens in Logs and Referrers

If the reset token is in the query string, for example:

text
https://example.com/reset-password?token=XYZ

It might appear in:

To reduce risk:

A common approach:

Example: End-to-End Flow (Simplified)

To make it concrete, here is a simple text sequence of the whole flow using a stateful token.

  1. User opens login page and clicks "Forgot password?"
  2. Frontend shows form:
http
   POST /auth/forgot-password
   Content-Type: application/json
   {
     "email": "user@example.com"
   }
  1. Backend:
    • Looks up user@example.com. If user exists:
      • Generates random token "XYZ".
      • Stores hash("XYZ") in password_reset_tokens with user_id, expires_at.
      • Sends email with link https://app.example.com/reset-password?token=XYZ.
    • Returns generic response.
  2. User opens email, clicks link.
  3. Browser hits:
http
   GET /reset-password?token=XYZ
  1. Frontend:
    • Reads token=XYZ from URL.
    • Shows form to enter new password.
  2. User submits new password:
http
   POST /reset-password
   Content-Type: application/json
   {
     "token": "XYZ",
     "new_password": "NewSecure#Password1"
   }
  1. Backend:
    • Looks up token hash.
    • Verifies not expired and not used.
    • Validates password strength.
    • Hashes the new password.
    • Updates the user record in users table.
    • Marks token as used or deletes it.
    • Responds with success.
  2. Optional:
    • Invalidate all existing sessions for that user.
    • Send an email: "Your password was changed. If this was not you, contact support."

Extra Safety Measures

As your applications grow more important, you may want additional protections.

Notify on Password Reset Requests

When someone starts a password reset for an account, you can notify the user:

Example email for reset requested:

We received a request to reset the password for your account.
If you did not request this, someone may be trying to access your account.
If you are concerned, contact support.

Even if an attacker triggers these, the user learns that something is happening.

Invalidating Existing Sessions After Reset

If an attacker had access to a user's session but not the email, and the user resets the password, you want to kick the attacker out.

You can:

When a password is reset:

This is more advanced but very effective.


Common Mistakes to Avoid

Here are some mistakes you must avoid when building password reset.

MistakeWhy it is dangerous
Using short, guessable tokensAllows brute-force attacks
Storing tokens in plaintext in the databaseDB leak equals full account takeover
No expiration time on tokensOld emails can reset accounts years later
Allowing token reuseIf someone sees the link, they can reuse it
Revealing whether an email existsEnables account enumeration
Sending new passwords via emailEmail is not a safe channel for passwords
Logging tokens or passwordsLogs often have wide access
Using HTTP instead of HTTPSTokens can be intercepted on the network

Try to avoid all of these in your own implementation.


Summary

In password reset, you give users a safe way to recover accounts, but you also open a powerful entry point into your system. The key ideas are:

If you follow these guidelines, your password reset feature will be both user friendly and secure, and will fit cleanly into the broader authentication system you are building.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!