Add Two-Factor Authentication to the Django Admin

Require two-factor authentication for the Django admin in production: django-otp with OTPAdminSite and TOTP devices, recovery codes, enrolling existing staff, passkeys with django-allauth MFA, and avoiding lockouts during rollout.

Problem statement

The Django admin can delete data, change permissions, and see personal information, yet by default it is protected by a username and password only. Reused or phished passwords are the most common way admin panels are taken over. Requiring a second factor (a time-based code from an authenticator app, a hardware key, or a passkey) blocks those attacks even when the password leaks. Django does not ship 2FA itself, but mature packages add it to the admin with a few settings, and the main work is rolling it out without locking staff out.

Admin login with 2FAPasswordsomething you knowOTP or passkeysomething you haveVerified sessionotp_device setAdmin accessonly after bothA stolen password alone no longer opens the admin.
  1. pip install django-otp qrcode and add django_otp, django_otp.plugins.otp_totp, and django_otp.plugins.otp_static to INSTALLED_APPS.
  2. Add django_otp.middleware.OTPMiddleware after AuthenticationMiddleware.
  3. Switch the admin to OTPAdminSite, which requires a verified device for admin access.
  4. Enrol every staff user with a TOTP device and issue static recovery codes before enforcing.
  5. Alternatively, use django-two-factor-auth for ready-made enrolment views, or django-allauth's MFA (TOTP, recovery codes, WebAuthn/passkeys) if you already use allauth.
  6. Document a recovery procedure for lost devices.

Step-by-step solution

1. Install django-otp

INSTALLED_APPS += [
    "django_otp",
    "django_otp.plugins.otp_totp",
    "django_otp.plugins.otp_static",
]

MIDDLEWARE = [
    # ...
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django_otp.middleware.OTPMiddleware",
    # ...
]
OTP_TOTP_ISSUER = "Example Ltd admin"
python manage.py migrate

OTPMiddleware adds request.user.is_verified(), true only when the user authenticated with a device in this session.

2. Require OTP on the admin

# config/urls.py
from django.contrib import admin
from django_otp.admin import OTPAdminSite

admin.site.__class__ = OTPAdminSite

urlpatterns = [path("admin/", admin.site.urls), ...]

OTPAdminSite shows a login form with a token field and only grants access to verified users. Changing the class of the default site keeps every existing ModelAdmin registration working.

choosing a 2FA packageFactorsBest whendjango-otpTOTP, static codes, more viapluginsadmin-only, minimal UIdjango-two-factor-authTOTP, SMS, WebAuthn✓need enrolment viewsdjango-allauth MFATOTP, recovery, passkeys✓already using allauthAll three can protect the admin; pick by the rest of your auth stackdjango-deployment.com
Prefer authenticator apps or passkeys over SMS, which can be intercepted.

3. Enrol staff before enforcing

If you switch to OTPAdminSite before anyone has a device, nobody can log in. Roll out in stages:

  1. Deploy django-otp without changing the admin site.
  2. Create devices for each staff user. In the admin, add a TOTP device for each user and share the QR code (config_url) securely, or build a small self-service enrolment view.
  3. Create static devices with recovery codes for each user and have them store the codes in a password manager.
  4. Confirm every active staff user has a confirmed device:
from django.contrib.auth import get_user_model
from django_otp import devices_for_user
missing = [u.username for u in get_user_model().objects.filter(is_staff=True, is_active=True)
           if not any(d.confirmed for d in devices_for_user(u))]
print(missing)
  1. Switch the admin to OTPAdminSite once the list is empty.

4. Lost devices and recovery

Recovery codes from the static device each work once. If a user loses both phone and codes, another superuser deletes their devices in the admin (or via the shell) and re-enrols them after verifying identity out of band. Write this procedure down; it is also how an attacker would try to socially engineer access, so require a second person to approve.

5. Passkeys and allauth

If the site already uses django-allauth, its MFA module adds TOTP, recovery codes, and WebAuthn passkeys with built-in views. Protecting the admin then means requiring that staff log in through allauth with MFA and limiting the admin to verified sessions, for example by routing /admin/login/ to the allauth login.

a lockout-free rollout1Deploy django-otp, admin unchangedmigrations applied2Enrol TOTP for every staff userQR shared securely3Issue recovery codesstored in a password manager4Check no staff lack a devicescript or report5Switch to OTPAdminSiteenforced for allEnforcement comes last, after everyone can pass itdjango-deployment.com
Keep one break-glass superuser procedure documented and tested.

Verification check: log in to /admin/ without a token and confirm access is refused; log in with a valid TOTP code and confirm it works; use a recovery code once and confirm it cannot be reused.

Explanation

Time-based one-time passwords (TOTP, RFC 6238) derive a six-digit code from a shared secret and the current time, so a phone app and the server agree on the code without network contact. django-otp stores devices per user and marks the session as verified when a token matches. OTPAdminSite checks that flag on every admin request, so a session authenticated with only a password cannot reach admin views. Passkeys replace both factors with a cryptographic key bound to the site, which also defeats phishing pages.

Edge cases / notes

  • Clock drift. TOTP depends on accurate server time; keep NTP running.
  • Throttling. django-otp throttles failed token attempts per device; combine with login rate limiting.
  • API access with tokens or sessions outside the admin is not covered by OTPAdminSite; protect sensitive APIs separately.
  • Service accounts that use the admin should not exist; use management commands or APIs with scoped tokens.

Next steps

FAQ

How do I add 2FA to the Django admin?

Install django-otp, add its middleware and TOTP plugin, enrol staff devices, and switch the admin to OTPAdminSite.

Will enabling OTPAdminSite lock everyone out?

Yes, if staff have no devices. Enrol everyone and issue recovery codes first.

Can the Django admin use passkeys?

Yes, through django-allauth's MFA with WebAuthn or django-two-factor-auth's WebAuthn support.

What if a user loses their phone?

They use a recovery code; otherwise a second admin removes their devices and re-enrols them after verifying identity.

Learning path · step 7 of 15

Hardening a Django deployment

Keep going