You enable TOTP with django-allauth's allauth.mfa on a community site; how does it hook into login, and which gaps must you still close yourself?
answer
- an extra stage after credentials
- authenticator rows per user
- who else can log in?
- where replay and rate state live
basics
~20 sallauth.mfa adds a login stage that asks for a TOTP (or WebAuthn) code after credentials for users who enabled it. Still yours: the admin login bypass, a shared cache for replay and rate limits, and encrypting stored secrets.
solid answer
~40 sWith `allauth.mfa` installed, users enable authenticators at `/accounts/2fa/`; each is an `Authenticator` row whose type is `recovery_codes`, `totp` or `webauthn` (`MFA_SUPPORTED_TYPES` defaults to the first two). Any login through allauth's flows, password or social, gains a stage: if the user has TOTP or WebAuthn, they are sent to `mfa_authenticate` before `login()` completes, within `ACCOUNT_LOGIN_TIMEOUT`. Defaults are 30-second, 6-digit codes with zero tolerance and 10 recovery codes. Gaps to close: the Django admin's own login skips the stage (wrap it with `secure_admin_login`); a used TOTP code and the rate limits are tracked in the Django cache, so it must be shared across processes; secrets are stored as-is unless you override the MFA adapter's `encrypt()`/`decrypt()`; and MFA is opt-in per user.
code
python · 13 lines# urls.py
from django.contrib import admin
from django.urls import include, path
from allauth.account.decorators import secure_admin_login
admin.autodiscover()
admin.site.login = secure_admin_login(admin.site.login)
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")),
]go deeper
Know that allauth.mfa adds TOTP and recovery codes as an extra login step for users who enable them.
Explain the Authenticator model, the MFA login stage and when it fires, and the default TOTP period, digits and tolerance.
Close the production gaps: admin login bypass, shared cache for replay and rate limits, encrypting secrets via the adapter, and a safe recovery process.
Decide who must use MFA, how enforcement and recovery are staffed, and whether passkeys should replace passwords for some members.
## What allauth.mfa provides `allauth.mfa` is the multi-factor app of django-allauth, installed with the `mfa` extra and added to `INSTALLED_APPS` next to `allauth` and `allauth.account`. It supports: - **TOTP**: time-based one-time codes from an authenticator app; - **recovery codes**: single-use backup codes; - **WebAuthn**: security keys and passkeys, disabled by default. Each enabled factor is an `Authenticator` row linked to the user, with a `type` and a JSON `data` field holding the TOTP secret or the recovery-code seed. Users manage them from the MFA index at `/accounts/2fa/` when allauth's URLs are mounted under `accounts/`. ## Default settings | Setting | Default | |---|---| | `MFA_SUPPORTED_TYPES` | `["recovery_codes", "totp"]` | | `MFA_TOTP_PERIOD` | `30` seconds | | `MFA_TOTP_DIGITS` | `6` | | `MFA_TOTP_TOLERANCE` | `0` time steps | | `MFA_RECOVERY_CODE_COUNT` / `_DIGITS` | `10` codes of `8` digits | | `MFA_ALLOW_UNVERIFIED_EMAIL` | `False` | | `MFA_TRUST_ENABLED` | `False` (no "trust this browser") | ## How it hooks into login allauth's login is a sequence of **stages** run after credentials are accepted and before `login()` completes: 1. The password (or social provider, or login code) is verified. 2. Earlier stages run, such as email verification. 3. The **MFA authenticate stage** checks whether the user has TOTP or WebAuthn enabled. Recovery codes alone do not trigger it; a trusted-browser cookie or a passkey login skips it. 4. If required, the user is redirected to `mfa_authenticate` and must enter a code within `ACCOUNT_LOGIN_TIMEOUT` (900 seconds). 5. Only then is the session logged in. Because social logins go through the same flow, a member who enabled TOTP is asked for it after a social sign-in too. ## Gaps you must close - **The Django admin login.** The admin has its own login view outside allauth's flow, so it enforces neither MFA nor allauth's rate limits. allauth documents wrapping it: `admin.site.login = secure_admin_login(admin.site.login)`, which sends people through allauth's login first. - **A shared cache.** To stop a code being replayed within its window, allauth records each accepted code with `cache.add()` for one TOTP period. Its rate limits live in the cache too. A process-local cache such as `LocMemCache` only protects requests handled by the same process, so production needs a cache shared by all workers. - **Secrets at rest.** The MFA adapter's `encrypt()` and `decrypt()` return the text unchanged by default, so TOTP secrets and recovery seeds sit in the database as stored. Override them in an `MFA_ADAPTER` subclass to encrypt with a key kept outside the database. - **Enforcement.** MFA is something each user turns on. If moderators must use it, enforce that in your own code, for example by checking the adapter's `is_mfa_enabled()` before granting moderator tools. - **Unverified email.** With the default `MFA_ALLOW_UNVERIFIED_EMAIL = False`, an account with an unverified address cannot enable MFA, preventing an attacker who signed up with someone else's address from locking the real owner out with their own authenticator. ## Recovery codes and support Recovery codes are derived from a stored seed and a bitmask of used positions, so each code works once. Users can view and regenerate them unless `MFA_RECOVERY_CODES_SHOW_ONCE` is set. Plan a support path for members who lose both phone and codes; a manual reset of TOTP is an account-takeover vector, so it deserves the same identity checks as a password reset. ## Testing it - Test that a user with TOTP enabled is redirected to the MFA step after the password step and is not yet authenticated. - Test that the same code submitted twice fails the second time, under the cache backend used in production. - Test the admin login path after wrapping it, so a later refactor cannot silently reopen the bypass. ## Rolling it out to members - Start with moderators and staff, whose accounts carry the most power, and make enrolment part of granting the role. - Show recovery codes at enrolment and ask members to store them; most lockouts come from lost phones, not forgotten passwords. - Consider `MFA_TRUST_ENABLED` for "trust this browser" on personal devices, remembering that a trusted-browser cookie skips the second factor for its lifetime. - Watch support volume after launch: recovery requests are the signal that the flow or the messaging needs work.
- Why does allauth refuse to let an account with an unverified email enable MFA by default?An attacker could sign up with a victim's address, skip verification and enable their own authenticator. When the real owner later tries to recover the account by email, the attacker's second factor still blocks them. `MFA_ALLOW_UNVERIFIED_EMAIL = False` closes that path; relax it only if the risk is acceptable.
- What is the effect of raising MFA_TOTP_TOLERANCE from 0 to 1?Codes from one time step before or after the current one are also accepted, which absorbs clock drift on users' phones. The cost is a wider window in which a phished or shoulder-surfed code works, so keep it small and rely on the replay guard to reject reuse.
saying these in an interview costs you the question
- Enabling allauth.mfa forces every user to set up TOTP.
- allauth's MFA also protects the stock Django admin login.
- Recovery codes alone make allauth prompt for a second factor at login.
- TOTP replay protection works with any cache backend, including a per-process one.
- allauth encrypts stored TOTP secrets out of the box.