Adding Django Admin Panel MFA to an Existing Project
Tags: django, webdev, security, python
Published: 2022-12-02 18:34:45

## 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
```

`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):
```bash
pip3 install django-otp qrcode
```

### Poetry
To install with Poetry:
```bash
poetry add django-otp qrcode
```

## 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`:
```python
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',
]
```

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:

```python
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',
]
```

### 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.

```python
# Additional settings

ENABLE_MFA = os.environ.get("ENABLE_MFA", "false") == "true"
```

## 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:

```python

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)
```

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:

```python
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
```

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

```python
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)
```

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:

```python
admin_site.register(Article, ArticleAdmin)
admin_site.register(Project, ProjectAdmin)
admin_site.register(Contact, ContactAdmin)
```

In all, you should have:

```python
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)
```

## Updating The URLs
Finally, let's head over to our project's `urls.py` file.

Add the following import:
```python
from data.admin import admin_site
```

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`:
```python
urlpatterns = [
    path('admin/', admin_site.urls),
    ...
]
```

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 [](http://localhost:8000/admin) and you should see the old admin login:

![The Old Django Admin Login](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wlye7w5xdoopq3kio3cb.png)

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!