Adding Django Admin Panel MFA to an Existing Project

enderthenetrunner

Robert Babaev

Posted on December 2, 2022

Adding Django Admin Panel MFA to an Existing Project

Contents

  • Introduction
  • MFA? What's That?
  • Is This Gonna Be Super Complicated?
  • Baseline Assumptions
  • Installing Packages
    • Pip
    • Poetry
  • Updating The Settings
    • INSTALLED_APPS
    • MIDDLEWARE
    • ENABLE_MFA
  • Updating The Admin Panel
  • Updating The URLs
  • Deployment
  • Conclusion

Introduction

If you're like me, you've probably started up a Django project, got your Admin panel all nice and set up . . . and then realize down the road that maybe relying on just your password manager for account security isn't a great idea. We're all human, so let's fix that and add some MFA to our admin panel!

This task will use a TOTP-based method, leveraging apps such as Google Authenticator, Authy, etc. Since all MFA apps that work this way use essentially the same method, you don't need to worry about choosing one authenticator over the other, aside from possibly security considerations between the apps. Otherwise, pick your favourite and go!

MFA? What's That?

MFA (or Multi-Factor Authentication), is a security principle that revolves around combining authentication factors for added security. The three primary factor categories are Something You Know (think passwords, explicit information), Something You Have (an out-of-band device like a smartphone or hardware token), and Something You Are (biometrics, think fingerprints and retina scans). The theory behind this is that someone would have to compromise more than one factor in order to gain access to your account, as opposed to just cracking your password.

Since biometrics really aren't great for web authentication, we'll stick to the two other categories we have: Something You Know (username/password), and Something You Have (a smartphone).

Is This Gonna Be Super Complicated?

Nope! Thanks to libraries like django-otp and qrcode, implementing this in Django will be simple. You'll just have to flick a switch in the settings once you have MFA set up.

Baseline Assumptions

Let's assume that you already have:

  • A Django application with some models already registered
  • A superuser in the application

For the sake of the tutorial, let's say you have a project structure similar to this:

myproject/
├── data
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   ├── models.py
│   ├── serializers.py
│   ├── tests.py
│   └── views.py
├── manage.py
└── myproject
    ├── asgi.py
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
Enter fullscreen mode Exit fullscreen mode

myproject is the actual django project, and data is an "app" in the project.

If you have Docker, Poetry, or whatever else you might have in your project, add that as needed.

Installing Packages

We'll need the following packages:

  • django-otp
  • qrcode

Pip

To install with Pip (assuming no requirements.txt file):

pip3 install django-otp qrcode
Enter fullscreen mode Exit fullscreen mode

Poetry

To install with Poetry:

poetry add django-otp qrcode
Enter fullscreen mode Exit fullscreen mode

Updating The Settings

We'll need to delve into the settings.py file and update a few things.

INSTALLED_APPS

First, update your INSTALLED_APPS list as follows, adding django_otp and django_otp.plugins.otp_totp:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'data',
    'django_otp',
    'django_otp.plugins.otp_totp',
]
Enter fullscreen mode Exit fullscreen mode

This will add the django-otp capabilities into the app so we can actually work with it.

MIDDLEWARE

Next, update your middleware. You'll need to add django_otp.middleware.OTPMiddleware somewhere after django.contrib.auth.middleware.AuthenticationMiddleware, like this:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django_otp.middleware.OTPMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Enter fullscreen mode Exit fullscreen mode

ENABLE_MFA

Finally, we'll need to add a setting to the bottom of the settings.py file to enable or disable MFA (i.e. enable it once we have the token in our authenticator app).

I've used environment variables here (which, if you can't use cloud secrets, is probably the next best thing for security, as you're still not hardcoding anything into your application). Of course, if you're not using environment variables for some reason, adapt as necessary.

# Additional settings

ENABLE_MFA = os.environ.get("ENABLE_MFA", "false") == "true"
Enter fullscreen mode Exit fullscreen mode

Updating The Admin Panel

Now, this method is optimized for a single application in the project, but you could do it for multiple with some tweaks.

First up, you'll need to change up how you define your admin panel.

For the sake of this tutorial, let's assume your old admin.py file looked like this:


from django.contrib import admin
from data.models import *

# Register your models here.

class ArticleAdmin(admin.ModelAdmin):
    pass

class ProjectAdmin(admin.ModelAdmin):
    pass

class ContactAdmin(admin.ModelAdmin):
    pass

admin.site.register(Article, ArticleAdmin)
admin.site.register(Project, ProjectAdmin)
admin.site.register(Contact, ContactAdmin)
Enter fullscreen mode Exit fullscreen mode

Now, in order to effectively enable MFA we need to do a few things.

First, update your imports. Add the following to your imports section:

from django.conf import settings
from django_otp.admin import OTPAdminSite
from django.contrib.auth.models import User
from django_otp.plugins.otp_totp.models import TOTPDevice
from django_otp.plugins.otp_totp.admin import TOTPDeviceAdmin
Enter fullscreen mode Exit fullscreen mode

Next, add the following code to the section just before you register the Article, Project, and Contact models:

class OTPAdmin(OTPAdminSite):
    pass

if settings.ENABLE_MFA:
    admin_site = OTPAdmin(name='OTPAdmin')
    admin_site.register(User)
    admin_site.register(TOTPDevice, TOTPDeviceAdmin)
else:
    admin_site = admin.site
    setup_admin_site = OTPAdmin(name='OTPAdmin')
    setup_admin_site.register(User)
    setup_admin_site.register(TOTPDevice, TOTPDeviceAdmin)
Enter fullscreen mode Exit fullscreen mode

So what does all that junk do? Well, we have the OTPAdmin class, which is basically a wrapper for Django's actual admin panel. Now we do one of two things depending on whether MFA is enabled in the settings:

  • If MFA is enabled, we make an OTPAdmin site and use that for our admin site, registering the User and TOTPDevice models. This allows us to login with our authenticator app.
  • If it isn't, we use the Django default admin site, and set up a dummy admin site for the TOTP setup. This way, we can set up our MFA in the admin panel and then enable it with the flick of a switch in the settings.

Now, just update your model registrations:

admin_site.register(Article, ArticleAdmin)
admin_site.register(Project, ProjectAdmin)
admin_site.register(Contact, ContactAdmin)
Enter fullscreen mode Exit fullscreen mode

In all, you should have:

from django.contrib import admin
from data.models import *
from django.conf import settings
from django_otp.admin import OTPAdminSite
from django.contrib.auth.models import User
from django_otp.plugins.otp_totp.models import TOTPDevice
from django_otp.plugins.otp_totp.admin import TOTPDeviceAdmin

# Register your models here.

class ArticleAdmin(admin.ModelAdmin):
    pass

class ProjectAdmin(admin.ModelAdmin):
    pass

class ContactAdmin(admin.ModelAdmin):
    pass

class OTPAdmin(OTPAdminSite):
    pass

if settings.ENABLE_MFA:
    admin_site = OTPAdmin(name='OTPAdmin')
    admin_site.register(User)
    admin_site.register(TOTPDevice, TOTPDeviceAdmin)
else:
    admin_site = admin.site
    setup_admin_site = OTPAdmin(name='OTPAdmin')
    setup_admin_site.register(User)
    setup_admin_site.register(TOTPDevice, TOTPDeviceAdmin)

admin_site.register(Article, ArticleAdmin)
admin_site.register(Project, ProjectAdmin)
admin_site.register(Contact, ContactAdmin)
Enter fullscreen mode Exit fullscreen mode

Updating The URLs

Finally, let's head over to our project's urls.py file.

Add the following import:

from data.admin import admin_site
Enter fullscreen mode Exit fullscreen mode

Remember that admin_site variable we made that's either the OTPAdmin wrapper or the Django admin site? That's the one!

Then, swap the admin.site.urls for admin_site.urls in your urlpatterns:

urlpatterns = [
    path('admin/', admin_site.urls),
    ...
]
Enter fullscreen mode Exit fullscreen mode

Now when we deploy, we'll either be met with the standard Django admin urls by default, or the OTPAdmin URLs when we enable MFA in the settings. Easy!

Deployment

Make and run your migrations and launch the app, making sure that ENABLE_MFA is set to False (however you need to do that) in your settings.

Navigate to and you should see the old admin login:

The Old Django Admin Login

Now you're going to want to do the following once you log in:

  1. Add a new TOTP Device (see the OTP_TOTP section of the admin panel).
  2. Select your user id (or search for it), and name the device however you like.
  3. Leave the rest of the settings as they are.
  4. Save the new device.
  5. Click on the "qrcode" link on the new device.
  6. Scan the QR Code into your authenticator app.

That's it! You should be able to set ENABLE_MFA to True in your settings, restart the server, and log in to your admin panel with your authenticator app.

Conclusion

This was a tutorial on how to integrate MFA into your Django admin panel. You can expand this to multiple apps in the project with some tweaks, but I'll leave that as an exercise for your situation.

Thank you for reading, and best of luck building secure apps!

💖 💪 🙅 🚩
enderthenetrunner
Robert Babaev

Posted on December 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related