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.
pip install django-otp qrcodeand adddjango_otp,django_otp.plugins.otp_totp, anddjango_otp.plugins.otp_statictoINSTALLED_APPS.- Add
django_otp.middleware.OTPMiddlewareafterAuthenticationMiddleware. - Switch the admin to
OTPAdminSite, which requires a verified device for admin access. - Enrol every staff user with a TOTP device and issue static recovery codes before enforcing.
- Alternatively, use
django-two-factor-authfor ready-made enrolment views, or django-allauth's MFA (TOTP, recovery codes, WebAuthn/passkeys) if you already use allauth. - 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.
3. Enrol staff before enforcing
If you switch to OTPAdminSite before anyone has a device, nobody can log in. Roll out in stages:
- Deploy django-otp without changing the admin site.
- 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. - Create static devices with recovery codes for each user and have them store the codes in a password manager.
- 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)
- Switch the admin to
OTPAdminSiteonce 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.
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
- Combine with network restrictions: Put the Django Admin on a Private Tailscale Network.
- Review the rest of the Django Admin Security Checklist.
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.