In today’s security‑first world, relying on just a password is no longer enough to protect your applications. Implementing multi‑factor authentication (MFA) in Python not only raises the bar against credential stuffing and phishing attacks, but also builds trust with users who expect modern, robust protection. This guide walks you through the concepts, tools, and step‑by‑step code you need to set up MFA—whether you’re building a Django web app, a Flask API, or a simple command‑line utility.
Why MFA Matters for Python Applications
Multi‑factor authentication adds at least one additional verification layer beyond the traditional username and password. The most common factors are:
- Something you know – a password or PIN.
- Something you have – a time‑based one‑time password (TOTP) generated by an authenticator app, a hardware token, or an SMS code.
- Something you are – biometric data such as fingerprint or facial recognition.
By requiring two or more of these factors, you dramatically reduce the attack surface. For Python developers, integrating MFA can be done with a handful of well‑maintained libraries, making it both secure and maintainable.
Core Components of a Python MFA System
1. Secret Key Generation
Every user needs a unique secret key that will be shared between the server and the authenticator app. The secret is usually a base‑32 string, compliant with the HOTP and TOTP standards.
2. QR Code Delivery
To make enrollment user‑friendly, you generate a QR code that encodes the secret key along with the account name and issuer. Scanning the QR code with Google Authenticator, Authy, or any TOTP app adds the token automatically.
3. Verification Logic
When a user logs in, you validate the TOTP code they provide against the secret stored on the server. A small time window (usually ±30 seconds) is allowed to accommodate clock drift.
4. Backup & Recovery Options
Because users can lose access to their authenticator device, it’s best practice to offer backup codes, email OTP, or SMS fallback. This ensures a smooth user experience without compromising security.
Choosing the Right Python Libraries
The Python ecosystem offers several mature packages for MFA. Below is a quick comparison:
- PyOTP – Implements HOTP/TOTP according to RFC 4226/6238. Simple, well‑documented, and works with any framework.
- qrcode – Generates QR codes in PNG or SVG format, perfect for embedding in HTML emails or web pages.
- django-otp – A Django‑specific solution that integrates with the authentication middleware and supports multiple OTP devices.
- Flask-User – Extends Flask‑Login with optional two‑factor support.
- python-jose – If you prefer JWT‑based MFA, this library helps sign and verify tokens.
Step‑by‑Step MFA Setup in a Flask Application
Prerequisites
- Python 3.9+ installed.
- Flask, PyOTP, and qrcode libraries (`pip install flask pyotp qrcode[pil]`).
- A basic Flask app with user registration and login already in place.
1. Extend the User Model
# models.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
# New fields for MFA
mfa_enabled = db.Column(db.Boolean, default=False)
mfa_secret = db.Column(db.String(32)) # Base‑32 secret
# Optional: store backup codes as a JSON list
backup_codes = db.Column(db.Text) # JSON‑encoded list
2. Generate a Secret and QR Code During Enrollment
# mfa.py
import pyotp
import qrcode
import io
import base64
def generate_mfa_secret():
# 16‑character base32 secret (80‑bit security)
return pyotp.random_base32()
def get_qr_code_uri(username, secret, issuer_name="MyFlaskApp"):
totp = pyotp.TOTP(secret)
return totp.provisioning_uri(name=username, issuer_name=issuer_name)
def create_qr_image(uri):
img = qrcode.make(uri)
buffer = io.BytesIO()
img.save(buffer, format="PNG")
base64_img = base64.b64encode(buffer.getvalue()).decode()
return f"data:image/png;base64,{base64_img}"
3. Enrollment Route
@app.route('/enroll-mfa', methods=['GET', 'POST'])
@login_required
def enroll_mfa():
if request.method == 'POST':
# User confirms they have scanned the QR code
token = request.form.get('token')
if pyotp.TOTP(current_user.mfa_secret).verify(token):
current_user.mfa_enabled = True
db.session.commit()
flash('MFA enabled successfully!', 'success')
return redirect(url_for('dashboard'))
flash('Invalid token, try again.', 'danger')
else:
# First visit – generate secret & QR
if not current_user.mfa_secret:
current_user.mfa_secret = generate_mfa_secret()
db.session.commit()
uri = get_qr_code_uri(current_user.username, current_user.mfa_secret)
qr_data = create_qr_image(uri)
return render_template('enroll_mfa.html', qr_data=qr_data)
4. Login Flow with MFA Challenge
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user = User.query.filter_by(username=request.form['username']).first()
if user and check_password_hash(user.password_hash, request.form['password']):
if user.mfa_enabled:
session['pre_mfa_user_id'] = user.id
return redirect(url_for('mfa_challenge'))
login_user(user)
return redirect(url_for('dashboard'))
flash('Invalid credentials', 'danger')
return render_template('login.html')
@app.route('/mfa-challenge', methods=['GET', 'POST'])
def mfa_challenge():
user_id = session.get('pre_mfa_user_id')
if not user_id:
return redirect(url_for('login'))
user = User.query.get(user_id)
if request.method == 'POST':
token = request.form.get('token')
if pyotp.TOTP(user.mfa_secret).verify(token):
login_user(user)
session.pop('pre_mfa_user_id')
return redirect(url_for('dashboard'))
flash('Invalid MFA code', 'danger')
return render_template('mfa_challenge.html')
5. Generating Backup Codes (Optional)
import secrets
import json
def generate_backup_codes(count=10):
codes = [secrets.token_hex(4) for _ in range(count)]
return json.dumps(codes)
# Store once during enrollment
current_user.backup_codes = generate_backup_codes()
db.session.commit()
Implementing MFA in Django with django‑otp
If you’re using Django, the django‑otp package provides a plug‑and‑play solution that works with the default authentication system.
- Install the package:
pip install django-otp django-two-factor-auth - Add to INSTALLED_APPS:
INSTALLED_APPS = [ # … 'django_otp', 'django_otp.plugins.otp_totp', 'two_factor', ] - Update URLs:
from django.urls import path, include urlpatterns = [ # … path('account/', include('two_factor.urls', 'two_factor')), ] - Run migrations:
python manage.py migrate - Enable MFA per user via the admin panel or programmatically:
from django_otp.plugins.otp_totp.models import TOTPDevice device = TOTPDevice.objects.create(user=user, name='default') device.save()
django‑otp automatically handles token verification, QR code generation, and backup token storage, letting you focus on the user experience.
Best Practices for Secure MFA Implementation
- Never store plain‑text secrets. Encrypt the
mfa_secretcolumn using a server‑side key or a dedicated secrets manager. - Limit verification attempts. Block the account or introduce exponential back‑off after 5–10 failed MFA attempts.
- Use HTTPS everywhere. MFA codes are time‑sensitive; exposing them over plain HTTP defeats the purpose.
- Provide clear recovery flows. Offer backup codes, email OTP, or SMS as secondary methods, but treat them with the same security level as primary tokens.
- Log MFA events. Record successful and failed MFA challenges with timestamps, IP addresses, and user agents for audit trails
Leave a Reply