TOTP (Time-based One-Time Password) authentication allows users to generate temporary codes using authenticator apps like Google Authenticator, Authy, 1Password, or Microsoft Authenticator.
How It Works
TOTP generates six-digit codes that change every 30 seconds based on:
- A shared secret key stored in your database
- The current time
- The HMAC-SHA1 algorithm
When a user sets up TOTP:
- A random secret is generated
- The secret is encoded as a QR code
- User scans the QR code with their authenticator app
- User enters a code from their app to verify setup
- The secret is encrypted and stored in the database
Configuration
TOTP is enabled by default. Configure it in settings.py:
# TOTP is included by default
MFA_SUPPORTED_TYPES = ["totp", "recovery_codes"]
# Customize TOTP settings
MFA_TOTP_PERIOD = 30 # Code validity period in seconds
MFA_TOTP_DIGITS = 6 # Number of digits in the code
MFA_TOTP_ISSUER = "My App" # Appears in authenticator apps
MFA_TOTP_TOLERANCE = 1 # Accept codes from ±1 time period
Settings Reference
MFA_TOTP_PERIOD
Default: 30
The number of seconds a TOTP code remains valid.
MFA_TOTP_PERIOD = 30 # Standard 30-second window
MFA_TOTP_DIGITS
Default: 6
The number of digits in generated codes. Most authenticator apps support 6 or 8 digits.
MFA_TOTP_DIGITS = 6 # Standard 6-digit codes
MFA_TOTP_ISSUER
Default: "" (uses site name)
The issuer name displayed in authenticator apps. Helps users identify which account the code is for.
MFA_TOTP_ISSUER = "MyCompany Production"
MFA_TOTP_TOLERANCE
Default: 0
Number of time periods (past and future) to accept. Helps with clock drift between server and client.
MFA_TOTP_TOLERANCE = 1 # Accept previous and next code
Higher tolerance values are less secure but more tolerant of clock drift. Use 0 for maximum security or 1 for better user experience.
URL Endpoints
TOTP URLs are available at:
/accounts/mfa/totp/activate/ - Activate TOTP authentication
/accounts/mfa/totp/deactivate/ - Deactivate TOTP authentication
Activation Flow
Users can activate TOTP through the ActivateTOTPView:
from allauth.mfa.totp.views import ActivateTOTPView
# The view handles:
# 1. Generating a secret
# 2. Creating a QR code
# 3. Validating the user's first code
# 4. Storing the encrypted secret
Template Context
The activation template receives:
{
'form': ActivateTOTPForm,
'totp_url': 'otpauth://totp/...', # The TOTP URI
'totp_svg': '<svg>...</svg>', # QR code as SVG
'totp_svg_data_uri': 'data:image/svg+xml;base64,...' # Data URI
}
Custom Template Example
templates/mfa/totp/activate_form.html
{% extends "account/base.html" %}
{% block content %}
<h1>Set Up Two-Factor Authentication</h1>
<p>Scan this QR code with your authenticator app:</p>
<div class="qr-code">
<img src="{{ totp_svg_data_uri }}" alt="TOTP QR Code" />
</div>
<p>Or manually enter this key: <code>{{ form.secret }}</code></p>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Verify and Activate</button>
</form>
{% endblock %}
Programmatic Usage
Generate and Validate Codes
from allauth.mfa.totp.internal.auth import (
generate_totp_secret,
validate_totp_code,
hotp_value,
format_hotp_value
)
# Generate a new secret
secret = generate_totp_secret(length=20)
# Returns base32-encoded string like: 'JBSWY3DPEHPK3PXP'
# Validate a user-provided code
code = "123456"
is_valid = validate_totp_code(secret, code)
# Generate the current code (for testing)
import time
current_time = int(time.time())
counter = current_time // 30 # Using default TOTP_PERIOD
value = hotp_value(secret, counter)
current_code = format_hotp_value(value)
Activate TOTP for a User
from allauth.mfa.totp.internal.auth import TOTP
from allauth.mfa.models import Authenticator
# Activate TOTP for a user
user = request.user
secret = generate_totp_secret()
# Verify user has entered correct code
user_code = request.POST.get('code')
if validate_totp_code(secret, user_code):
totp = TOTP.activate(user, secret)
# TOTP is now active for this user
Check if User Has TOTP Enabled
from allauth.mfa.models import Authenticator
has_totp = Authenticator.objects.filter(
user=user,
type=Authenticator.Type.TOTP
).exists()
Validate TOTP During Login
from allauth.mfa.models import Authenticator
# Get user's TOTP authenticator
authenticator = Authenticator.objects.get(
user=user,
type=Authenticator.Type.TOTP
)
# Validate code
totp = authenticator.wrap()
if totp.validate_code(user_code):
# Code is valid
authenticator.record_usage()
Override the default forms in settings.py:
MFA_FORMS = {
'activate_totp': 'myapp.forms.CustomActivateTOTPForm',
'deactivate_totp': 'myapp.forms.CustomDeactivateTOTPForm',
}
from allauth.mfa.totp.forms import ActivateTOTPForm
class CustomActivateTOTPForm(ActivateTOTPForm):
def clean_code(self):
code = super().clean_code()
# Add custom validation
return code
Customizing the TOTP URL
Override the adapter to customize QR code generation:
from allauth.mfa.adapter import DefaultMFAAdapter
class CustomMFAAdapter(DefaultMFAAdapter):
def get_totp_label(self, user):
# Customize the label shown in authenticator apps
return f"{user.email} - Production"
def get_totp_issuer(self):
# Customize the issuer name
return "MyCompany Inc."
def build_totp_url(self, user, secret):
# Fully customize the otpauth:// URL
url = super().build_totp_url(user, secret)
# Modify as needed
return url
MFA_ADAPTER = 'myapp.adapter.CustomMFAAdapter'
Security Considerations
Secret Storage
TOTP secrets are stored encrypted in the database:
from allauth.mfa.utils import encrypt, decrypt
# Secrets are automatically encrypted
encrypted_secret = encrypt(secret)
# And decrypted when needed
original_secret = decrypt(encrypted_secret)
To add custom encryption, override the adapter:
from allauth.mfa.adapter import DefaultMFAAdapter
from cryptography.fernet import Fernet
class EncryptedMFAAdapter(DefaultMFAAdapter):
def encrypt(self, text):
# Use your encryption method
cipher = Fernet(settings.MFA_ENCRYPTION_KEY)
return cipher.encrypt(text.encode()).decode()
def decrypt(self, encrypted_text):
cipher = Fernet(settings.MFA_ENCRYPTION_KEY)
return cipher.decrypt(encrypted_text.encode()).decode()
Code Reuse Prevention
TOTP codes are cached after use to prevent replay attacks:
# From totp/internal/auth.py
def validate_code(self, code):
if self._is_code_used(code):
return False # Code already used
if validate_totp_code(secret, code):
self._mark_code_used(code) # Cache for TOTP_PERIOD seconds
return True
return False
Development & Testing
Bypass Code for Testing
Only use this in development environments with DEBUG = True.
# Only works when DEBUG = True
MFA_TOTP_INSECURE_BYPASS_CODE = "123456"
This allows you to use 123456 as a valid code during testing, regardless of the actual TOTP value.
Recovery Codes
When users activate TOTP, recovery codes are automatically generated. This ensures users can access their account if they lose their authenticator device.
Learn more about recovery codes →
Common Issues
Clock Drift
If users report codes not working:
# Increase tolerance to accept codes from adjacent time periods
MFA_TOTP_TOLERANCE = 1 # Accept ±30 seconds
QR Code Not Displaying
Ensure qrcode is installed:
pip install "django-allauth[mfa]"
Codes Not Accepted
Check that django.contrib.messages is installed and configured:
INSTALLED_APPS = [
'django.contrib.messages',
# ...
]
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# ...
]