Ever struggled to let users regain access to their accounts without compromising security? A reliable password‑reset flow is a must‑have feature for any modern web application, and Python makes it surprisingly straightforward. In this tutorial we’ll walk through every step required to build a secure, email‑based password reset system—from generating a time‑limited token to sending the reset link via SMTP and finally updating the user’s password safely. Whether you’re using Flask, Django, or a lightweight script, the concepts remain the same, and you’ll walk away with production‑ready code you can drop into your next project.
Why a Proper Password Reset Matters
Implementing a password reset isn’t just about convenience; it’s a critical security control. A well‑designed flow prevents:
- Account takeover: Attackers can’t guess or reuse old passwords.
- Phishing exploits: Tokens are short‑lived and bound to a specific user.
- Brute‑force attacks: Rate limiting and token expiration stop automated attempts.
By following best practices—such as using cryptographically strong tokens, HTTPS, and secure hashing—you protect both your users and your brand reputation.
High‑Level Overview of the Process
- Request Reset: User submits their email address.
- Generate Token: Server creates a signed, time‑limited token.
- Send Email: An email containing a reset URL is dispatched via SMTP.
- Validate Token: When the user clicks the link, the token is verified.
- Update Password: User enters a new password, which is hashed and stored.
Setting Up the Environment
Required Packages
For this tutorial we’ll use Flask as the web framework, itsdangerous for token handling, and Flask-Mail (or smtplib for a pure‑Python approach). Install them with:
pip install Flask itsdangerous Flask-Mail python-dotenv
We also recommend python-dotenv to keep secret keys out of source control.
Project Structure
.
├── app.py
├── config.py
├── templates
│ ├── reset_request.html
│ ├── reset_password.html
│ └── email_reset.html
└── .env
Step‑by‑Step Implementation
1. Configure Flask and Mail Settings
# config.py
import os
from dotenv import load_dotenv
load_dotenv() # Loads variables from .env
class Config:
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
SECURITY_PASSWORD_SALT = os.getenv('SECURITY_PASSWORD_SALT', 'dev-salt')
# SMTP configuration
MAIL_SERVER = os.getenv('MAIL_SERVER', 'smtp.gmail.com')
MAIL_PORT = int(os.getenv('MAIL_PORT', 587))
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'true').lower() == 'true'
MAIL_USERNAME = os.getenv('MAIL_USERNAME')
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
2. Initialize Flask, Mail, and Token Serializer
# app.py
from flask import Flask, render_template, request, flash, redirect, url_for
from flask_mail import Mail, Message
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
mail = Mail(app)
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
3. Create the Reset Request Form
The user supplies their email address. If the address exists in the database, we generate a token and send the email.
@app.route('/reset', methods=['GET', 'POST'])
def reset_request():
if request.method == 'POST':
email = request.form['email']
# TODO: Replace with real DB lookup
user = get_user_by_email(email)
if user:
token = serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT'])
reset_url = url_for('reset_token', token=token, _external=True)
send_reset_email(user.email, reset_url)
flash('A password reset link has been sent to your email.', 'info')
return redirect(url_for('login'))
else:
flash('Email address not found.', 'danger')
return render_template('reset_request.html')
4. Sending the Reset Email
def send_reset_email(to_email, reset_url):
subject = "Your Password Reset Link"
html_body = render_template('email_reset.html', reset_url=reset_url)
msg = Message(subject=subject, recipients=[to_email], html=html_body)
mail.send(msg)
5. Build the Email Template
<!-- templates/email_reset.html -->
<p>Hello,</p>
<p>You requested a password reset. Click the link below to set a new password. This link will expire in 30 minutes.</p>
<p><a href="{{ reset_url }}">Reset My Password</a></p>
<p>If you didn’t request this, please ignore this email.</p>
<p>Thanks,<br>Your Application Team</p>
6. Validate the Token and Show the New Password Form
@app.route('/reset/<token>', methods=['GET', 'POST'])
def reset_token(token):
try:
email = serializer.loads(
token,
salt=app.config['SECURITY_PASSWORD_SALT'],
max_age=1800 # 30 minutes
)
except SignatureExpired:
flash('The reset link has expired.', 'danger')
return redirect(url_for('reset_request'))
except BadSignature:
flash('Invalid reset token.', 'danger')
return redirect(url_for('reset_request'))
if request.method == 'POST':
password = request.form['password']
confirm = request.form['confirm']
if password != confirm:
flash('Passwords do not match.', 'danger')
return render_template('reset_password.html')
# TODO: Hash password and update DB
update_user_password(email, password)
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)
7. Secure Password Storage
Never store plain‑text passwords. Use werkzeug.security.generate_password_hash (or bcrypt) to hash the new password before saving.
from werkzeug.security import generate_password_hash
def update_user_password(email, raw_password):
hashed = generate_password_hash(raw_password)
# Replace with actual DB update logic
user = get_user_by_email(email)
user.password_hash = hashed
db.session.commit()
8. Adding Rate Limiting (Optional but Recommended)
To stop abuse, integrate Flask-Limiter or implement a simple counter in your database that tracks how many reset requests a user makes within a given timeframe.
Testing the Flow Locally
- Set up a
.envfile with your SMTP credentials (e.g., Gmail app password). - Run
flask runand navigate to/reset. - Enter a registered email address; you should receive a reset link.
- Click the link, change the password, and verify you can log in with the new credentials.
If you’re using Gmail, remember to enable “Less secure app access” or, better yet, create an App Password for added security.
Deploying to Production
Key Checklist
- HTTPS Only: Force SSL/TLS to protect token leakage.
- Environment Secrets: Store
SECRET_KEY,SECURITY_PASSWORD_SALT, and mail credentials in a secrets manager (AWS Secrets Manager, Docker secrets, etc.). - Token Expiration: Keep the window short (15‑30 minutes) to limit exposure.
- Audit Logging: Record reset attempts and successes for compliance.
- CAPTCHA: Add a CAPTCHA challenge on the reset request form to deter bots.
Example Production Settings
# .env (do NOT commit!)
SECRET_KEY=super‑strong‑random‑bytes‑base64
SECURITY_PASSWORD_SALT=another‑random‑string
MAIL_SERVER=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=apikey
MAIL_PASSWORD=SG.xxxxxxx # SendGrid API key
MAIL_DEFAULT_SENDER=no-reply@yourdomain.com
Common Pitfalls and How to Avoid Them
- Token Reuse: Always generate a fresh token per request; never store the token in the database.
- Plain‑Text Links in Logs: Mask the reset URL when logging to avoid accidental exposure.
- Weak Password Policies: Enforce minimum length, complexity, and disallow common passwords.
- Missing CSRF Protection: Use Flask‑WTF or Django’s built‑in CSRF middleware on all forms.
- Unverified Email Addresses: Ensure
Leave a Reply