https://medium.com/@dakota.lillie/django-react-jwt-authentication-5015ee00ef9a
Recently I’ve been building an app using a combination of React for the frontend and Django for the backend. I wasn’t previously all too familiar with Django, but this seemed like a good opportunity to teach myself, and having experience with Ruby on Rails has made the process a little easier. However, Django and Rails have their fair share of differences, and one of the things which I’ve had the most trouble implementing is user authentication. So I’m sharing what I’ve learned, in the hopes that you too might find it useful!
We’ll be building a demo app, for which you can find the finalized code for the frontend here and for the backend here. Also, I should mention that I’m currently working with Django 2.0.4, and React 16.3.
The Basic Premise: Sessions vs. Tokens
There are many different potential approaches to implementing authentication. Here I’ll just cover two of the most common ones: session authentication (via cookies), and token authentication. There are plenty of articles around the internet differentiating the two, but I’ll give a quick summary of what they are and how they work.
Session authentication is stateful, which means when a user logs in, data pertaining to their authenticated status gets stored either in memory or a database. This data is collectively referred to as a session, and to facilitate its access, a cookie with the user’s session ID is sent back to the client and stored in the browser. Then, next time the client makes a request, that cookie is included in the request and the server searches for a session that matches the cookie’s session ID. If there’s a match, then the backend proceeds to process the request. When the user logs out, another request has to be made to the server so that the relevant session data is destroyed, along with the cookie in the browser’s storage.
Session authentication was for a long time considered the preferred approach, and it remains widely used. However, it suffers from several notable drawbacks. Most relevant to our particular circumstance is the fact that cookies are tied to a particular domain, which leads to significant CORS headaches when the front and back ends are decoupled.
That leads us to token authentication. Tokens are key-value pairs which usually live in the local storage of your browser. In this regard they are similar to cookies — however, where session authentication was stateful, token authorization is stateless, meaning there’s no record kept on the server of which users are logged in, how many tokens have been issued etc. Instead, tokens are generated by means of a complex encryption process which, when reversed and decrypted, authenticates the user.
This is a very broad generalization of the methodology employed by JSON Web Tokens (JWTs for short). JWTs are regarded as the gold standard in authentication right now, so that’s what we’ll be using today. With that said, let’s write some code!
Setting Up Django
First off, we need to set up our virtual environment (if that’s unfamiliar to you, I wrote a whole blog post about it!). I’m going to use pipenv here—make sure you have it installed, then navigate to the directory you want your project to be in and run:
pipenv install
Once that’s done, activate the virtual environment with:
pipenv shell
Now we’re going to need to install some packages, including Django, Django REST framework (hereafter referred to as the DRF), Django REST framework JWT, and Django CORS headers. The DRF is what we’ll be layering on top of Django to turn our project into an API, while Django REST framework JWT gives us the ability to use JWT tokens for our app, and Django CORS headers is necessary to avoid CORS issues:
pipenv install django
pipenv install djangorestframework
pipenv install djangorestframework-jwt
pipenv install django-cors-headers
Once this is done, we’re ready to create the Django project. Run the following:
django-admin startproject mysite .
Note the period at the end there — that denotes that we want to create the project with the current directory as as the root, rather than putting it in a new subdirectory. At this point, your project structure should look something like this:
Now we need to adjust some of our project’s settings. Go into settings.py and make the following modifications:
1 | # ... |
Let’s examine what we’ve done here: first, we’ve registered the DRF and CORS headers packages with our project by adding them to INSTALLED_APPS. Then, we added a piece of custom CORS middleware, making sure to place it place it above any middleware that generates responses such as Django’s CommonMiddleware. We then customized some of the default settings for the DRF: first, we set the DEFAULT_PERMISSION_CLASSES, which in this case will require a request to be authenticated before it is processed unless specified otherwise. Then, we set the DEFAULT_AUTHENTICATION_CLASSES, which determines which authentication methods the server will try when it receives a request, in descending order. Finally, we added localhost:3000 to the CORS_ORIGIN_WHITELIST, since that’s where the requests from our React app will be coming from.
The DRF JWT package provides us with a default view for decoding received JWTs. We can add that to urls.py:
1 | #... |
And we’re already almost ready to test whether or not this works! But before we can, we need to create a user, and before do that, we need to apply our migrations. Run the following:
python manage.py migrate
Once that’s done, the easiest way to create a new user is with:
python manage.py createsuperuser
Fill in all the fields and you should be good to go. Now if you start your development server:
python manage.py runserver
and navigate to http://localhost:8000/token-auth/, you should see an html form there with username and password fields (this is a convenience provided by the DRF… isn’t it awesome?). Fill in these fields and you should see the JWT itself displayed right there on the page.
This takes care of logging in but we still need our users to be able to sign up. Fortunately, Django comes with a built in User model that we can use (which is easy enough to customize, should you need to do so). All we need to do is create the view for it. But if we’re making a view, we’re going to need an app to put it in. So let’s do that now:
python manage.py startapp core
I’m calling the app “core”, but you can of course call it whatever you want. Before we do anything else, let’s register the app in settings.py:
1 | # ... |
Now we need to take a few steps that might seem circuitous at first but which I promise will make sense by the end. First, we need to create a couple serializers for our User model. These serializers will be responsible for serializing/unserializing the User model into and out of various formats, primarily JSON in our case. Go ahead and create a new core/serializers.py file, and fill it with the following:
1 | from rest_framework import serializers |
The reason we’re making two different serializers for the model is because we’ll be using the UserSerializerWithToken for handling signups. When a user signs up, we want the response from the server to include both their relevant user data (in this case, just the username), as well as the token, which will be stored in the browser for further authentication. But we don’t need the token every time we request a user’s data — just when signing up. Thus, separate serializers.
That’s the ‘why’, but let’s take a closer look at the code for the ‘how’. Both serializers inherit from rest_framework.serializers.ModelSerializer, which provides us with a handy shortcut for customizing the serializers according to the model data they’ll be working with (otherwise we’d need to spell out every field by hand). In the internal Meta class, we indicate which model each serializer will be representing, and which fields from that model we want the serializer to include.
But the User class doesn’t have an internal ‘token’ field, so for that we do need to define our own custom field. We define the token variable to be a custom method, then add a get_token() method which handles the manual creation of a new token. It does this using the default settings for payload and encoding handling provided by the JWT package (the payload is the data being tokenized, in this case the user). Finally, we added the custom ‘token’ field to the fields variable in our Meta internal class.
We also need to make sure the serializer recognizes and stores the submitted password, but doesn’t include it in the returned JSON. So we add the ‘password’ field to fields, but above that also specify that the password should be write only. Then, we override the serializer’s create() method, which determines how the object being serialized gets saved to the database. We do this primarily so that we can call the set_password() method on the user instance, which is how the password gets properly hashed.
Now we’re ready to start configuring our views in core/views.py. There are many ways of going about doing this (function-based views, class-based views, or viewsets). Since viewsets can be confusing if you don’t understand what’s happening internally (and since we don’t have enough views here to reap their benefits anyway), I’ll use the other forms of views here:
1 | from django.http import HttpResponseRedirect |
Let’s walk through this. First, we have the current_user function-based view. This view will be used anytime the user revisits the site, reloads the page, or does anything else that causes React to forget its state. React will check if the user has a token stored in the browser, and if a token is found, it’ll make a request to this view. Since we’ve set things up properly, the token will be parsed automatically to check for authentication, and if validated we’ll receive the user object associated with that token in the request’s user property. We can then serialize the user object, and return the data from the serializer in the response. This whole function then serves as the input for the @api_view decorator, which specifies the request methods this view will respond to (in this case, just GET requests).
Next, we have our class-based UserList view. When a request is routed to this view, a UserSerializerWithToken serializer object is instantiated with the data the user entered into the signup form. The serializer checks whether or not the data is valid, and if it is, it’ll save the new user and return that user’s data in the response (including the token, since we’re using this particular serializer). Note that we specify the permissions for this class to be permissions.AllowAny, because otherwise, the user would have to be logged in before they could sign up, which could be frustrating.
We have our views now, but as of yet still no way of accessing them. To do that, we need to assign them some routes. It’s customary to give each app its own route configuration, so create a new core/urls.py file and edit it like so:
1 | from django.urls import path |
Now we’ll hook up this file to the root urls conf by editing that file (mysite/urls.py):
1 | #... |
With that, we’re almost good to go — but we still have a problem. Currently, when a user logs in, they receive their token but not any of their user data. To remedy this, we could make a separate request to the current_user() view we defined earlier… but that’s annoying. Why make multiple requests? Instead, let’s customize our JWT settings a bit.
What we’re going to need to do is define a custom JWT response payload handler which includes the user’s serialized data. Within the mysite directory, make a new file called utils.py and fill it with the following:
1 | from core.serializers import UserSerializer |
All this is doing is adding a new ‘user’ field with the user’s serialized data when a token is generated. This is going to be our new default JWT response handler, which we can set up by adding a little bit to our settings.py file:
1 | # ... |
Now, when a user logs in, they’ll get all their user data along with their token… And we should finally be done! On to the frontend.
One additional point: JWT encoding/decoding involves the use of a secret key, which defaults to the SECRET_KEY constant defined in settings.py. If you’re deploying an app that uses JWT to production, be sure to change this, or at least hide the secret key from Github.