django-multifactor¶
Drop-in multi-factor authentication for Django. Ships with standalone views, opinionated defaults, and a very simple integration pathway to retrofit onto mature sites.
django-multifactor is a second layer of defence, not a passwordless system.
It sits on top of Django’s existing authentication and asks a logged-in user to
prove themselves with a second factor before sensitive views render.
Supported factors:
FIDO2 / WebAuthn — security keys, Windows Hello, Touch ID, Android SafetyNet, NFC.
TOTP — any RFC 6238 authenticator app (Google Authenticator, Authy, 1Password, Bitwarden, …).
Fallback OTP — pluggable transports (email by default, plus anything you bolt on — SMS, push, in-app messaging, carrier pigeon).
Note
U2F was removed in version 0.6. If you still depend on U2F, pin to an older release and plan a migration to FIDO2.
Who this site is for¶
This documentation is written so that a junior developer can get a working MFA
flow in their Django project in under an hour, while still giving senior
developers the architectural depth, security model, and tuning knobs they need
to deploy django-multifactor to production with confidence.
If you are brand new, start with Installation
and follow the chapters in order. If you have used django-multifactor before,
the Reference and Debugging
sections are probably what you want.
How the docs are organised¶
Section |
Audience |
What you’ll find |
|---|---|---|
Junior |
Install, configure, protect your first view. |
|
All |
How the package is wired together; full request flow diagrams. |
|
All |
Task-oriented recipes — admin integration, custom fallbacks, branding, i18n. |
|
All |
Every setting, decorator, mixin, model field and named URL. |
|
Senior |
Threat model, hardening, fallback risk, recheck tuning. |
|
Senior |
Troubleshooting recipes, logging configuration, the bundled testsite. |
|
Contributors |
Local development, the test matrix, translations, releases. |
A 10-second taste¶
# settings.py
INSTALLED_APPS = [
# ...
"django.contrib.messages",
"multifactor",
]
MULTIFACTOR = {
"FIDO_SERVER_ID": "example.com",
"FIDO_SERVER_NAME": "My Django App",
"TOKEN_ISSUER_NAME": "My Django App",
}
# urls.py
from django.urls import include, path
urlpatterns = [
path("admin/multifactor/", include("multifactor.urls")),
# ...
]
# views.py
from multifactor.decorators import multifactor_protected
@multifactor_protected(factors=1)
def billing(request): ...
That’s it — every user with one or more registered second factors will be sent
through MFA challenge before billing renders.
Table of contents¶
Getting started
Guides
Reference
Security
- Threat model
- Security best practices
- 1. HTTPS, properly
- 2. Lock down cookies
- 3. Get FIDO_SERVER_ID right
- 4. Rate-limit the MFA endpoints
- 5. Tighten RECHECK and max_age
- 6. Audit factor changes
- 7. Limit who can disable factors
- 8. Verify email before relying on it for fallback
- 9. Encourage backup factors
- 10. Watch for unexpected bypasses
- 11. Pin your dependencies
- See also
- Fallback risks
- Tuning RECHECK and max_age
- Reporting a security issue
Debugging
- Common issues
- “I get a 404 on /admin/multifactor/”
- “I get a 500 on first visit”
- “WebAuthn pops up but registration fails silently”
- “TOTP says ‘Could not validate key, please try again’ for the right code”
- “The user logs in, gets MFA, goes to the next page, gets challenged again”
- “Users see the login message every page load”
- “Advertise banner won’t go away”
- “FIDO2 registration works in Chrome, fails in Safari”
- “I disabled a key in admin but the user still gets through”
- “I bumped Django and admin URLs broke”
- “The fallback OTP arrives in email but the form rejects it”
- When the dump-everything approach is faster
- Where next?
- Logging
- FIDO2 troubleshooting
- TOTP troubleshooting
- Symptom: “Could not validate key, please try again”
- Symptom: QR code won’t scan
- Symptom: “the user added TOTP but their phone shows expired codes”
- Symptom: TOTP enrolment succeeds but auth fails immediately
- Symptom: codes match on the server’s clock but the user types something different
- Inspecting from a Django shell
- See also
- Session debugging
- Quick inspection from the shell
- Inspecting the session inside a request
- Django Debug Toolbar
- Why does my factor disappear after a few hours?
- Why is
active_factorsempty after the user just verified? - Why is
has_multifactorTruebutactive_factorsempty? - Force-clearing MFA state for a user
- Why is the session sticky across recheck even though I set RECHECK_MIN/MAX low?
- See also
- Running the bundled testsite
Contributing
Changelog