In today’s security‑first landscape, a single password is no longer enough to protect user accounts. 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 something they are. If you’re a Python developer looking to bolster the security of your web or API projects, this guide walks you through everything you need to know to set up robust MFA—from the theory behind it to hands‑on code examples using popular libraries and frameworks.
Why MFA Matters for Python Applications
Cyber‑criminals constantly evolve their tactics, and credential stuffing attacks have surged by more than 50 % in the past two years. Implementing MFA can:
- Reduce the risk of unauthorized access by requiring a second verification step.
- Boost user trust—customers feel safer knowing their data is protected.
- Help meet compliance standards such as GDPR, HIPAA, and PCI‑DSS, which often mandate MFA for privileged accounts.
- Improve overall security posture without drastically changing your existing authentication flow.
Core Concepts Behind Multi‑Factor Authentication
Types of Factors
MFA combines at least two of the following:
- Knowledge factor – something the user knows (password, PIN).
- Possession factor – something the user has (smartphone, hardware token).
- Inherence factor – something the user is (fingerprint, facial recognition).
Common MFA Methods in Python
When building a Python solution, the most widely adopted methods are:
- Time‑Based One‑Time Passwords (TOTP) – generated by apps like Google Authenticator or Authy.
- SMS/Email OTP – a code sent to the user’s phone or inbox.
- Push notifications – a prompt sent to a mobile app for approval.
- Hardware security keys – U2F or WebAuthn devices such as YubiKey.
Choosing the Right Python Library
Several mature libraries simplify MFA implementation. Below is a quick comparison to help you decide:
| Library | Supported Methods | Framework Compatibility | Documentation |
|---|---|---|---|
pyotp |
TOTP, HOTP | Flask, Django, FastAPI, any | Comprehensive, examples |
django-otp |
TOTP, YubiKey, SMS | Django only | Well‑maintained |
flask-2fa |
TOTP, Email OTP | Flask only | Simple API |
python‑webauthn |
WebAuthn/U2F | Any (requires custom integration) | Advanced, security‑focused |
For most projects, pyotp offers the perfect blend of flexibility and simplicity, especially when you need a cross‑framework solution.
Step‑by‑Step: Implementing TOTP MFA with PyOTP
1. Install the Required Packages
pip install pyotp qrcode[pil] Flask
2. Generate a Secret Key for Each User
The secret key is the shared secret between the server and the authenticator app. Store it securely (e.g., encrypted column in your database).
import pyotp
import os
def generate_secret():
# 32‑character base32 string – safe for QR code generation
return pyotp.random_base32()
user_secret = generate_secret()
# Save user_secret to the user record in DB
3. Create a QR Code for Easy Enrollment
Most users prefer scanning a QR code rather than typing the secret manually. The following Flask route renders a QR code that can be scanned by Google Authenticator, Authy, or any TOTP app.
from flask import Flask, render_template_string, request, redirect, url_for
import qrcode
import io
import base64
app = Flask(__name__)
@app.route('/mfa/setup')
def mfa_setup():
secret = user_secret # retrieve from logged‑in user record
totp_uri = pyotp.totp.TOTP(secret).provisioning_uri(name='user@example.com', issuer_name='MyApp')
img = qrcode.make(totp_uri)
buf = io.BytesIO()
img.save(buf, format='PNG')
img_b64 = base64.b64encode(buf.getvalue()).decode('utf-8')
return render_template_string('''
Scan this QR Code with your Authenticator App
After scanning, enter the 6‑digit code below to verify.
''', img_data=img_b64)
4. Verify the Token Provided by the User
When the user submits the 6‑digit code, compare it against the server‑generated TOTP value.
@app.route('/mfa/verify', methods=['POST'])
def mfa_verify():
token = request.form['token']
secret = user_secret # fetch from DB again
totp = pyotp.TOTP(secret)
if totp.verify(token):
# Mark MFA as enabled for the user
return 'MFA setup successful!'
else:
return 'Invalid code. Please try again.', 400
5. Enforce MFA on Login
Modify your login flow to check whether the user has MFA enabled. If so, prompt for the TOTP after password verification.
def login(username, password):
user = get_user(username)
if not user or not check_password(user, password):
return 'Invalid credentials', 401
if user.mfa_enabled:
# Store user ID in session temporarily and redirect to MFA page
session['pre_mfa_user_id'] = user.id
return redirect(url_for('mfa_challenge'))
else:
# Regular login without MFA
session['user_id'] = user.id
return redirect(url_for('dashboard'))
@app.route('/mfa/challenge', methods=['GET', 'POST'])
def mfa_challenge():
if request.method == 'POST':
token = request.form['token']
user = get_user_by_id(session['pre_mfa_user_id'])
if pyotp.TOTP(user.mfa_secret).verify(token):
session['user_id'] = user.id
session.pop('pre_mfa_user_id')
return redirect(url_for('dashboard'))
else:
return 'Invalid MFA code', 400
return render_template_string('''
Enter your MFA code
''')
Beyond TOTP: Adding SMS or Email OTP
If you need a fallback method for users who don’t have an authenticator app, integrate an SMS or email service. The workflow is similar—generate a random numeric code, send it via the chosen channel, and verify it within a short time window (typically 5‑10 minutes).
- SMS providers: Twilio, Nexmo, Plivo.
- Email services: SendGrid, Amazon SES, Mailgun.
- Store the OTP hash (e.g., SHA‑256) instead of plain text for extra security.
Sample Code for Email OTP
import secrets, hashlib, time
from flask_mail import Mail, Message
mail = Mail(app)
def generate_otp(length=6):
return ''.join(secrets.choice('0123456789') for _ in range(length))
def send_email_otp(user_email):
otp = generate_otp()
# Store a hash with expiration timestamp
otp_hash = hashlib.sha256(otp.encode()).hexdigest()
cache.set(f'otp:{user_email}', otp_hash, timeout=300) # 5 minutes
msg = Message('Your Login OTP', recipients=[user_email])
msg.body = f'Your one‑time code is {otp}. It expires in 5 minutes.'
mail.send(msg)
Best Practices for Secure MFA Implementation
- Never expose the secret key in URLs, logs, or client‑side code.
- Rate‑limit verification attempts to mitigate brute‑force attacks.
- Use HTTPS everywhere—MFA tokens are as sensitive as passwords.
- Provide backup codes for users who lose their device; store them hashed.
- Allow MFA reset only after strong identity verification (e.g., support ticket with ID verification).
Leave a Reply