Getting started
Requirements
Python |
3.10, 3.11, 3.12, 3.13 |
Django |
4.2, 5.2, 6.1 |
Django 6.1 needs Python 3.12 or newer — that is Django’s own requirement, not django-mfa’s. On Python 3.10 or 3.11, use Django 4.2 or 5.2 (both LTS).
Any database Django supports works: factor state lives in a JSONField, and no
backend-specific features are used.
Upgrading from an older release? See Upgrading from django-mfa 2.x/3.x first — several changes are breaking, and one migration is irreversible.
Installation
pip install django-mfa # or: uv add django-mfa
Wiring it up
1. Add the app and the middleware
MfaMiddleware must come after AuthenticationMiddleware — it needs
request.user to exist.
INSTALLED_APPS = [
...,
"django_mfa",
]
MIDDLEWARE = [
...,
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_mfa.middleware.MfaMiddleware",
]
2. Run migrations
python manage.py migrate
3. Mount the URLs
urlpatterns = [
...,
path("mfa/", include("django_mfa.urls")),
]
django_mfa.urls bakes the mfa namespace into the pattern list itself — do not
pass namespace= to include(). Every reverse() call inside django-mfa uses the
mfa: prefix, so the URL prefix you mount it under (mfa/ above) is entirely
yours to choose.
4. Exempt your logout URL
django-mfa has no way to discover it, and a user who cannot complete their second factor needs a way out:
MFA_EXEMPT_PATHS = ["/logout/"]
Skip this and a user with a lost device is stuck in a redirect loop they cannot even log out of. See the warning in Settings reference.
5. For security keys and passkeys
Only needed if you want WebAuthn. To offer authenticator apps alone, set
MFA_FACTORS = ["totp", "recovery_codes"] and skip this step entirely — the
WebAuthn system checks switch themselves off with it.
MFA_FIDO2_RP_ID = "example.com" # your registrable domain
AUTHENTICATION_BACKENDS = [
"django_mfa.backends.WebAuthnBackend",
"django.contrib.auth.backends.ModelBackend",
]
Warning
Set MFA_FIDO2_RP_ID once and never change it. Changing it invalidates every
security key and passkey already registered, with no migration path. See
Settings reference for the full explanation.
Both of these are verified by manage.py check (and therefore by migrate and
runserver) — see System checks. If you got either
wrong, you will hear about it at startup rather than from a user.
6. Name yourself in authenticator apps
Optional, but users with more than one account will thank you. Without it their app shows a bare username with no clue which site it belongs to.
MFA_ISSUER_NAME = "Cool Django App"
Check that it works
That’s the whole setup. Your login view does not change — django-mfa hooks Django’s
own user_logged_in signal, so whatever authenticates your users today keeps doing
so.
python manage.py check # should report no issues
Then, logged in as any user, visit /mfa/security/. You should see a page listing
enabled methods (none yet), methods available to add, and recovery codes remaining.
Enroll one, log out, and log back in: you’ll be challenged for it.
Users with no factor enrolled are never challenged and never blocked, so you can turn this on for a live site and let people opt in at their own pace.
Where to go next
Flow and URLs — the URLs this exposes and what happens at each step
Customizing the UI — make the screens look like the rest of your site
Integration recipes — allauth, custom login views, testing, and troubleshooting
Settings reference — every setting, its default, and what it does
Security model — what django-mfa defends against, and your pre-launch checklist