How to set up a policy to use Django two-factor auth for all logins?
I've a website which I want to protect behind 2FA completely (email only). I've seen in the docs that it's possible with django-two-factor-auth, but not implemented.
How can I implement this?
Here is a working, final setup. django-two-factor-auth does not apply 2FA on everybody, by default, you apply it by adding one single piece of middleware to bounce any logged in and not verified user to the django setup/login page. The method is called email, and it's a plugin that you activate. Steps below.
Install and register the apps ( Django Two Factor Auth 1.15+ required for the email plugin ):
python
INSTALLED_APPS = [
...
'django_otp',
'django_otp.plugins.otp_static',
'django_otp.plugins.otp_totp',
'two_factor',
'two_factor.plugins.email', # the email method; remove phonenumber plugin, thus only email is offered
]
- Middleware order matters. The enforcer needs to be executed after OTPMiddleware, which provides the new property request.user.is_verified():
python
MIDDLEWARE = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_otp.middleware.OTPMiddleware',
'yourapp.middleware.ForceTwoFactorMiddleware',
...
]
LOGIN_URL = 'two_factor:login'
- The enforcer middleware. The real solution to "use it for all logins". It allows this unauthenticated user to trigger the login flow and after the user logs in without a verified OTP device, all pages except the 2FA/logout/static routes are redirected to the 'setup:'.
python
# yourapp/middleware.py
from django.conf import settings
from django.shortcuts import redirect
from django.urls import resolve
class ForceTwoFactorMiddleware:
# Routes that don't need to be accessed by a user until a user has been verified.
ALLOWED = {
'two_factor:login',
'two_factor:setup',
'two_factor:qr',
'two_factor:setup_complete',
'logout',
'admin:logout',
}
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
user = getattr(request, 'user', None)
if user and user.is_authenticated and not user.is_verified():
match = resolve(request.path_info)
name = f'{match.namespace}:{match.url_name}' if match.namespace else match.url_name
if name not in self.ALLOWED and not request.path.startswith(settings.STATIC_URL):
return redirect('two_factor:setup')
return self.get_response(request)
- Do not use Django's default login path, use the 2_factor login path:
python
# urls.py
from django.urls import path, include
from two_factor.urls import urlpatterns as tf_urls
urlpatterns = [
path('', include(tf_urls)),
...
]
Just be sure to not have any routes that still target django.contrib.auth.views.LoginView, otherwise users can sneak through the old login.
(Optional but highly recommended) Lock down the admin as well, because it also will have a login:
python
TWO_FACTOR_PATCH_ADMIN = True # default; do not set it to False
In addition, admin users are also forced to go through 2FA with the above mentioned middleware and is_verified().
That's the policy in a nutshell. The idea is that the library provides you with the flows (the login, the setup, the email token) and is_verified() from OTPMiddleware provides you with the check, which is "if logged in and not verified, you can only see the login/setup page. After setting up emails, is_verified() will be true for the user for that session and they will browse normally.
You can use django-two-factor-auth to add two-factor authentication to your Django website. First install and configure the package and set up the 2FA authentication flow. Then protect the views that require 2FA, for example by using OTPRequiredMixin. This ensures that a user must complete the second-factor verification before accessing protected pages.