Python Multi-Factor Authentication Mfa Setup

Written by

in

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.

  1. Install the package: pip install django-otp django-two-factor-auth
  2. Add to INSTALLED_APPS:
    INSTALLED_APPS = [
        # …
        'django_otp',
        'django_otp.plugins.otp_totp',
        'two_factor',
    ]
  3. Update URLs:
    from django.urls import path, include
    
    urlpatterns = [
        # …
        path('account/', include('two_factor.urls', 'two_factor')),
    ]
  4. Run migrations:
    python manage.py migrate
  5. 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_secret column 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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *