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:
- Usernames and passwords are something you know
- Smart cards and FIDO2 USB keys are something you have
- Fingerprints and facial imprints are based on something you are
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):
MULTIFACTOR = {
'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!)