Welcome to this step‑by‑step Python password reset via email tutorial. Whether you’re building a small Flask app or a full‑featured Django site, offering a secure, user‑friendly password reset flow is essential for both user satisfaction and security compliance. In this guide you’ll learn how to generate a one‑time token, send a reset link through email, and safely update the user’s password—all with clean, reusable Python code.
Why a Password‑Reset Feature Matters
Users forget passwords, and a robust password reset via email is the most common recovery method. Implementing it correctly protects against:
- Credential stuffing attacks.
- Unauthorized account access.
- Compliance violations (GDPR, CCPA, etc.).
By following this tutorial you’ll avoid common pitfalls such as plain‑text token storage, insecure link handling, and email spoofing.
Prerequisites
Before diving into code, make sure you have the following ready:
- Python 3.9+ installed.
- A virtual environment (recommended
venvorconda). - Basic knowledge of Flask or Django (the tutorial covers both).
- Access to an SMTP server (Gmail, SendGrid, Mailgun, etc.).
Project Setup
1. Create a new virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
2. Install required packages
We’ll use Flask for the lightweight example and itsdangerous for token generation. For Django, the same token library works out of the box.
pip install Flask itsdangerous Flask-Mail
# For Django users:
pip install Django
Configuring the Email Backend
Both Flask and Django need SMTP credentials. Store them securely in environment variables to keep them out of source control.
# .env (example)
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your.email@gmail.com
MAIL_PASSWORD=your_app_password
SECRET_KEY=super‑secret‑key‑for‑tokens
Load these variables in your app using python-dotenv or the OS module.
Generating a Secure Token
The core of a Python password reset via email tutorial is a time‑limited token that can be verified later. itsdangerous.URLSafeTimedSerializer provides exactly that.
from itsdangerous import URLSafeTimedSerializer
import os
def get_serializer():
secret_key = os.getenv('SECRET_KEY')
return URLSafeTimedSerializer(secret_key)
def generate_reset_token(email):
s = get_serializer()
return s.dumps(email, salt='password-reset-salt')
def verify_reset_token(token, max_age=3600):
s = get_serializer()
try:
email = s.loads(token, salt='password-reset-salt', max_age=max_age)
except Exception:
return None
return email
Flask Implementation
Application Skeleton
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_mail import Mail, Message
import os
app = Flask(__name__)
app.config.update(
SECRET_KEY=os.getenv('SECRET_KEY'),
MAIL_SERVER=os.getenv('MAIL_SERVER'),
MAIL_PORT=int(os.getenv('MAIL_PORT')),
MAIL_USE_TLS=os.getenv('MAIL_USE_TLS') == 'True',
MAIL_USERNAME=os.getenv('MAIL_USERNAME'),
MAIL_PASSWORD=os.getenv('MAIL_PASSWORD')
)
mail = Mail(app)
Reset Request View
This endpoint accepts an email address, creates a token, and sends the reset link.
@app.route('/reset-password', methods=['GET', 'POST'])
def reset_request():
if request.method == 'POST':
email = request.form['email']
# TODO: Verify email exists in your DB
token = generate_reset_token(email)
reset_url = url_for('reset_token', token=token, _external=True)
msg = Message('Password Reset Request',
sender=app.config['MAIL_USERNAME'],
recipients=[email])
msg.body = f'''Hi,
We received a request to reset your password. Click the link below to choose a new password:
{reset_url}
If you didn’t request this, simply ignore this email.
'''
mail.send(msg)
flash('A password reset link has been sent to your email.', 'info')
return redirect(url_for('login'))
return render_template('reset_request.html')
Reset Token View
The user lands on this page after clicking the link. We verify the token and allow a new password to be set.
@app.route('/reset-password/<token>', 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':
new_password = request.form['password']
# TODO: Hash the password and update the user record
flash('Your password has been updated!', 'success')
return redirect(url_for('login'))
return render_template('reset_token.html', token=token)
Django Implementation
If you prefer Django, the built‑in PasswordResetView handles most of the heavy lifting. Below is a minimal setup that still follows the same token logic.
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('reset/', views.PasswordResetRequestView.as_view(), name='password_reset'),
path('reset///', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
]
views.py
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.models import User
from django.core.mail import send_mail
from django.shortcuts import render, redirect
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from django.views import View
import os
class PasswordResetRequestView(View):
def get(self, request):
return render(request, 'password_reset_form.html')
def post(self, request):
email = request.POST['email']
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
# For security, don’t reveal if the email exists
pass
else:
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
reset_link = request.build_absolute_uri(
f'/reset/{uid}/{token}/')
send_mail(
subject='Password Reset',
message=f'Click the link to reset your password: {reset_link}',
from_email=os.getenv('MAIL_USERNAME'),
recipient_list=[email],
)
return redirect('login')
Confirm View
class PasswordResetConfirmView(View):
def get(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (User.DoesNotExist, ValueError, TypeError):
user = None
if user and default_token_generator.check_token(user, token):
return render(request, 'password_reset_confirm.html', {'validlink': True, 'uidb64': uidb64, 'token': token})
else:
return render(request, 'password_reset_confirm.html', {'validlink': False})
def post(self, request, uidb64, token):
# Same validation as GET
# Update password
new_password = request.POST['password']
user.set_password(new_password)
user.save()
return redirect('login')
Security Best Practices
- Never store plain‑text passwords. Always hash with
bcryptorargon2. - Limit token lifetime (commonly 1 hour) using the
max_ageargument. - Rate‑limit reset requests to prevent enumeration attacks.
- Use HTTPS for every endpoint that handles tokens or passwords.
- Include a short, random nonce in the email body to thwart replay attacks.
Testing the Flow
Before deploying, run through these checks:
- Submit a valid email – verify you receive a correctly formatted reset link.
- Click the link after the token expires – ensure the user sees an expiration message.
- Attempt to reuse a token – it should be invalid after the first successful password change.
- Submit a weak password – enforce your password policy and return clear error messages.
Deploying to Production
When you push your code live, remember to:
- Set
MAIL_USE_TLS=TrueorMAIL_USE_SSL=Truedepending on your provider. - Configure a dedicated email domain (e.g.,
no-reply@yourdomain.com) to improve deliverability. - Enable SPF, DKIM, and DMARC records for your sending domain.
- Monitor bounce rates and implement a fallback email service if needed.
Conclusion
Implementing a secure Python password reset via email tutorial doesn’t have to be daunting. By leveraging itsdangerous for token handling, configuring a reliable SMTP backend, and
Leave a Reply