JWT Authentication in Django, Part 1: Implementing the Backend

John Kealy
Geek Culture
Published in
7 min readMar 11, 2021

--

This article walks through the implementation of JWT authentication using a Django backend with an independent frontend, such as React or Vue. Since this topic is at a more intermediate level, a little knowledge of the following is assumed:

  • Django
  • Django Rest Framework

If you’d like to jump straight to Part 2 (all things Frontend) click here.

JWT Authentication, a controversial topic?

Authentication (along with its little brother, authorization) is an essential component of the modern web. That’s why I was so surprised to find such controversy still existing around it. If you dig in, you’ll find blogs and articles that seem to contradict each other, and JWT is often on the sh*t-list.

The reason I wrote this article was to showcase what I learned when trying to set up authentication in my decoupled Django project, hopefully saving you some pain. The goals I wanted to accomplish were the following:

  • Set up authentication/authorization in a new or pre-existing project, powered by a stand-alone Django Rest Framework backend and a Vue.js frontend.
  • Host the frontend at an apex domain (e.g. example.com) and the backend at a subdomain (e.g. api.example.com)
  • Use JWT (JSON Web Tokens) as the authentication method
  • Not throw my laptop out the window

So why is JWT authentication controversial?

Some developers will simply tell you not to use JWT, despite its popularity. I think a lot of this hating on JWT stems from the fact that it is stateless, meaning that if an attacker can get hold of your access token, the server will trust it no matter where it originated. Cross-site scripting (XSS) is a key vulnerability here, one that is less of a worry when using session authentication. On the other hand, it’s not uncommon for devs to implement JWT without really needing to do so, and session authentication is a safe and reliable alternative.

Many blogs and tutorials explaining JWT will casually tell the reader to just go ahead and store the access tokens in Local Storage, or cookies. Since Javascript can access these, bad things can happen.

Anyway, the point of this article is not to debate JWT, it assumes you’ve decided JWT is the best choice for your use case. This article will explain how to implement it. Part 1 will focus on the the backend, followed by a review of the frontend in Part 2.

Django authentication libraries

There are tons of authentication libraries for Django. Many do the same or similar things. Some are not well maintained, some are incredibly well maintained and documented. If you want to do some homework, here are my suggestions to look at:

At some point, you’ll probably come across OAuth,OAuth2, and OpenID Connect as well. These are deeper topics. Just keep in mind that

OAuth is an open standard for access delegation, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords.

So OAuth is a standard, and you can use JWT within that standard. There’s also a third party service called Auth0 which I was constantly confusing with OAuth… so watch out for that one. Of course, if you want to look into third party tools, feel free to check out Auth0 and Okta.

All things considered, I chose Dj Rest Auth. This was the one I found to best fit my preference for the library to “just work”. And in actual fact, you can configure it to use the Simple-JWT and All-Auth libraries anyway, so it all ties together nicely. Plus, the documentation can take you into using social authentication (e.g. Facebook or Github logins) when you’re ready to do so.

So without further ado, let’s do some coding.

Setting up the Django backend

I won’t go into the basics of how set up a Django application, this is assumed. Nor will I go into the very simple installation of dj-rest-auth; I trust you to follow the excellent documentation.

After installing dj-rest-auth (don’t forget to apply the migrations) you will immediately have access to various REST endpoints such as /login and /logout. It’s a good idea to test these out in the Django Rest Framework browsable API (which normally lives at localhost:8000).

If you hit errors at this stage, make sure you’ve added all the necessary libraries in settings.py , and installed everything into your virtual environment.

INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.admin',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'corsheaders',
'dj_rest_auth',
'rest_framework',
'rest_framework.authtoken',
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'dj_rest_auth.registration',
'sslserver', # we'll discuss this one later
'users' # This is just the name of my django app
]

Notice that this list includes allauth social accounts; this is actually so that the registration endpoint of dj-rest-auth can work. Also, be sure to have djangorestframework-simplejwt installed in your virtual environment.

Okay, now we’ll explicitly start using JWT. The settings to apply in settings.py are:


...
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'dj_rest_auth.jwt_auth.JWTCookieAuthentication'
),
'DEFAULT_SCHEMA_CLASS': \
rest_framework.schemas.coreapi.AutoSchema',
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated'
]
}
REST_SESSION_LOGIN = False
REST_USE_JWT = True
JWT_AUTH_COOKIE = 'jwt-access-token' # you can set these
JWT_AUTH_REFRESH_COOKIE = 'jwt-refresh-token' # to anything
JWT_AUTH_SECURE = True
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = ['https://example.com']
...

This way, we’ve turned off session authentication, told Django to use JWT, set the names of the cookies we’ll send to the browser, set CORS to accept requests with embedded credentials, and finally, set https.

The last point creates a problem: how are we supposed to use SSL/TLS in a development environment? Sure, we could just not set JWT_AUTH_SECURE, but it turns out that we’re going to need https anyway. Which leads us to…

Cookies (nom nom)

Okay, back the controversy. I think people mostly seem to hate on JWT because developers keep placing the access token and the refresh token in LocalStorage. Don’t do this. And don’t put them in regular cookies either.

What needs to be done is to put the tokens in a special type of cookie called a httpOnly cookie. Javascript can’t read this type of cookie, which offers some protection against XSS.

Luckily, dj-rest-auth abstracts nearly everything about httpOnly cookies (well, maybe not that luckily, I picked the library pretty much because of this). What isn’t covered, however, is exactly how to make a decoupled frontend application interact with the backend using these cookies. There are important differences that now arise that you didn’t need to worry about when just using the browsable API:

  • dj-rest-auth’s httpOnly cookies like https
  • The domains must be ‘same-site’

What these points boil down to is that we need https, even in our development environment, and we must use proper domain names that are related i.e. subdomains. We need to spoof these to in order to use our development environment. In production, this won’t be an issue because we’ll have SSL and real DNS.

Note: I don’t think I need to tell you to use https in production. If you find Nginx a little confusing, look into CaddyServer, it’s https by default.

How to create a https dev environment with custom domains

On the backend, there’s a tool for this: django-sslserver. Install it into your environment, add it to INSTALLED_APPS, and replace runserver with runsslserver. Now just tell your browser that it’s okay to accept the self signed certificate, and you’re good to go. Magic.

We also need our authentication backend to see the API requests as coming from the same root/apex domain for it to work right. This can be achieved by applying local domains in your hosts file. In Linux, this is located at /etc/hosts.

Add the following to /etc/hosts, or whatever the hosts file is for your operating system:

127.0.0.1     api.example.com
127.0.0.1 example.com

We still need to include the ports, but you should now be able to visit the browsable API at https://api.example.com:8000, and the frontend at https://example.com:3000 (or whatever port your frontend is listening on).

Final Backend Task: A splash of custom middleware

Okay, we’re very nearly done with the backend. However, there’s a sticking point about dj-rest-auth, as discussed on this Github issue tracker entry. Hopefully it will be solved in a later release, but for now, the problem is that our browser is going to be sending our access tokens in the request header.

This is fine for normal requests, but if we wish to refresh our access token, dj-rest-auth requires that the refresh token be sent in the body, not the headers. The following middleware was suggested in the Github issue to move the tokens from the header to the body.

In your Django app, create a middleware.py file. Add this code:

You must then add this middleware to INSTALLED_APPS:

MIDDLEWARE = [        ...    'yourappname.middleware.MoveJWTRefreshCookieIntoTheBody',
]

With all this set up, Django should now be running a functioning JWT server in your development environment.

In Part 2 of this article, we’ll explore some frontend settings, and round off the full stack implementation.

--

--