In today’s security‑first landscape, relying on a single password is no longer enough to protect your Python applications. Multi‑factor authentication (MFA) adds an extra layer of defense by requiring users to prove their identity with something they know, something they have, or even something they are. This guide walks you through the fundamentals of MFA, why it matters for Python developers, and provides step‑by‑step code examples for integrating TOTP‑based MFA into popular frameworks like Flask and Django.
What Is Multi‑Factor Authentication and Why It Matters
Multi‑factor authentication combines two or more independent credentials to verify a user’s identity. The most common factors are:
- Knowledge: a password or PIN.
- Possession: a smartphone app (Google Authenticator, Authy), a hardware token, or an SMS code.
- Inherence: biometric data such as fingerprints or facial recognition.
Implementing MFA dramatically reduces the risk of credential stuffing, phishing, and brute‑force attacks. According to a Verizon Data Breach Investigation Report, breaches involving compromised passwords drop by up to 99% when MFA is enforced.
Choosing the Right MFA Method for Python Projects
Python developers have several options when it comes to MFA. The most widely adopted approach for web applications is Time‑Based One‑Time Passwords (TOTP), which are generated by standard authenticator apps and follow the RFC 6238 specification. TOTP is:
- Open‑source and free.
- Supported across Android, iOS, and desktop platforms.
- Easy to implement with libraries like
pyotpanddjango-otp.
If you need a higher assurance level, consider integrating FIDO2/WebAuthn for hardware keys, or using third‑party services such as Auth0, Okta, or Duo Security. This article focuses on TOTP because it strikes a balance between security, user experience, and implementation effort.
Setting Up a Basic TOTP MFA Flow in Python
Prerequisites
- Python 3.8+ installed.
- A virtual environment (recommended).
- Basic familiarity with Flask or Django.
- Install required packages:
pip install pyotp qrcode[pil] pillow
Generating a Secret and QR Code
The secret key is a base‑32 string stored securely for each user. It can be displayed as a QR code that authenticator apps scan.
import pyotp
import qrcode
from io import BytesIO
import base64
def generate_mfa_secret(username: str) -> str:
# Create a unique secret per user
secret = pyotp.random_base32()
# Store `secret` in your user database (hashed or encrypted)
return secret
def get_qr_code_uri(secret: str, username: str, issuer: str = "MyApp"):
# Build the provisioning URI (compatible with Google Authenticator)
totp = pyotp.TOTP(secret)
return totp.provisioning_uri(name=username, issuer_name=issuer)
def qr_code_image(uri: str) -> str:
# Generate PNG image and encode as base64 for embedding in HTML
img = qrcode.make(uri)
buffer = BytesIO()
img.save(buffer, format="PNG")
return "data:image/png;base64," + base64.b64encode(buffer.getvalue()).decode()
Verifying a User‑Submitted Token
When a user logs in and provides the 6‑digit code from their authenticator app, you validate it against the stored secret.
def verify_mfa_token(secret: str, token: str) -> bool:
totp = pyotp.TOTP(secret)
# `verify` accepts a window of ±1 step (30 seconds) to tolerate clock drift
return totp.verify(token, valid_window=1)
Integrating MFA into a Flask Application
Project Structure
.
├─ app.py
├─ models.py # simple user model with `mfa_secret`
├─ templates/
│ ├─ login.html
│ ├─ mfa_setup.html
│ └─ mfa_verify.html
└─ requirements.txt
Key Flask Routes
from flask import Flask, render_template, request, redirect, session, url_for, flash
from models import User, db_session
import pyotp, qrcode, base64, io
app = Flask(__name__)
app.secret_key = "super‑secret‑key"
@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 user.check_password(request.form["password"]):
session["user_id"] = user.id
if user.mfa_secret:
return redirect(url_for("mfa_verify"))
else:
return redirect(url_for("mfa_setup"))
flash("Invalid credentials")
return render_template("login.html")
@app.route("/mfa/setup", methods=["GET", "POST"])
def mfa_setup():
user = User.query.get(session["user_id"])
if request.method == "POST":
token = request.form["token"]
if verify_mfa_token(user.mfa_secret, token):
flash("MFA enabled successfully")
return redirect(url_for("dashboard"))
flash("Invalid token")
else:
# First time: generate secret and QR code
if not user.mfa_secret:
user.mfa_secret = generate_mfa_secret(user.username)
db_session.commit()
uri = get_qr_code_uri(user.mfa_secret, user.username)
qr_data = qr_code_image(uri)
return render_template("mfa_setup.html", qr_data=qr_data)
@app.route("/mfa/verify", methods=["GET", "POST"])
def mfa_verify():
user = User.query.get(session["user_id"])
if request.method == "POST":
token = request.form["token"]
if verify_mfa_token(user.mfa_secret, token):
session["mfa_verified"] = True
return redirect(url_for("dashboard"))
flash("Invalid token")
return render_template("mfa_verify.html")
Template Snippet for QR Code Display
<!-- mfa_setup.html -->
<h2>Set Up Multi‑Factor Authentication</h2>
<p>Scan the QR code below with Google Authenticator, Authy, or any TOTP app.</p>
<img src="{{ qr_data }}" alt="MFA QR Code" />
<form method="post">
<label for="token">Enter the 6‑digit code:</label>
<input type="text" name="token" id="token" required />
<button type="submit">Verify</button>
</form>
Adding MFA to a Django Project
Leverage django-otp and django-two-factor-auth
Both packages provide ready‑made views, models, and middleware. Install them with:
pip install django-otp django-two-factor-auth
Configuration Steps
- Add the apps to
INSTALLED_APPS:INSTALLED_APPS = [ ..., 'django_otp', 'django_otp.plugins.otp_totp', 'two_factor', 'two_factor.plugins.phonenumber', ] - Insert the OTP middleware before authentication middleware:
MIDDLEWARE = [ 'django_otp.middleware.OTPMiddleware', ..., 'django.contrib.auth.middleware.AuthenticationMiddleware', ..., ] - Update
urls.pyto include the two‑factor URLs:from django.urls import path, include urlpatterns = [ path('', include('two_factor.urls', 'two_factor')), # other app URLs ] - Run migrations:
python manage.py migrate
Customizing the Login Flow
If you already have a bespoke login view, you can wrap it with LoginView from two_factor.views:
from two_factor.views import LoginView
class MyLoginView(LoginView):
template_name = 'myapp/login.html'
# You can override form_class, success_url, etc.
Best Practices for Secure MFA Implementation
- Store secrets encrypted. Never keep the TOTP secret in plain text; use a server‑side encryption key or a vault service.
- Enforce backup codes. Provide users with a set of one‑time use backup codes for situations where their authenticator device is unavailable.
- Rate‑limit verification attempts. Limit failed MFA attempts to mitigate brute‑force attacks.
- Use HTTPS everywhere. MFA tokens are sensitive; TLS protects them from eavesdropping.
- Educate users. Explain why MFA is required and give clear instructions for setup.
- Log MFA events. Record successful and failed MFA challenges for audit trails.
Testing Your MFA Setup
Automated tests should cover both happy‑path and edge cases. Below is a pytest example
Leave a Reply