Threat model¶
django-multifactor is a second layer of defence. It assumes you already
have a working primary authentication system (passwords, SSO, whatever) and
adds friction for attackers who have compromised primary credentials. This
page lays out what the package protects against and — more importantly —
what it does not.
What MFA defends against¶
Threat |
How MFA helps |
|---|---|
Password reuse / leak |
An attacker with the user’s password cannot authenticate to your site without also possessing their second factor. The most important benefit of MFA. |
Phishing (with FIDO2) |
FIDO2 keys are bound to the registering domain. A phishing site cannot relay the WebAuthn challenge. TOTP and fallback OTP do not offer this guarantee. |
Credential stuffing |
Bulk-tried credentials fail at the MFA step. |
Brute force against passwords |
As above. Rate-limit MFA endpoints too — see below. |
Stolen session cookies (partially) |
|
What MFA does not defend against¶
Threat |
Why MFA doesn’t help (or only weakly helps) |
|---|---|
Server-side compromise |
Once the attacker is |
Database compromise |
TOTP secrets are stored plaintext (RFC 6238 requires the verifier to have them). An attacker with |
Session hijacking before recheck |
A stolen authenticated session works until |
Insider abuse |
An admin can disable any user’s factors via the |
Phishing (with TOTP or fallback) |
A user typing their 6-digit code on a phishing page reveals it within its 30-second validity window. Only FIDO2 prevents this. |
Email account takeover (with email fallback) |
If your fallback is email, an attacker who has compromised the user’s inbox can complete MFA. Mitigated by the fan-out design — see fallback risks. |
SIM-swap attacks (with SMS fallback) |
The attacker can social-engineer the user’s mobile carrier to receive the SMS. SMS fallback is convenient but well-known to be vulnerable. |
Lost devices / unrevoked keys |
A |
The “user has factors, just not active” gap¶
If a user has registered factors but all their session-level factor
entries have expired (e.g. RECHECK_MAX elapsed), they will be challenged
again. During the brief window between expiration and the next protected-view
hit, they are still “logged in” from Django’s perspective. If you want
every request to be MFA-fresh, lower max_age on your decorators.
Trust assumptions worth writing down¶
The Django session is the source of truth for “is this user MFA-authenticated right now?”. Session security ≈ MFA security. Set
SESSION_COOKIE_SECURE = True,SESSION_COOKIE_HTTPONLY = True,SESSION_COOKIE_SAMESITE = "Lax"(or stricter).MULTIFACTOR["BYPASS"], if set, is a security boundary. Audit it. See conditional bypass.Templates may be overridden. A bad override can re-introduce CSRF tokens, mishandle the FIDO2 JSON, or leak secrets. Keep overrides minimal.
The user’s email is trusted by the default fallback. If your
Usermodel lets users set their own email without verification, fallback trust is correspondingly weak.
Defence in depth checklist¶
HTTPS everywhere (HSTS preload if possible).
SESSION_COOKIE_SECURE,CSRF_COOKIE_SECURE,SECURE_SSL_REDIRECT = True.Rate limiter on
multifactor:*URLs.RECHECK = Truewith aRECHECK_MAXmatching your risk appetite.max_ageon the most sensitive views.Email verification for the email-fallback path.
Monitoring on
UserKey.objects.create(new factors registered) andUserKey.objects.filter(enabled=False).update(...)(factors disabled).Admin audit log for any
MultifactorUserAdmininline changes.
Reporting a security issue¶
See Disclosure.