Python Password Reset Via Email Tutorial

Written by

in

Forgotten passwords are a common pain point for users, and a seamless password‑reset flow can dramatically improve the experience on any web application. In this tutorial you’ll learn how to implement a robust “Python password reset via email” feature from scratch, using only standard libraries and a lightweight Flask framework. We’ll cover everything from generating secure tokens and configuring an SMTP server to sending reset links and validating them safely. By the end, you’ll have a production‑ready password‑reset system that boosts security, reduces support tickets, and keeps your users happy.

Why a Proper Email‑Based Reset Is Essential

When users forget their credentials, a poorly designed reset process can expose your site to account takeover, phishing, or brute‑force attacks. An effective solution must:

  • Verify the user’s identity via a unique, time‑limited token.
  • Send the token through a trusted email channel.
  • Store the token securely and invalidate it after use.
  • Protect against enumeration attacks by providing generic responses.

Following these best practices ensures that the “Python password reset via email” flow is both user‑friendly and secure.

Prerequisites

  • Python 3.8+ installed on your development machine.
  • Basic knowledge of Flask (or any micro‑framework) and HTML forms.
  • An SMTP service (Gmail, SendGrid, Mailgun, etc.) with credentials.
  • A SQLite or any relational database to store users and reset tokens.

Setting Up the Project Structure

myapp/
├─ app.py
├─ models.py
├─ utils.py
├─ templates/
│  ├─ reset_request.html
│  └─ reset_password.html
└─ requirements.txt

This layout keeps the core logic, database models, and utility functions separate, making the code easier to maintain and extend.

Step 1: Configuring the SMTP Server

First, install the required package and add your mail settings to app.py:

pip install Flask Flask-Mail itsdangerous
from flask import Flask
from flask_mail import Mail, Message

app = Flask(__name__)

# ==== SMTP SETTINGS ====
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'your_email@gmail.com'
app.config['MAIL_PASSWORD'] = 'your_app_password'  # use App Passwords for Gmail
mail = Mail(app)

Replace the placeholders with your actual credentials. For production, store these values in environment variables and load them with python-dotenv to avoid hard‑coding secrets.

Step 2: Generating a Secure Reset Token

We’ll use itsdangerous.URLSafeTimedSerializer to create a signed token that expires after a configurable period (default 1 hour).

# utils.py
from itsdangerous import URLSafeTimedSerializer
from flask import current_app

def generate_reset_token(email):
    serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
    return serializer.dumps(email, salt='password-reset-salt')

def verify_reset_token(token, expiration=3600):
    serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
    try:
        email = serializer.loads(
            token,
            salt='password-reset-salt',
            max_age=expiration
        )
    except Exception:
        return None
    return email

Make sure app.config['SECRET_KEY'] is a long, random string. The token contains only the user’s email, but it is cryptographically signed, so it cannot be forged.

Step 3: Creating the Password‑Reset Request View

This endpoint displays a form where users enter their email address. If the address exists, we send a reset link; otherwise, we return the same generic message to prevent user enumeration.

# app.py (continued)
from flask import render_template, request, flash, redirect, url_for
from utils import generate_reset_token
from models import User  # assume a simple ORM model with .query.filter_by

@app.route('/reset-password', methods=['GET', 'POST'])
def reset_request():
    if request.method == 'POST':
        email = request.form.get('email')
        user = User.query.filter_by(email=email).first()
        if user:
            token = generate_reset_token(user.email)
            send_reset_email(user.email, token)
        # Always flash the same message
        flash('If an account with that email exists, a reset link has been sent.', 'info')
        return redirect(url_for('login'))
    return render_template('reset_request.html')

Sending the Reset Email

# utils.py (continued)
def send_reset_email(to_email, token):
    reset_url = url_for('reset_token', token=token, _external=True)
    subject = "Your Password Reset Link"
    html_body = f"""
    

Hello,

Click the link below to reset your password. This link will expire in 1 hour.

Reset My Password

If you didn’t request this, you can safely ignore this email.

""" msg = Message(subject, recipients=[to_email], html=html_body, sender=app.config['MAIL_USERNAME']) mail.send(msg)

The _external=True flag generates an absolute URL, which is required for email links.

Step 4: Building the Reset‑Password Form

The link in the email points to a route that validates the token and presents a new password form.

# app.py (continued)
from utils import verify_reset_token

@app.route('/reset-password/', methods=['GET', 'POST'])
def reset_token(token):
    email = verify_reset_token(token)
    if not email:
        flash('The reset link is invalid or has expired.', 'danger')
        return redirect(url_for('reset_request'))

    if request.method == 'POST':
        password = request.form.get('password')
        confirm = request.form.get('confirm')
        if password != confirm:
            flash('Passwords do not match.', 'warning')
            return redirect(request.url)

        user = User.query.filter_by(email=email).first_or_404()
        user.set_password(password)   # assume a method that hashes the password
        db.session.commit()
        flash('Your password has been updated! You can now log in.', 'success')
        return redirect(url_for('login'))

    return render_template('reset_password.html', token=token)

HTML Templates (Simplified)

Below are minimal examples; feel free to style them with Bootstrap or Tailwind.


<form method="POST">
    <label for="email">Enter your email:</label>
    <input type="email" name="email" id="email" required>
    <button type="submit">Send Reset Link</button>
</form>

<form method="POST">
    <label for="password">New password:</label>
    <input type="password" name="password" id="password" required>
    <label for="confirm">Confirm password:</label>
    <input type="password" name="confirm" id="confirm" required>
    <button type="submit">Update Password</button>
</form>

Step 5: Securing the Workflow

Even a functional reset flow can be vulnerable if not hardened. Implement the following safeguards:

  • Rate limiting: Use Flask‑Limiter or a reverse proxy to restrict the number of reset requests per IP address.
  • Token reuse protection: Store a hash of the token in the database and mark it as used after a successful password change.
  • HTTPS everywhere: Ensure both the reset request page and the reset link are served over TLS.
  • Strong password policy: Enforce minimum length, complexity, and check against breached‑password lists.
  • Audit logging: Record reset attempts, successes, and failures for forensic analysis.

Common Pitfalls and How to Avoid Them

1. Sending Plain‑Text Passwords

Never include the new password in the email. Always let the user choose it after clicking the secure link.

2. Using Predictable Tokens

Do not roll your own random string. Libraries like itsdangerous or jwt provide cryptographically signed tokens that are far more secure.

3. Forgetting to Expire Tokens

Tokens that never expire become a goldmine for attackers. Set a reasonable max_age (e.g., 3600 seconds) and enforce it on verification.

4. Hard‑Coding Secrets

Never commit SECRET_KEY, SMTP passwords, or API keys to version control. Use environment variables or secret managers (AWS Secrets Manager, HashiCorp Vault).

Testing the Complete Flow

  1. Run the Flask app locally: flask run

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *