skip to content

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?

level: seniorimportance: nice to knowfreq 26%

answer

  1. an extra stage after credentials
  2. authenticator rows per user
  3. who else can log in?
  4. where replay and rate state live

basics

~20 s

allauth.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 s

With `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
python
# 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

for a junior

Know that allauth.mfa adds TOTP and recovery codes as an extra login step for users who enable them.

for a middle

Explain the Authenticator model, the MFA login stage and when it fires, and the default TOTP period, digits and tolerance.

for a senior

Close the production gaps: admin login bypass, shared cache for replay and rate limits, encrypting secrets via the adapter, and a safe recovery process.

for a principal

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.