Introduction
Two-factor authentication (2FA) adds a second verification step beyond the password. Even if a password is stolen, the attacker cannot log in without the second factor -- typically a 6-digit code from an authenticator app.
Key Concepts
TOTP: Time-based One-Time Password, the algorithm behind authenticator apps.
django-otp: Low-level OTP framework providing device models and verification middleware.
django-two-factor-auth: High-level package built on django-otp with setup views, QR code generation, and backup codes.
@otp_required: Decorator that requires 2FA verification in addition to login.
Backup Codes: Single-use recovery codes for when the authenticator device is unavailable.
Real World Context
After a phishing attack exposed several employee passwords, a company mandates 2FA for all internal tools. Using django-two-factor-auth, the team adds TOTP-based 2FA in an afternoon, complete with QR code enrollment, backup codes, and the @otp_required decorator on sensitive views like payroll and HR data.
Deep Dive
Add TOTP (Time-based One-Time Password) authentication for enhanced security using django-two-factor-auth.
Installation
bashpip install django-two-factor-auth pip install phonenumbers # For SMS backup pip install qrcode # For QR codes
Configuration
python# settings.py INSTALLED_APPS = [ # ... 'django_otp', 'django_otp.plugins.otp_static', 'django_otp.plugins.otp_totp', 'two_factor', 'two_factor.plugins.phonenumber', # SMS backup ] MIDDLEWARE = [ # ... 'django_otp.middleware.OTPMiddleware', ] # Login URL LOGIN_URL = 'two_factor:login' LOGIN_REDIRECT_URL = 'two_factor:profile' # Two-factor settings TWO_FACTOR_CALL_GATEWAY = None # For phone calls TWO_FACTOR_SMS_GATEWAY = None # For SMS TWO_FACTOR_TOTP_DIGITS = 6
URL Configuration
python# urls.py from django.urls import path, include from two_factor.urls import urlpatterns as tf_urls urlpatterns = [ path('', include(tf_urls)), # ... ]
Protecting Views
pythonfrom django.contrib.auth.decorators import login_required from django_otp.decorators import otp_required @login_required @otp_required def sensitive_view(request): """Requires both login AND 2FA verification.""" return render(request, 'sensitive.html') # For class-based views from django.contrib.auth.mixins import LoginRequiredMixin from django_otp.decorators import otp_required from django.utils.decorators import method_decorator @method_decorator(otp_required, name='dispatch') class SensitiveView(LoginRequiredMixin, View): def get(self, request): return render(request, 'sensitive.html')
Manual 2FA Implementation
For custom implementations:
python# models.py import pyotp from django.db import models class TwoFactorProfile(models.Model): user = models.OneToOneField('auth.User', on_delete=models.CASCADE) totp_secret = models.CharField(max_length=32, blank=True) is_2fa_enabled = models.BooleanField(default=False) backup_codes = models.JSONField(default=list) def generate_secret(self): self.totp_secret = pyotp.random_base32() self.save() return self.totp_secret def get_totp_uri(self): return pyotp.totp.TOTP(self.totp_secret).provisioning_uri( name=self.user.email, issuer_name='MyApp' ) def verify_code(self, code): totp = pyotp.TOTP(self.totp_secret) return totp.verify(code) def generate_backup_codes(self, count=10): import secrets codes = [secrets.token_hex(4) for _ in range(count)] self.backup_codes = codes self.save() return codes
python# views.py import qrcode import io import base64 def setup_2fa(request): """Setup page for 2FA.""" profile = request.user.twofactorprofile if request.method == 'POST': code = request.POST.get('code') if profile.verify_code(code): profile.is_2fa_enabled = True profile.save() backup_codes = profile.generate_backup_codes() return render(request, '2fa/backup_codes.html', { 'codes': backup_codes }) else: messages.error(request, 'Invalid code') # Generate new secret profile.generate_secret() # Create QR code qr = qrcode.make(profile.get_totp_uri()) buffer = io.BytesIO() qr.save(buffer, format='PNG') qr_code = base64.b64encode(buffer.getvalue()).decode() return render(request, '2fa/setup.html', { 'qr_code': qr_code, 'secret': profile.totp_secret, }) def verify_2fa(request): """Verify 2FA code during login.""" if request.method == 'POST': code = request.POST.get('code') profile = request.user.twofactorprofile # Check TOTP code if profile.verify_code(code): request.session['2fa_verified'] = True return redirect('dashboard') # Check backup codes if code in profile.backup_codes: profile.backup_codes.remove(code) profile.save() request.session['2fa_verified'] = True return redirect('dashboard') messages.error(request, 'Invalid code') return render(request, '2fa/verify.html')
Setup Template
html<!-- templates/2fa/setup.html --> <h1>Set Up Two-Factor Authentication</h1> <p>Scan this QR code with your authenticator app:</p> <img src="data:image/png;base64,{{ qr_code }}" alt="QR Code"> <p>Or manually enter this secret: <code>{{ secret }}</code></p> <form method="post"> {% csrf_token %} <label>Enter the 6-digit code from your app:</label> <input type="text" name="code" maxlength="6" pattern="[0-9]{6}" required> <button type="submit">Verify & Enable 2FA</button> </form>
Common Pitfalls
- Not providing backup codes: If a user loses their phone and there is no recovery method, they are permanently locked out. Always generate backup codes during 2FA setup.
- Forgetting
OTPMiddleware: Withoutdjango_otp.middleware.OTPMiddleware, theotp_requireddecorator has no effect and all views are accessible without 2FA. - Not testing clock synchronization: TOTP depends on the server and client clocks being within 30 seconds of each other. If your server's clock drifts, legitimate codes are rejected.
Best Practices
- Use
OTPMiddlewarefor site-wide enforcement: Place it afterAuthenticationMiddlewareso all authenticated users must complete 2FA. - Offer TOTP over SMS: SMS is vulnerable to SIM-swapping and interception. TOTP via authenticator apps is more secure and works offline.
- Generate 10 backup codes: Display them once during setup and instruct users to store them securely. Each code should be single-use.
Summary
- Two-factor authentication adds a second verification layer beyond passwords.
- django-two-factor-auth provides enrollment views, QR codes, and backup code management.
- Protect sensitive views with the
@otp_requireddecorator. - Always provide backup codes for account recovery.
- TOTP (authenticator apps) is more secure than SMS-based 2FA.
Code Examples
import pyotp
# Generate secret for user
secret = pyotp.random_base32()
totp = pyotp.TOTP(secret)
# Generate provisioning URI for QR code
uri = totp.provisioning_uri(
name=user.email,
issuer_name='MyApp'
)
# Verify a code
is_valid = totp.verify('123456')