Python Password Reset Via Email Tutorial

Written by

in

Are you looking for a reliable way to let users reset their forgotten passwords via email in a Python web application? In this step‑by‑step tutorial, we’ll walk you through everything you need—from setting up an SMTP server to generating secure tokens and updating passwords safely. Whether you’re building a Flask micro‑service or a full‑featured Django site, the concepts and code snippets below will give you a solid foundation for a production‑ready password‑reset flow.

Why a Proper Password‑Reset Flow Matters

Implementing a password‑reset feature isn’t just a convenience; it’s a security requirement. A well‑designed flow protects against:

  • Brute‑force attacks on reset tokens.
  • Phishing attempts that exploit poorly crafted emails.
  • Account takeover when tokens are leaked or reused.

By following best practices—such as using time‑limited, cryptographically signed tokens—you’ll keep both your users and your application safe.

Prerequisites and Tools

Before diving into the code, make sure you have the following ready:

  1. Python 3.8+ installed on your development machine.
  2. A virtual environment (recommended venv or conda).
  3. Flask (or Django) installed—this tutorial uses Flask for simplicity.
  4. An SMTP service (Gmail, SendGrid, Mailgun, or a local smtp server).
  5. Basic knowledge of HTML forms and HTTP methods.

Project Setup

1. Create a fresh Flask project

mkdir flask_password_reset
cd flask_password_reset
python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install Flask Flask-Mail itsdangerous

2. Directory structure

.
├── app.py
├── config.py
├── templates/
│   ├── reset_request.html
│   ├── reset_token.html
│   └── email_template.html
└── models.py   # simple user storage (SQLite for demo)

Configuring Email with Flask‑Mail

Flask‑Mail abstracts the SMTP details and lets you send HTML emails with just a few lines of code.

# config.py
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = 'your.email@example.com'
MAIL_PASSWORD = 'your‑app‑password'   # Use an app‑specific password!
MAIL_DEFAULT_SENDER = ('YourApp Support', MAIL_USERNAME)

# Secret key for signing tokens
SECRET_KEY = 'a‑very‑strong‑random‑string‑replace‑this'

Generating Secure Reset Tokens

We’ll use the itsdangerous library, which ships with Flask, to create time‑limited, signed tokens.

# models.py (simplified user model)
from itsdangerous import URLSafeTimedSerializer
from config import SECRET_KEY

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

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

Building the Reset Request Form

The first step in the user journey is a form where the user enters their email address.

# templates/reset_request.html
<!DOCTYPE html>
<html>
<body>
    <h2>Forgot Your Password?</h2>
    <form method="POST" action="/reset-request">
        <label for="email">Enter your email:</label><br>
        <input type="email" name="email" id="email" required><br><br>
        <button type="submit">Send Reset Link</button>
    </form>
</body>
</html>

Handling the Reset Request in Flask

# app.py
from flask import Flask, render_template, request, flash, redirect, url_for
from flask_mail import Mail, Message
from models import generate_reset_token, verify_reset_token
import sqlite3

app = Flask(__name__)
app.config.from_pyfile('config.py')
mail = Mail(app)

def get_user_by_email(email):
    conn = sqlite3.connect('users.db')
    cur = conn.cursor()
    cur.execute('SELECT id, email FROM users WHERE email = ?', (email,))
    user = cur.fetchone()
    conn.close()
    return user

@app.route('/reset-request', methods=['GET', 'POST'])
def reset_request():
    if request.method == 'POST':
        email = request.form['email']
        user = get_user_by_email(email)
        if user:
            token = generate_reset_token(email)
            reset_url = url_for('reset_token', token=token, _external=True)
            # Send email
            msg = Message('Password Reset for YourApp',
                          recipients=[email])
            msg.html = render_template('email_template.html', reset_url=reset_url)
            mail.send(msg)
            flash('A password reset link has been sent to your email.', 'info')
        else:
            flash('If the email exists in our system, a reset link will be sent.', 'info')
        return redirect(url_for('reset_request'))
    return render_template('reset_request.html')

Designing the Reset Email Template

# templates/email_template.html
<p>Hello,</p>
<p>We received a request to reset your password. Click the link below to choose a new password. This link will expire in 1 hour.</p>
<p><a href="{{ reset_url }}" style="background:#4CAF50;color:#fff;padding:10px 15px;text-decoration:none;">Reset My Password</a></p>
<p>If you didn’t request a password reset, you can safely ignore this email.</p>
<p>Thanks,<br>YourApp Team</p>

Creating the Password‑Reset Form

When the user clicks the link, they’ll land on a page where they can set a new password.

# templates/reset_token.html
<!DOCTYPE html>
<html>
<body>
    <h2>Set a New Password</h2>
    <form method="POST" action="{{ url_for('reset_token', token=token) }}">
        <label for="password">New Password:</label><br>
        <input type="password" name="password" id="password" required><br><br>
        <label for="confirm">Confirm Password:</label><br>
        <input type="password" name="confirm" id="confirm" required><br><br>
        <button type="submit">Update Password</button>
    </form>
</body>
</html>

Processing the New Password

# app.py (continued)
def update_user_password(email, new_password):
    # In a real app, hash the password with bcrypt or Argon2
    hashed = new_password  # placeholder; replace with proper hashing
    conn = sqlite3.connect('users.db')
    cur = conn.cursor()
    cur.execute('UPDATE users SET password = ? WHERE email = ?', (hashed, email))
    conn.commit()
    conn.close()

@app.route('/reset/', 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':
        pwd = request.form['password']
        confirm = request.form['confirm']
        if pwd != confirm:
            flash('Passwords do not match.', 'warning')
            return render_template('reset_token.html', token=token)
        # TODO: add password strength validation here
        update_user_password(email, pwd)
        flash('Your password has been updated. You can now log in.', 'success')
        return redirect(url_for('login'))   # assume a login route exists
    return render_template('reset_token.html', token=token)

Security Best Practices

Even though the code above works, production deployments should follow these extra safeguards:

  • Hash passwords securely using bcrypt, argon2-cffi, or passlib. Never store plain‑text passwords.
  • Rate‑limit reset requests per IP or per user to mitigate enumeration attacks.
  • Use HTTPS for every endpoint, especially the reset link.
  • Log token usage for audit trails, but never log the

Comments

Leave a Reply

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