Python Password Reset Via Email Tutorial

Written by

in

Implementing a secure Python password reset via email is a must‑have feature for any modern web application. Not only does it improve user experience, but it also protects your platform from unauthorized access. In this tutorial we’ll walk through the entire process—from generating a reset token to sending the email and finally updating the user’s password—using plain Python, Flask, and the smtplib library. By the end of the article you’ll have a production‑ready password‑reset flow that you can drop into any project.

What Makes a Good Password‑Reset System?

  • Security first: Use cryptographically strong tokens that expire after a short period.
  • Usability: The email should contain a clear call‑to‑action and a single clickable link.
  • Reliability: Handle bounced emails and rate‑limit requests to prevent abuse.
  • Scalability: Store tokens in a way that works for both small apps and large deployments.

High‑Level Workflow

  1. User clicks “Forgot password?” on the login page.
  2. App asks for the user’s email address.
  3. Server generates a signed, time‑limited token.
  4. Email containing a password‑reset link (with the token) is sent to the user.
  5. User clicks the link, which opens a secure reset form.
  6. Server validates the token, updates the password, and invalidates the token.

Setting Up the Development Environment

Before we dive into code, make sure you have the following installed:

  • Python 3.9+ (download here)
  • Flask (pip install flask)
  • itsdangerous (comes with Flask, used for token signing)
  • An SMTP service (Gmail, SendGrid, Mailgun, etc.)

Project Structure

password_reset_demo/
├── app.py
├── config.py
├── templates/
│   ├── forgot_password.html
│   ├── reset_password.html
│   └── email_template.html
└── requirements.txt

Creating a Secure Token with itsdangerous

The itsdangerous library lets us create a signed token that includes the user’s email and an expiration timestamp. This token can be safely embedded in a URL.

# config.py
import os

class Config:
    SECRET_KEY = os.getenv('SECRET_KEY', 'a-very-secret-key')
    SECURITY_PASSWORD_SALT = os.getenv('SECURITY_PASSWORD_SALT', 'another-secret')
    MAIL_SERVER = 'smtp.gmail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.getenv('MAIL_USERNAME')
    MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
    MAIL_DEFAULT_SENDER = MAIL_USERNAME
# 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=current_app.config['SECURITY_PASSWORD_SALT'])

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

Sending the Reset Email

We’ll use Python’s built‑in smtplib for simplicity, but you can replace it with Flask‑Mail or any third‑party service.

# mailer.py
import smtplib
from email.mime.text import MIMEText
from flask import url_for, render_template, current_app

def send_reset_email(to_email, token):
    reset_url = url_for('reset_password', token=token, _external=True)
    html_body = render_template('email_template.html', reset_url=reset_url)

    msg = MIMEText(html_body, 'html')
    msg['Subject'] = 'Your Password Reset Link'
    msg['From'] = current_app.config['MAIL_DEFAULT_SENDER']
    msg['To'] = to_email

    with smtplib.SMTP(current_app.config['MAIL_SERVER'],
                      current_app.config['MAIL_PORT']) as server:
        server.starttls()
        server.login(current_app.config['MAIL_USERNAME'],
                     current_app.config['MAIL_PASSWORD'])
        server.send_message(msg)

Flask Routes: From Request to Reset

# app.py
from flask import Flask, render_template, request, flash, redirect, url_for
from config import Config
from utils import generate_reset_token, verify_reset_token
from mailer import send_reset_email
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.config.from_object(Config)

# In‑memory user store for demo purposes
USERS = {
    'alice@example.com': {
        'password_hash': generate_password_hash('oldpassword')
    }
}

@app.route('/forgot', methods=['GET', 'POST'])
def forgot_password():
    if request.method == 'POST':
        email = request.form['email'].strip().lower()
        if email in USERS:
            token = generate_reset_token(email)
            send_reset_email(email, token)
            flash('A password reset link has been sent to your email.', 'info')
        else:
            flash('If that address exists in our system, you will receive an email.', 'info')
        return redirect(url_for('forgot_password'))
    return render_template('forgot_password.html')

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

    if request.method == 'POST':
        new_password = request.form['password']
        USERS[email]['password_hash'] = generate_password_hash(new_password)
        flash('Your password has been updated!', 'success')
        return redirect(url_for('login'))  # Assume a login route exists
    return render_template('reset_password.html', token=token)

if __name__ == '__main__':
    app.run(debug=True)

HTML Templates (Simplified)


<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>
    <button type="submit">Update Password</button>
</form>

<h3>Password Reset Request</h3>
<p>Hi,</p>
<p>We received a request to reset your password. Click the button below to choose a new password. This link will expire in 1 hour.</p>
<a href="{{ reset_url }}" style="display:inline-block;padding:10px 20px;background:#0069d9;color:#fff;text-decoration:none;border-radius:5px;">Reset My Password</a>
<p>If you didn’t request this, you can safely ignore this email.</p>

Best Practices & Security Tips

  • Never store plain‑text passwords. Always hash with werkzeug.security.generate_password_hash or bcrypt.
  • Use HTTPS. The reset link must be served over TLS to prevent token sniffing.
  • Rate limit requests. Limit “forgot password” attempts per IP/email to mitigate brute‑force attacks.
  • Invalidate old tokens. Store a token identifier in the database and mark it used after a successful reset.
  • Short token lifespan. 30‑60 minutes is a good balance between usability and security.

Testing the Flow Locally

  1. Set environment variables for MAIL_USERNAME and MAIL_PASSWORD.
  2. Run python app.py and navigate to http://127.0.0.1:5000/forgot.
  3. Enter a registered email (e.g., alice@example.com) and check your inbox for the reset link.
  4. Click the link, set a new password, and verify that the hash in the USERS dict changes.

Deploying to Production

When you move from a development server to production, keep these items in mind:

  • Replace the in‑memory USERS store with a real database (PostgreSQL, MySQL, etc.).
  • Use a robust email service (SendGrid, Amazon SES) to handle deliverability and bounce tracking.
  • Store SECRET_KEY and SECURITY_PASSWORD_SALT as environment variables, never in source control.
  • Enable Flask’s SESSION_COOKIE_SECURE and CS

Comments

Leave a Reply

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