In today’s security‑first landscape, Python multi-factor authentication (MFA) setup is no longer a nice‑to‑have feature—it’s a critical requirement for protecting user data and preventing unauthorized access. Whether you’re building a Flask API, a Django web app, or a simple script, adding MFA can dramatically reduce the risk of credential‑stuffing attacks and boost user confidence. In this guide we’ll walk through the concepts, choose the right Python libraries, and provide a step‑by‑step, code‑first tutorial that you can copy‑paste into your own project.
Why Multi‑Factor Authentication Matters for Python Applications
Traditional username‑and‑password authentication relies on a single secret that can be compromised through phishing, data breaches, or weak password choices. MFA adds one or more independent verification steps, typically something you have (a mobile device or hardware token) or something you are (biometrics). The benefits are clear:
- Reduced attack surface: Even if a password is leaked, an attacker still needs the second factor.
- Regulatory compliance: Standards such as GDPR, HIPAA, and PCI‑DSS often require MFA for sensitive operations.
- Improved user trust: Users feel safer knowing their accounts are protected by more than just a password.
Choosing the Right Python MFA Library
Python’s ecosystem offers several mature libraries for implementing MFA. Below is a quick comparison to help you decide which tool fits your stack:
- PyOTP – Simple Time‑Based One‑Time Password (TOTP) and HMAC‑Based OTP (HOTP) generation. Ideal for integrating Google Authenticator or Authy.
- python-fido2 – Full support for FIDO2/WebAuthn hardware keys (YubiKey, SoloKey). Great for password‑less flows.
- django-otp – Plug‑and‑play OTP support for Django projects, with built‑in models and views.
- Flask‑User / Flask‑Security‑Too – Extensions that add MFA capabilities on top of Flask authentication.
Step‑by‑Step MFA Setup Using PyOTP and Flask
Prerequisites
- Python 3.9+ installed
- Virtual environment (venv) activated
- Flask 2.x installed (`pip install flask`)
- PyOTP installed (`pip install pyotp`)
1. Generate and Store a Secret Key
The secret key is the shared secret between the server and the user’s authenticator app. It must be stored securely—typically encrypted in the database.
import pyotp
import os
import base64
def generate_secret():
# 32‑byte base32 secret, compatible with Google Authenticator
return pyotp.random_base32()
# Example usage
user_secret = generate_secret()
print(f"User secret (store securely): {user_secret}")
2. Create a QR Code for Easy Enrollment
Most users prefer scanning a QR code rather than typing the secret manually. The qrcode library works well with Flask.
import qrcode
from io import BytesIO
import base64
def get_qr_code_uri(username, issuer, secret):
# Build the otpauth URI
return pyotp.totp.TOTP(secret).provisioning_uri(name=username, issuer_name=issuer)
def generate_qr_image(uri):
img = qrcode.make(uri)
buffer = BytesIO()
img.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode()
# Example
uri = get_qr_code_uri("alice@example.com", "MyFlaskApp", user_secret)
qr_base64 = generate_qr_image(uri)
print(f"Data URI for HTML: data:image/png;base64,{qr_base64}")
Render the qr_base64 string in an <img> tag on the enrollment page.
3. Verify the OTP Provided by the User
When the user logs in, prompt for the 6‑digit code generated by their authenticator app and validate it against the stored secret.
def verify_otp(secret, token):
totp = pyotp.TOTP(secret)
return totp.verify(token, valid_window=1) # allow +/- 30 seconds
# Example verification
user_input = "123456"
if verify_otp(user_secret, user_input):
print("MFA success!")
else:
print("Invalid code.")
4. Integrate MFA Into Flask Login Flow
Below is a minimal Flask route that demonstrates a two‑step login: first password, then OTP.
from flask import Flask, request, session, redirect, url_for, render_template_string
app = Flask(__name__)
app.secret_key = os.urandom(24) # Replace with a stable secret in production
# Mock user database
users = {
"alice": {"password": "s3cr3t", "mfa_secret": user_secret}
}
login_template = """
<form method="post">
Username: <input name="username"><br>
Password: <input type="password" name="password"><br>
<input type="submit" value="Login">
</form>
"""
otp_template = """
<p>Enter the 6‑digit code from your authenticator app:</p>
<form method="post">
<input name="otp">
<input type="submit" value="Verify">
</form>
"""
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
user = users.get(username)
if user and user["password"] == password:
session["pre_mfa_user"] = username
return redirect(url_for("mfa"))
else:
return "Invalid credentials", 401
return render_template_string(login_template)
@app.route("/mfa", methods=["GET", "POST"])
def mfa():
username = session.get("pre_mfa_user")
if not username:
return redirect(url_for("login"))
if request.method == "POST":
otp = request.form["otp"]
secret = users[username]["mfa_secret"]
if verify_otp(secret, otp):
session["user"] = username
session.pop("pre_mfa_user", None)
return f"Welcome, {username}! MFA passed."
else:
return "Invalid OTP", 401
return render_template_string(otp_template)
if __name__ == "__main__":
app.run(debug=True)
Beyond TOTP: Adding Hardware Token Support with python‑fido2
While TOTP works for most use‑cases, some organizations demand stronger, phishing‑resistant authentication. The python-fido2 library enables WebAuthn flows that use hardware security keys or platform authenticators (Windows Hello, Touch ID).
- Register a credential: Server generates a challenge, the client creates a new public‑key credential, and the server stores the public key.
- Authenticate: Server sends a challenge, the client signs it with the private key, and the server verifies the signature.
Implementation details are beyond the scope of this article, but the official python‑fido2 GitHub repo provides a full Flask example that can be combined with the TOTP flow for a hybrid MFA strategy.
Storing Secrets Securely
Never store MFA secrets in plain text. Follow these best practices:
- Encrypt at rest: Use a library like
cryptographyto encrypt the secret before writing to the database. - Use a dedicated column: Keep MFA data separate from password hashes to simplify key rotation.
- Rotate secrets periodically: Offer users the ability to reset their MFA device, generating a new secret.
Backup Codes and Recovery Options
Users can lose access to their authenticator app. Provide a set of one‑time backup codes that can be regenerated from the account settings page. Example generation:
import secrets
def generate_backup_codes(count=10):
return [secrets.token_hex(4) for _ in range(count)]
print(generate_backup_codes())
Store these codes hashed (e.g., using bcrypt) and mark them as used after the first successful login.
Testing Your MFA Implementation
Automated testing ensures the flow works across browsers and devices. Consider the following strategies:
- Unit tests: Verify
verify_otpworks with known secrets and timestamps. - Integration tests: Use
seleniumorPlaywrightto simulate a user scanning a QR code and entering the generated token. - Security audits: Run static analysis tools (Bandit, SonarQube) to catch hard‑coded secrets.
Common Pitfalls and How to Avoid Them
- Clock drift: T
Leave a Reply