Oli Warner About Contact Oli on Twitter Subscribe

Easy multifactor authentication in Django

Friday, 3 June 2022 django security

Use django-multifactor to make your Django websites extra-secure by requiring a secondary authentication factor. Disclaimer: I made this.


Three years ago a I needed to add an extra layer of security around Django Admin. Usernames and passwords and even network level limits weren’t enough. That need quickly turned brewed into django-multifactor but I’ve never blogged about it. This is a library that I now habitually install, so thought it worth mentioning just in case it helps others too. Before we get very much older, let’s answer the question that some of you might have: What is multifactor authentication?

Authentication factors come in a number of flavours; fundamentally different types of information:

Multifactor authentication makes the user use more than one of these at once, and in doing so makes it far less likely that somebody can gain access with stolen credentials.

Django is a batteries-included framework. They’re great batteries too; its authentication and Admin django.contrib libraries are core requirements for a lot of my projects, but like with most framework code, it’s hard to change the behaviour. Shimming in an extra factor in the normal login flow isn’t easy.

This is where the decorator-based library django-multifactor steps up. It wraps the views you want to protect, and keeps track of its own authentication status. You can have some views with no secondary factor requirements, and some that demand two or three. It’s out of the normal authentication flow so it doesn’t need to alter how existing Django subsystems work.

Installing django-multifactor

Start by installing it (and a library to make wrapping includes easy):

pip install django-multifactor django-decorator-include

We need to make a couple of changes so your settings.py. Firstly add 'multifactor', to INSTALLED_APPS and then we’ll need to tell FIDO2 and U2F tokens what the name of our service is (ie the domain name):

'FIDO_SERVER_ID': 'example.com',
'FIDO_SERVER_NAME': 'My Django App',
'TOKEN_ISSUER_NAME': 'My Django App',
'U2F_APPID': 'https://example.com',

Now we just need to wrap it around the views in urls.py that we want to protect. Let’s protect the Admin:

from decorator_include import decorator_include
from multifactor.decorators import multifactor_protected

urlpatterns = [
path('admin/multifactor/', include('multifactor.urls')),
path('admin/', decorator_include(
multifactor_protected(factors=1), admin.site.urls)),
# ...

That’s it! Users accessing /admin/ will be bounced through /admin/multifactor/ to make sure they have enough factors. Your site is already more secure.

Taking it further

This gets you a system that allows OTP, U2F and any FIDO2 factors, with a fallback to email if they don’t have any of their installed factors to hand. On that, email is a weak factor. Many people share password between accounts and the transport can be unencrypted. It’s easy to disable the email fallback, and it’s just as easy to replace it with another more secure transport. Think everything from SMS to carrier pigeons. You can also tweak the design, or see who’s using it in UserAdmin.

The project has some miles on the clock now, and has been used in multiple production deployments of mine. It’s had help from external contributors too so I’d like to thank them all, especially @StevenMapes, for kicking my arse into gear. It’s been a weird few years.

If you think something’s missing, bug reports and PRs are very welcome. And if you can figure out a way to make this useful for django-rest-framework deployments, I welcome those suggestions on the drf bug.

But remember…

… few things withstand $5 wrenches. django-multifactor can insulate you against a lot of remote attacks but very little will secure against greed and fear. If you’re dealing with actual-important data, it’s important that you have monitored auditing in place, as well as a sensible permissions framework to ensure only the right people have access (no everyday superuser accounts!)