ReactJS is a fantastic frontend framework, and Django is a fantastic backend framework. However, as usual when dealing with anything of more than trivial complexity, it isn’t easy to get the two to place nicely together. It's not like taping a banana to a wall.
This is a mid-level tutorial, going beyond what most tutorials deal with for making Django and React work together. I’m not just going to leave you with an incomplete picture here. This is the whole shebang. Get over it and build something useful. It was a major pain in the ass when we did it for my startup Lollipop.ai the first time, so please use this knowledge wisely and pardon the plug.
Why even use JSON Web Tokens? Since sessions aren’t typically supported in Apps, and in this we want to be able to build a backend API that can support both web and apps, we are going to use JSON Web Tokens or JWT to handle the authentication hand-off between the front and backends. On top of that, JWT are quite compact and easy to use. For more information, check Auth0’s description here.
It’ll only take you 6 steps to do it all!
Part 1 - Django:
1. Django Custom User
2. DRF serializers and auth
Part 2 - React:
3. Installing React inside our Django project as a standalone app
4. Preparing React for Authentication, with routing, and the signup & login forms
5. Axios for requests and tokens
6. Logging out & blacklisting tokens
The completed code lives on GitHub here and you can just flip through branches to see the code at each step (1–1, 1–2, so on)
1–1) Setting up a Custom User in Django
First, make a fresh directory to hold our entire project.
$ mkdir django-jwt-react
$ cd django-jwt-react
Then create our virtual environment and install with
pipenv
.$ pipenv --python 3.7
$ pipenv install django djangorestframework djangorestframework-simplejwt
Note: You may see references online to a package called
djangorestframework-jwt
but it is no longer maintained. Use djangorestframework-simplejwt instead.Activate the virtual environment and create the Django project.
$ pipenv shell
$ django-admin startproject djsr
Now you should have the below in your
django-jwt-react/
directory.-django-jwt-react/
--djsr/
---djsr/
----__init__.py
----settings.py
----urls.py
----wsgi.py
---manage.py
--Pipfile
--Pipfile.lock
Most of the time you won’t be using the stock Django user in a project, you’ll be adding custom attributes such as favorite color. But Django doesn’t like it very much when we modify the User model after it has already been created in the database. To avoid those errors we first create our custom user and only then make and run database migrations. Django themselves recommend to do this.
This is a good time to create our Authentication app in Django.
$ python djsr/manage.py startapp authentication
And add it to your
INSTALLED_APPS
in settings.py
.# djsr/djsr/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'authentication'
]
Let’s create our custom user model in
authentication/models.py
and add the fav_color attribute, since we really care about colorful users. # djsr/authentication/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
fav_color = models.CharField(blank=True, max_length=120)
CustomUser
extends from AbstractUser
which gives us access to the standard Django User model attributes and functionalities such as username, password, and so on. We don’t need to add those to ours as a result. Nice.And adding it to authentication/admin.py with the most basic
ModelAdmin
.# djsr/authentication/admin.py
from django.contrib import admin
from .models import CustomUser
class CustomUserAdmin(admin.ModelAdmin):
model = CustomUser
admin.site.register(CustomUser, CustomUserAdmin)
Finally in
settings.py
we configure CustomUser
as our AUTH_USER_MODEL
.# djsr/djsr/settings.py
# ...
# Custom user model
AUTH_USER_MODEL = "authentication.CustomUser"
Now with our custom user setup, we can make and run migrations. While we’re at it, create a superuser too.
$ python djsr/manage.py makemigrations
$ python djsr/manage.py migrate
$ python djsr/manage.py createsuperuser
Cool. Now run the server.
$ python djsr/manage.py runserver
You should see the default Django success page.
Note: If you made your migrations before creating the CustomUser, you may have to delete and re-create the database.
GitHub code for section 1–1 lives here.
1–2) DRF serializers and auth
Great, now that your project is set up with a Custom User, we can use that custom user and Django Rest Framework + DRF Simple JWT to create Javascript Web Token based authentication. We’ve already got those installed.
This section will cover:
a. Configuring DRF + DRF Simple JWT
b. Authenticating and getting Refresh and Access tokens
c. Refreshing the tokens
d. Customizing the Obtain Token serializer and view to add extra context
e. Register a new user
f. Creating and testing a protected view
1–2a. Configuring DRF + DRF Simple JWT
To get the ball rolling, in
settings.py
let’s configure DRF and Simple JWT. Add “rest_framework”
to installed apps and the REST_FRAMEWORK
configuration dict. The Django Rest Framework Simple JWT package doesn’t need to be added to the INSTALLED_APPS
.# djsr/djsr/settings.py
# Needed for SIMPLE_JWT
from datetime import timedelta
# ...
INSTALLED_APPS = [
...
'rest_framework' # add rest_framework
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
), #
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=14),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUTH_HEADER_TYPES': ('JWT',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
}
By default we’ll only let authenticated viewers access our views, and they can authenticate using JWTAuthentication from the simplejwt package.
Configuring Simple JWT can get a little complicated. The key things to note here are that Refresh tokens (which last 14 days) are used to get Access tokens (which last 5 minutes). Only with a valid Access token can the user access a view, otherwise DRF will return a 401 unauthorized error. We rotate the Refresh tokens so that our users don’t have to log in again if they visit within 14 days, for ease of use. You can blacklist the tokens after rotating them, but we won’t cover it here.
If you’re not using the stock
user_id
, and are using something like the email address, then you’ll also want to change the USER_ID_FIELD
and USER_ID_CLAIM
to correspond to whatever the new user ID field is.Make special note of the “
AUTH_HEADER_TYPES
” as whatever value you put here must be reflected in React’s headers a bit later on. We set it as "JWT"
, but I’ve seen “Bearer”
used as well.No need to do migrations again.
1-2b. Authenticating and getting Refresh and Access tokens
We need to add the DRF Simple JWT URLs into our project to be able to test logging in, first.
# djsr/djsr/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('authentication.urls'))
]
And make a new
urls.py
in the authentication directory so that we can use the twin views supplied by DRF Simple JWT to obtain token pairs and refresh tokens.# djsr/authentication/urls.py
from django.urls import path
from rest_framework_simplejwt import views as jwt_views
urlpatterns = [
path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'), # override sjwt stock token
path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
]
Now use CURL with the superuser credentials you set earlier.
$ curl --header "Content-Type: application/json" -X POST http://127.0.0.1:8000/api/token/obtain/ --data '{"username":"djsr","password":"djsr"}'
{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMTg0OSwianRpIjoiYmE3OWUxZTEwOWJkNGU3NmI1YWZhNWQ5OTg5MTE0NjgiLCJ1c2VyX2lkIjoxfQ.S7tDJaaymUUNs74Gnt6dX2prIU_E8uqCPzMtd8Le0VI","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDEyNTQ5LCJqdGkiOiJiMmM0MjM4MzYyZjI0MTJhYTgyODJjMTMwNWU3ZTQwYiIsInVzZXJfaWQiOjF9.0ry66-v6SUxiewAPNmcpRt99D8B8bu-fgfqOCpVnN1k"}
Boom. Tokens. We’re authenticated! But wait. There are tokens plural. The Refresh token lasts 14 days (we can consider this logged in) but the Access token only lasts a mere 5 minutes. That means, whenever your user tries to access something without a valid Access token, it’ll get rejected, and then you need to send a refresh request from the frontend to the backend to get a new one. Let’s do that with CURL.
1–2c. Refreshing the tokens
Take the Refresh token from above and use CURL again:
$ curl --header "Content-Type: application/json" -X POST http://127.0.0.1:8000/api/token/refresh/ --data '{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMTg0OSwianRpIjoiYmE3OWUxZTEwOWJkNGU3NmI1YWZhNWQ5OTg5MTE0NjgiLCJ1c2VyX2lkIjoxfQ.S7tDJaaymUUNs74Gnt6dX2prIU_E8uqCPzMtd8Le0VI"}'
{"access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDEyOTQ0LCJqdGkiOiI1N2ZiZmI3ZGFhN2Y0MzkwYTZkYTc5NDhhMjdhMzMwMyIsInVzZXJfaWQiOjF9.9p-cXSn2uwwW2E0fX1FcOuIkYPcM85rUJvKBhypy1_c","refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMjI0NCwianRpIjoiYWUyZTNiNmRiNTI0NGUyNDliZjAyZTBiMWI3NTFmZjMiLCJ1c2VyX2lkIjoxfQ.peB-nzZRjzgMjcNASp1TZZ510p3lJt7N9SeCWUt0ngI"}
Look at that, new tokens! If you didn’t have
ROTATE_REFRESH_TOKENS:True
in settings.py
then the Refresh token would be the same, but since we are rotating, it’s also a fresh Refresh token. As long as the user keeps visiting before this expires, they'll never have to log in again.What makes up a JWT anyway? Head over to jwt.io and plug your token in. For our Refresh token above, after decoding you’ll see.
Header:
{
"typ": "JWT",
"alg": "HS256"
}
Payload:
{
"token_type": "refresh",
"exp": 1561622244,
"jti": "ae2e3b6db5244e249bf02e0b1b751ff3",
"user_id": 1
}
Note that token != jti. JTI is contained within the token, along with the type, expiration and any other info you put into it.
And the Access token holds similar information.
See how the payload includes the user_id? You can add any information you want with the token, you just have to modify the claim a bit, first.
1–2d. Customizing the Obtain Token Serializer and view
Earlier we added a fav_color attribute on our
CustomUser
model. First, go into the admin panel 127.0.0.1:8000/admin/
and choose a color. The DRF Simple JWT package makes it very easy to develop custom claims so that we can send our user’s favorite color in each token by importing and subclassing with the original serializer.
# djsr/authentication/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super(MyTokenObtainPairSerializer, cls).get_token(user)
# Add custom claims
token['fav_color'] = user.fav_color
return token
It needs a view to go along with it.
# djsr/authentication/views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import MyTokenObtainPairSerializer
class ObtainTokenPairWithColorView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
serializer_class = MyTokenObtainPairSerializer
Which needs a new entry in
urls.py
to replace the packaged one.# djsr/authentication/urls.py
from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from .views import ObtainTokenPairWithColorView
urlpatterns = [
path('token/obtain/', ObtainTokenPairWithColorView.as_view(), name='token_create'),
path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
]
The old Refresh token will still work to get new Access tokens, so at this point you should blacklist all outstanding tokens to effectively log everybody out.
To see the new token in action, use CURL again.
$ curl --header "Content-Type: application/json" -X POST http://127.0.0.1:8000/api/token/obtain/ --data '{"username":"djsr","password":"djsr"}'
{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyNjQ5NywianRpIjoiODVhMmRlNWUyNjQ0NGE1ZWFmOGQ1NDAzMmM1ODUxMzIiLCJ1c2VyX2lkIjoxLCJmYXZfY29sb3IiOiIifQ.1eJr6XVZXDm0nmm19tyu9WP9AfdY8Ny_D_tK4Qtvo9E","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDE3MTk3LCJqdGkiOiI5ZjE4NmM4OTQ0ZWI0NGYyYmNmYjZiMTQ5MzkyY2Y4YSIsInVzZXJfaWQiOjEsImZhdl9jb2xvciI6IiJ9.Ad2szXkTB4eOqnRk3GIcm1NDuNixZH3rNyf9RIePXCU"}
There you can see your fav color in the decoded token.
Cool. Why is this useful? Depends on your implementation, but it can be nice to have a little extra context along with the token, or to use the custom serializer/view to perform custom actions. This isn’t meant to act as any sort of
get_user_info()
function. Don't use it like that.Now we can log in an existing user and give them a token. We have two things left to do. Register a user and create a protected view.
1–2e. Registering a user
Surprisingly (or not) creating a new user has nothing at all to do with JWT. It’s just vanilla Django Rest Framework.
We don’t need to do anything to our
CustomUser
model, but we need to make a serializer for it, and put that in a view with a URL.First, the
CustomUserSerializer
model serializer. If you’re unfamiliar with Django Rest Framework, the serializers are essentially responsible for taking JSON and turning it into usable Python data structures, and then taking action with that. For more on serializers, take a look at the very good DRF docs. This serializer we’re using is SUPER typical.# djsr/authentication/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework import serializers
from .models import CustomUser
# ...
class CustomUserSerializer(serializers.ModelSerializer):
"""
Currently unused in preference of the below.
"""
email = serializers.EmailField(
required=True
)
username = serializers.CharField()
password = serializers.CharField(min_length=8, write_only=True)
class Meta:
model = CustomUser
fields = ('email', 'username', 'password')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
password = validated_data.pop('password', None)
instance = self.Meta.model(**validated_data) # as long as the fields are the same, we can just use this
if password is not None:
instance.set_password(password)
instance.save()
return instance
For our viewset, rather than use a
ModelViewSet
, we create our own view with just a POST endpoint. We would have a different endpoint for any GET requests for the CustomUser
objects. In
settings.py
since REST_FRAMEWORK’s permissions defaults are for views to be accessible to authenticated users only, we have to explicitly set the permissions to AllowAny, otherwise a new user trying to sign up and pay you would get an unauthorized error. Bad juju.When you feed data to a model serializer like we are doing here, as long as the serializer has a create() or update() method, you can use serializer.save() to magically create (or update) the corresponding object (in our case,
CustomUser
) and return the instance. Docs.# djsr/authentication/views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer
class ObtainTokenPairWithColorView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
class CustomUserCreate(APIView):
permission_classes = (permissions.AllowAny,)
def post(self, request, format='json'):
serializer = CustomUserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
if user:
json = serializer.data
return Response(json, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
And finally in
urls.py
we add the new view.# djsr/authentication/urls.py
from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from .views import ObtainTokenPairWithColorView, CustomUserCreate
urlpatterns = [
path('user/create/', CustomUserCreate.as_view(), name="create_user"),
path('token/obtain/', ObtainTokenPairWithColorView.as_view(), name='token_create'),
path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
]
View + Serializer + URL = good to go.
You’re going to have to make sure to test it at this point, since if this view is restricted to authenticated users only, you’ll get exactly 0 new users. They would be unhappy and your odds of getting an investor go way up when you have users. That means, more CURLing.
$ curl --header "Content-Type: application/json" -X POST http://127.0.0.1:8000/api/user/create/ --data '{"email":"[email protected]","username":"ichiro1","password":"konnichiwa"}'
{"email":"[email protected]","username":"ichiro1"}
Bingo! Works just fine.
The final step for us on the Django Rest Framework side is to create a protected view for us to try to access.
1–2f. Creating and testing a protected view
The only way for us to tell if all of this is working or not is to create a dummy view… let’s call it HelloWorld… and protect it as we try to access it with and without our Javascript Web Token Authentication Tokens.
Let’s just make the simplest possible view.
# djsr/authentication/views.py
...
class HelloWorldView(APIView):
def get(self, request):
return Response(data={"hello":"world"}, status=status.HTTP_200_OK)
And adding it to
urls.py
.# djsr/authentication/urls.py
from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from .views import ObtainTokenPairWithColorView, CustomUserCreate, HelloWorldView
urlpatterns = [
path('user/create/', CustomUserCreate.as_view(), name="create_user"),
path('token/obtain/', ObtainTokenPairWithColorView.as_view(), name='token_create'),
path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
path('hello/', HelloWorldView.as_view(), name='hello_world')
]
Back to CURL. If we did this right, our API request without the token will fail.
$ curl --header "Content-Type: application/json" -X GET http://127.0.0.1:8000/api/hello/
{"detail":"Authentication credentials were not provided."}
As expected. Now with the credentials. Make sure to refresh them first, odds are the Access token has expired. Or try with your new user.
$ curl --header "Content-Type: application/json" -X POST http://127.0.0.1:8000/api/token/obtain/ --data '{"username":"ichiro1","password":"konnichiwa"}'
{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYzODQxNiwianRpIjoiMGM5MjY5NWE0ZGQwNDUyNzk2YTM5NTY3ZDMyNTRkYzgiLCJ1c2VyX2lkIjoyLCJmYXZfY29sb3IiOiIifQ.sV6oNQjQkWw2F3NLMQh5VWWleIxB9OpmIFvI5TNsUjk","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k"}
$ curl --header "Content-Type: application/json" -X GET http://127.0.0.1:8000/api/hello/
{"detail":"Authentication credentials were not provided."}
$ curl --header "Content-Type: application/json" --header "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k" -X GET http://127.0.0.1:8000/api/hello/
{"hello":"world"}
First we logged new user Ichiro in, then tried a GET request on our protected endpoint. It’s still rejected, because we didn’t actually pass the token, even though we can see it with our naked eyes. It has to be passed in the header. This is why it’s key to remember that earlier in settings.py we set
“JWT”
in the AUTH_HEADER_TYPES
. In the header, the token MUST be preceeded by
“Authorization: JWT “ + access token
. Or whatever you set AUTH_HEADER_TYPES
as. Otherwise, no dice. This will become important when we connect the frontend.When we do that, as you can see, we were able to get the view’s response of
{"hello":"world"}
! Amazing. Now we can authenticate custom users using JWT Refresh and Access tokens, and let them access protected views only when transferring an Access token in the header.
What if you waited 5 minutes and tried to use an expired Access Token?
$ curl -Type: application/json" --header "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k" -X GET http://127.0.0.1:8000/api/hello/
{"detail":"Given token not valid for any token type","code":"token_not_valid","messages":[{"token_class":"AccessToken","token_type":"access","message":"Token is invalid or expired"}]}
In the Django server console you'll be able to see the magic:
Unauthorized: /api/hello/
[13/Jun/2019 12:53:29] "GET /api/hello/ HTTP/1.1" 401 183
The view would be inaccessible, and you’d have to refresh. It’s super tedious to do manually with CURL, but within a frontend framework, it’s easily automated.
Next we’ll continue and create a frontend using ReactJS to consume our API.
GitHub code for section 1–2 lives here.
Setting up a frontend framework to work with Django can be done any number of ways. They could be completely separate, only coming into contact over the API. In that case, local development would run a Django dev server and a separate React dev server, while deployment would involve a frontend deployed independently of the backend. React on AWS S3 for example, while Django is hosted on an EC2 server. It would require some fun CORS configurations.
On the other end of the spectrum, React could be intertwined more deeply within Django’s own templating system, so that Django handles serving the templates but you can still use React magic. It somewhat defeats the purpose of having a frontend framework in the first place.
This tutorial uses a middle approach — which is to install React inside a standalone Django app. CORS headers don’t need much work, and you get the full benefits of the React framework. There are definite caveats. The same server would be responsible for serving all the data, which could slow it down. URL routing is a little tricky, as well, if you want to have some Django URLs available, such as a health check or /admin. Initial setup as well is something of a pain. We’ll walk through it.
First thing to do is make a new Django app to hold React.
$ cd djsr
$ python manage.py startapp frontend
And add it to your
INSTALLED_APPS
in settings.py
.In frontend make a
templates/frontend/index.html
file, which will act as the base template for React along with a regular Django rendered index view. In this particular template, Django template context processors are available to use, which can be incredibly useful if you want to control how React behaves from within settings.py
. Let’s prepare it like a standard Django base template, for now. <!-- djsr/frontend/templates/frontend/index.html -->
<!DOCTYPE html>
<html>
{% load static %}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'frontend/style.css' %}">
<title>DRF + React = Winning the game</title>
</head>
<body>
<div id="root" class="content">
This will be the base template.
</div>
</body>
</html>
And make a minimal style.css while you’re at it.
// djsr/fontend/static/frontend/style.css
#root{
background-color:rebeccapurple;
}
We finish this with a view and updated URLs.
# djsr/djsr/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('authentication.urls')),
path('', include('frontend.urls'))
]
Make sure to put this include at the end of
urlpatterns
. This way, anything not matching Django URLs will be handled by the frontend, which lets us use React’s router to manage frontend views while still hosting it on the same server. Thus nicely avoiding CORS madness.# djsr/frontend/views.py
from django.shortcuts import render
# Create your views here.
def index(request):
return render(request, 'frontend/index.html', context=None)
This view renders the
index.html
that will be the base template for everything React. All it needs to do is render the template. Could add context if desired.Finally we add this view to the frontend URLs. We have to add it twice, first to catch the empty URL, e.g. https://lollipop.ai and second to catch every other URL e.g. https://lollipop.ai/lollisignup/.
# djsr/frontend/urls.py
urlpatterns = [
path('', index_view), # for the empty url
url(r'^.*/$', index_view) # for all other urls
]
With that final touch, run the server again and see if the index renders correctly.
$ cd ../.. (so you're in project root: django-jwt-react)
$ python djsr/manage.py runserver
Navigate to http://127.0.0.1/ and see this beautiful mostly empty page rendered succesfully. And you’ll see the same at http://127.0.0.1:8000/asdjfklasdjfklasdfjklasdf/ thus showing us we didn’t mess up. Not yet, at least. Cool. Now we can start playing with React.
Note that the regex ends with /$. Django by default appends a / to the end of each URL, so when we do routing in React, each path will need a / at the end of it. This helps enforce that discipline.
Now we can get to the dirty business of setting React up in the frontend app. This isn’t a straightforward task, we cannot just rely on Create React App.
Instead, we create our own toolchain, adapting the great tutorial by Jedai Saboteur.
Make sure you’re at the root folder of the app (with the Pipfile). First create the
package.json
file. The answers to the questions here don’t matter much, so just leave them at pretty much the default.$ npm init
Now within the
frontend
Django app, create a src
directory. This will hold our React components. Within the static/frontend
directory create another directory called public to hold the compiled React files.Now the Django app directory should look like this:
djsr
+-- authentication/
+-- djsr/
+-- frontend/
| +-- migrations/
| +-- src/
| +-- static/
| | +-- frontend/
| | | +-- public/
| | | +-- style.css
| +-- templates/
| | +-- frontend/
| | | +-- index.html
+--db.sqlite3
+--manage.py
Open up
index.html
and add this kinda awkward line to the bottom of the body. We can still use Django’s templating magic in this html file.<script type="text/javascript" src="{% static 'frontend/public/main.js' %}"></script>
When compiled, the React components we make will be contained in
main.js
. You can rename this file to whatever should you so desire.To make that work even better in production, you should split main.js into bundles if it gets too fat.
This line is the key intersection between Django and React for us. With our Django-served index.html loading main.js we can serve any awesome frikken React wizardry we’d like. And we’d like to do a lot of that wizardry indeed.
With our
index.html
ready and waiting, we have to figure out how to create and compile something for it to grab and present. This requires Babel and Webpack.
Babel
Install
@babel/core
, @babel/preset-env
, @babel-preset-react
$ npm install --save-dev @babel/core@7.4.5 @babel/preset-env@7.4.5 @babel/preset-react@7.0.0
Babel takes the code we write in whatever we use and turns it into JavaScript browsers love.
babel/preset-env
is for modern Javascript ES6+ and babel/preset-react
for JSX. If you are coming from the Angular world and want to use TypeScript (and why wouldn’t you? It’s great) install babel/preset-typescript
instead. We’ll stay away from TypeScript in this tutorial. To use our freshly installed Babel, we must create a
.babelrc
file in the project root, next to package.json
.{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
Not really any way to test this quite yet.
Webpack
Webpack is a bundler. It takes our modules (dependencies and custom code both) and converts them into static assets. This infographic from their website https://webpack.js.org does a good job of illustrating how it works.
This is what will take our various React components and (after Babel has had its way with the JS(X)) will turn it all into nice static files to load as browsers are used to.
Let’s install it and a few more packages that enable us to use it with Babel.
$ npm install --save-dev webpack webpack-cli babel-loader
+ babel-loader@8.0.6
+ webpack-cli@3.3.4
+ webpack@4.35.0
We are going to let Django do CSS loading within
index.html
, but if you wish, Webpack is more than capable of taking over that loading functionality as well. It can also provide a dev server, but Django can handle that nearly as well. It just won’t detect changes to the src files (which to be fair can get quite tedious).Let’s create our Webpack configuration file
webpack.config.js
at the root, next to packages.json
.const path = require('path');
module.exports = {
mode: "development",
entry: path.resolve(__dirname, 'djsr/frontend/src/index.js'),
output: {
// options related to how webpack emits results
// where compiled files go
path: path.resolve(__dirname, "djsr/frontend/static/frontend/public/"),
// 127.0.0.1/static/frontend/public/ where files are served from
publicPath: "/static/frontend/public/",
filename: 'main.js', // the same one we import in index.html
},
module: {
// configuration regarding modules
rules: [
{
// regex test for js and jsx files
test: /\.(js|jsx)?$/,
// don't look in the node_modules/ folder
exclude: /node_modules/,
// for matching files, use the babel-loader
use: {
loader: "babel-loader",
options: {presets: ["@babel/env"]}
},
}
],
},
};
Looking at the exported configuration object, we see a few things that are important to note.
The entry point is where Webpack will find the start of our React app and bundle from there.
We have to use absolute paths for Webpack, and the path module makes that very easy when we use them in the entry and output settings.
Setting mode to “development” versus “production” and comes along with some optimizations that help either way. Stick to development for now.
Within output, there are 3 relevant settings. path is the location where files are emitted after compilation.
publicPath
is essentially setting the location where the application will find the React static files. In development, just set it to where they’re emitted. In production using a CDN, the pattern here would be STATIC_PATH/{{path after emitting}}
where STATIC_PATH is wherever your Django project saves static files after running collectstatic
. filename is of course the name of the file emitted after Webpack is done compiling it.Since we’re letting Django handle a lot of the loading duties, we only use one rule within the module setting. For all
.js
or .jsx
files found after entry, the files will be loaded and transformed by babel-loader. We explicitly exclude the /node_modules/
folder from being tested. It would be worse than committing it to Git! If you’re coming from a Django development background, you are probably quite shocked by all this configuring before we even get to do ANY React coding. Yeah, it’s a lot. It’ll be worth it!
We have finally reached the point where we can install React.
React
$ npm install --save react react-dom
+ react@16.8.6
+ react-dom@16.8.6
With React installed we can finally make that
index.js
we referenced in webpack.config.js
. This is the beginning of our React app, and for now will just import .// djsr/frontend/src/index.js
import React from 'react'
import {render} from 'react-dom'
import App from './components/App';
render(<App />, document.getElementById('root'));
See the
getElementByID(‘root’)
part? That renders the App in place of the #root div we made in index.html
. It used to say “This will be the base template” but after we’re done here — very soon — we’ll see something else.We’re forward thinking people, so create a
components/
directory and our App.js
within it.// djsr/frontend/src/components/App.js
import React, { Component} from "react";
class App extends Component{
render(){
return(
<div className="site">
<h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1>
</div>
);
}
}
export default App;
This is our first React component! Awesome. Note that React uses
className
rather than class. Since we aren’t using the Webpack development server, we have to compile manually a different way. Let’s be lazy about this and make it a script. Open up
package.json
and add ”build”: “webpack — config webpack.config.js”
.// package.json
{
"name": "django-jwt-react",
"version": "1.0.0",
"description": "To learn how to merge React and Django",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"test": "test"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.6",
"webpack": "^4.35.0",
"webpack-cli": "^3.3.4"
},
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}
And finally.
$ npm run build
If it worked, take a look at
djsr/frontend/public/
. Is there a main.js
file there? There should be.If we run Django, we should see the new line of text. Let’s test that.
$ python djsr/manage.py runserver
That wasn’t simple nor easy, but it worked. Django is now serving our React app, which is living happily in our frontend app. Way to keep up this far.
GitHub code for section 2–1 lives here.
Our end goal is to authenticate with React and Django. We’ve already made all the endpoints we need to on the Django side. Before we write the authentication bits on the frontend, we have to do some legwork. Namely: Routing for components, setting up Axios, and making our form views.
We’re going to skip over a lot of the complexities about client-side routing in React for now. I may expand upon it in a later article if there’s demand, but there are some really good resources out there already, although this may be one of the first that touches React Router 5.
Routing
Install
. We use the -dom variant since we’re building a website, not an app. For the curious making a React Native app, you’d use react-router-native.react-router-dom
$ npm install --save react-router-dom
+ react-router-dom@5.0.1
With react-router-dom 4 & 5, for projects that’ll be used through a browser like our own, we have 2 kinds of browsers available to us.
BrowserRouter
and HashRouter
. Based on the history API, BrowserRouter
is the preferred choice when the React app is a Single-Page App (index.html
served each time by the server) or is backed by a dynamic server to handle all requests; urls are formatted without a hash # — www.lollipop.ai/bananaphone/. HashRouter urls are formatted with a /#/ — www.lollipop.ai/#/bananaphone/. HashRouter
is more commonly used when the server only serves static pages and static assets. Thanks to Django, we have a dynamic server, so we can use the BrowserRouter
. We need to add it to
index.js
as well, wrapping our App like so, since render only expects to receive 1 component.// djsr/frontend/src/index.js
import React from 'react'
import {render} from 'react-dom'
import {BrowserRouter} from 'react-router-dom'
import App from './components/App';
render((
<BrowserRouter>
<App />
</BrowserRouter>
), document.getElementById('root'));
With the
BrowserRouter
imported and index.js
updated to use it, the next step is to create another couple components we can render depending on the URLs.Make 2 new files:
login.js
and signup.js
in components/
. // djsr/frontend/components/login.js
import React, { Component } from "react";
class Login extends Component{
constructor(props){
super(props);
}
render() {
return (
<div>
<h2>Login page</h2>
</div>
)
}
}
export default Login;
And:
// djsr/frontend/components/signup.js
import React, { Component } from "react";
class Signup extends Component{
constructor(props){
super(props);
}
render() {
return (
<div>
<h2>Signup page</h2>
</div>
)
}
}
export default Signup;
Pretty much the simplest React components you can write. It has its constructor, renders some HTML, and exports itself.
With our components ready, let’s import Switch, Route, and Link, which we’ll need .
// djsr/frontend/src/components/App.js
import React, { Component} from "react";
import { Switch, Route, Link } from "react-router-dom";
import Login from "./login";
import Signup from "./signup";
class App extends Component {
render() {
return (
<div className="site">
<main>
<h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1>
<Switch>
<Route exact path={"/login/"} component={Login}/>
<Route exact path={"/signup/"} component={Signup}/>
<Route path={"/"} render={() => <div>Home again</div>}/>
</Switch>
</main>
</div>
);
}
}
export default App;
Using the <main> HTML element is a good habit, so we add that for our main content. Within that we add a <Switch> block which lets React know that in that space we will switch rendered components depending on the defined routes.
URL routing in React Switches are very similar to Django’s path syntax. Through the use of the
exact
property, when the URL path is matched exactly, the relevant component is rendered. All other paths go to home (or better yet, a future 404 page). With this, we should be able to test the URLs by typing them into the address bar in our browser.
Navigate to http://127.0.0.1:8000/signup and see what you have there.
Looks like it’s working. Man, that's ugly. You can build better looking websites right?
It’s tedious to type a URL each time we want to test it, so let’s add a couple links to App.js to save us some keystrokes.
// djsr/frontend/src/components/App.js
import React, { Component} from "react";
import { Switch, Route, Link } from "react-router-dom";
import Login from "./login";
import Signup from "./signup";
class App extends Component {
render() {
return (
<div className="site">
<nav>
<Link className={"nav-link"} to={"/"}>Home</Link>
<Link className={"nav-link"} to={"/login/"}>Login</Link>
<Link className={"nav-link"} to={"/signup/"}>Signup</Link>
</nav>
<main>
<h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1>
<Switch>
<Route exact path={"/login/"} component={Login}/>
<Route exact path={"/signup/"} component={Signup}/>
<Route path={"/"} render={() => <div>Home again</div>}/>
</Switch>
</main>
</div>
);
}
}
export default App;
Still using good habits, we of course put it all in a <nav> element. In React, don’t use
, use React router’s <a href=””>
component, which we have to for React router to work correctly with the history API. For now, add the class <Link>
“nav-link”
for some bare-minimum "styling."/* djsr/frontend/static/frontend/style.css */
#root{
background-color:rebeccapurple;
color:white;
}
.nav-link{
color:white;
border: 1px solid white;
padding: 1em;
}
Yes, everything is still incredibly ugly. Maybe I should do something about that later.
First, confirm the
<Link>
s do what they’re supposed to do.$ npm run build
$ python djsr/manage.py runserver
Looks good again. Routing complete! Let’s flesh out those forms.
Forms
Two forms are needed for Login and Signup. Signup has username, email, and password fields while login only needs username and password. I really love how React makes forms so friendly to both developers and users.
I started this form by ripping the form out of React’s own docs. It’s got almost everything we need to start.
// djsr/frontend/src/components/login.js
import React, { Component } from "react";
class Login extends Component {
constructor(props) {
super(props);
this.state = {username: "", password: ""};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handleSubmit(event) {
alert('A username and password was submitted: ' + this.state.username + " " + this.state.password);
event.preventDefault();
}
render() {
return (
<div>Login
<form onSubmit={this.handleSubmit}>
<label>
Username:
<input name="username" type="text" value={this.state.username} onChange={this.handleChange}/>
</label>
<label>
Password:
<input name="password" type="password" value={this.state.password} onChange={this.handleChange}/>
</label>
<input type="submit" value="Submit"/>
</form>
</div>
)
}
}
export default Login;
First, look at the HTML of the form. When the form is submitted, it triggers the onSubmit method, which we have written as
handleSubmit
. This method will handle what we want it to do after the user submits the form. In this case, what we want it to do is alert us with the data that has been input. Normally a submission would trigger a reload or redirect, so by adding in preventDefault()
that unwanted behavior can be stopped.To the two other input fields (username, password) we set the onChange property to a method called
handleChange
. Whenever the content of that input field changes, when a keystroke occurs, it triggers the handleChange method to do what we want it to do, which is to update the local component state to match the entered text value for each input field using setState
.If you aren’t careful, you could end up with a handleSomething method for each input field. However, since we want to stick to DRY principles, we can cleverly just give each input a name, and ensure thelocal component state matches that name. With that, we can get away with only one
handleChange
method, using one line of code. Not exactly a Python one-liner, but it scratches that itch.It’s super important to bind
this
to each class method in the constructor, or else this will be undefined in the callback. Very annoying to forget. The updated signup page is essentially the same, but with a bonus email input field (and matching component state).
// djsr/frontend/src/components/signup.js
import React, { Component } from "react";
class Signup extends Component{
constructor(props){
super(props);
this.state = {
username: "",
password: "",
email:""
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handleSubmit(event) {
alert('A username and password was submitted: ' + this.state.username + " " + this.state.password + " " + this.state.email);
event.preventDefault();
}
render() {
return (
<div>
Signup
<form onSubmit={this.handleSubmit}>
<label>
Username:
<input name="username" type="text" value={this.state.username} onChange={this.handleChange}/>
</label>
<label>
Email:
<input name="email" type="email" value={this.state.email} onChange={this.handleChange}/>
</label>
<label>
Password:
<input name="password" type="password" value={this.state.password} onChange={this.handleChange}/>
</label>
<input type="submit" value="Submit"/>
</form>
</div>
)
}
}
export default Signup;
Test it by building and running the Django server, plus a refresh.
$ npm run build
$ pythong djsr/manage.py runserver
If you’ve been keeping up, there should be a super attractive alert, and maybe even a warning from your browser asking if you want it to block this site from displaying alerts.
Thanks Firefox for protecting me from this annoying signup page.
Ok, so our React frontend can now handle routing and we can use our signup and login forms — kinda.
This next part is my least favorite. Axios.
GitHub code for section 2–2 lives here.
Within Javascript circles, there are two primary ways to GET/POST/UPDATE/DELETE/whatever data from/to a REST API: Axios and JavaScript’s included Fetch. Personally, I have had more success with Axios. Both use Promises, and Internet Explorer doesn’t like either of them (but it seems to like Fetch a teeny bit less). I would like to explore GraphQL later on in a followup article.
We want to use Axios for:
POSTing to /api/user/create/ to create a userPOSTing to /api/token/obtain/ to login a user and obtain a JWT token pairPOSTing to /api/token/refresh/ to refresh the JWT token pairGETting from the protected /api/hello/ to see what the backend secretly has to say
Scroll back up and take a look at our CURL commands. To access the protected view, the JWT token has to be sent in the header. Axios will need to take care of that in addition to sending along the POSTed data. Whenever the JWT access token expires, we don’t want to make the user get a new one manually, since it expires every 5 minutes. That user would run away. Axios will have to handle acquiring a new token automatically, as well.
It’s a tall task.
First step — install Axios:
$ npm install --save axios
+ axios@0.19.0
Axios has two superpowers useful here. First, we can create a standalone instance with custom configurations that can be used throughout the website — this is where we’ll set it to send JWT headers. Second, we can make a custom interceptor for the new Axios instance. The interceptor lets us “do stuff” while handling the request — this is where we’ll handle refreshing tokens.
Create a file within
src/
called axiosApi.js
. // djsr/frontend/src/axiosApi.js
import axios from 'axios'
const axiosInstance = axios.create({
baseURL: 'http://127.0.0.1:8000/api/',
timeout: 5000,
headers: {
'Authorization': "JWT " + localStorage.getItem('access_token'),
'Content-Type': 'application/json',
'accept': 'application/json'
}
});
To create an Axios instance, import it and use the
.create()
method with the custom configuration to set the defaults. Set the baseURL
to where the backend API is served from. The headers are important. In settings.py
the SIMPLE_JWT
dict sets the AUTH_HEADER_TYPES
as ‘JWT’
so for the Authorization header here it has to be the same. Don’t neglect to add the space after JWT in axiosAPI.js. Also do NOT a space in settings.py. Same same, but different, but still same.
Each time Axios gets a token, it stores the access_token in local storage. We initiate the creation of the Axios instance by getting that token. If there’s no token in local storage, don’t even worry about it for the header. It will be set every time a user logs in.
To test anything, we still have to write the methods to login and signup. Then we do still need to tackle logging out as well. Can’t forget that!
Logging in
Within our original
login.js
file, we can now improve handleSubmit
to POST to the Django backend’s token creation endpoint /api/token/obtain/
and get a token pair. // djsr/frontend/src/components/login.js
import React, { Component } from "react";
import axiosInstance from "../axiosApi";
class Login extends Component {
constructor(props) {
super(props);
this.state = {username: "", password: ""};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handleSubmit(event) {
event.preventDefault();
try {
const response = axiosInstance.post('/token/obtain/', {
username: this.state.username,
password: this.state.password
});
axiosInstance.defaults.headers['Authorization'] = "JWT " + response.data.access;
localStorage.setItem('access_token', response.data.access);
localStorage.setItem('refresh_token', response.data.refresh);
return data;
} catch (error) {
throw error;
}
}
render() {
return (
<div>
Login
<form onSubmit={this.handleSubmit}>
<label>
Username:
<input name="username" type="text" value={this.state.username} onChange={this.handleChange}/>
</label>
<label>
Password:
<input name="password" type="password" value={this.state.password} onChange={this.handleChange}/>
</label>
<input type="submit" value="Submit"/>
</form>
</div>
)
}
}
export default Login;
Very simple upgrade. Our prior version of the view already set the username and password in state, so now all that is left to do is to import the custom Axios instance, and post the username and password. If it works, the response will contain a pair of brand new JWT tokens. Since access to views is determined by the access token, that’s the one that needs to go in every header. We want to save both of them to local storage, however. Why? We’ll get to that.
Run the standard routine, and then navigate to the
/login/
endpoint and try to login:$ npm run build
$ python djsr/manage.py runserver
Django says all OK.
Firefox got an OK, too.
Oh hey look a token pair!!!!!!!!
Testing a protected view
Token pair in hand. Can we see the protected view yet?
Nope. Nothing in React actually tries to get that info yet. Time for another React component.
Make a file called
hello.js
in the components folder. Within this component we want to do only 1 thing: GET a message from a protected API endpoint on the backend, and display it. We will use our custom Axios instance again.There are a couple tricky parts.
// djsr/frontend/src/components/hello.js
import React, { Component } from "react";
import axiosInstance from "../axiosApi";
class Hello extends Component {
constructor(props) {
super(props);
this.state = {
message:"",
};
this.getMessage = this.getMessage.bind(this)
}
getMessage(){
try {
let response = axiosInstance.get('/hello/');
const message = response.data.hello;
this.setState({
message: message,
});
return message;
}catch(error){
console.log("Error: ", JSON.stringify(error, null, 4));
throw error;
}
}
componentDidMount(){
// It's not the most straightforward thing to run an async method in componentDidMount
// Version 1 - no async: Console.log will output something undefined.
const messageData1 = this.getMessage();
console.log("messageData1: ", JSON.stringify(messageData1, null, 4));
}
render(){
return (
<div>
<p>{this.state.message}</p>
</div>
)
}
}
export default Hello;
By now you should be familiar with the contents of the
constructor()
method. We set message in the state
, and later on in render
, we render whatever the message is. Without going into too much detail about the React lifecycle, it’s intuitive enough just to say that when the component is mounted/loaded, the
componentDidMount
method is fired. Since we want to get this message RIGHT AWAY, this is the right place to trigger our GET request. Just, first, gotta add this component to the Navbar and Switch in
App.js
. No big deal.// djsr/frontend/src/components/App.js
import React, { Component} from "react";
import { Switch, Route, Link } from "react-router-dom";
import Login from "./login";
import Signup from "./signup";
import Hello from "./hello";
class App extends Component {
render() {
return (
<div className="site">
<nav>
<Link className={"nav-link"} to={"/"}>Home</Link>
<Link className={"nav-link"} to={"/login/"}>Login</Link>
<Link className={"nav-link"} to={"/signup/"}>Signup</Link>
<Link className={"nav-link"} to={"/hello/"}>Hello</Link>
</nav>
<main>
<h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1>
<Switch>
<Route exact path={"/login/"} component={Login}/>
<Route exact path={"/signup/"} component={Signup}/>
<Route exact path={"/hello/"} component={Hello}/>
<Route path={"/"} render={() => <div>Home again</div>}/>
</Switch>
</main>
</div>
);
}
}
export default App;
Build and recompile the server. Navigate to
/hello/
and see what happens.It doesn’t work!
Number 1 — the access token has expired, so there’s a 401 error.
Number 2 — React tried to assign
message = response.data.hello
without waiting for the response.Either one of those would have taken down the site. You can confirm that #2 would break it even with a fresh access token by logging in again. Or, actually, not.
This isn’t the first time that we tried to assign a value before waiting for the response. We couldn’t see it at the time since it didn’t outright break anything, but earlier in
login.js
, although we POSTed and got the token and can see it in the console, it wasn’t actually getting set to localStorage or the header of the axiosInstance.Change
getMessage()
to see how the access_token
in localStorage
is logged as undefined.// djsr/frontend/src/components/hello.js
...
getMessage(){
try {
const header = localStorage.getItem("access_token");
console.log(header);
// let response = axiosInstance.get('/hello/');
// const message = response.data.hello;
// this.setState({
// message: message,
// });
// return message;
}catch(error){
console.log("Error: ", JSON.stringify(error, null, 4));
throw error;
}
}
...
With no token set in the headers, you can’t test #1 either.
How do we fix that?
Async/Await & .then
We need to tell React to wait for a response before continuing along through a method, assigning values that haven’t been returned in a response yet.
You can add a
.then()
callback function using promises, which is the other way to do it. It would look something like this:// djsr/frontend/src/components/login.js
class Login extends Component {
constructor(props) {
...
this.handleSubmitWThen = this.handleSubmitWThen.bind(this);
}
...
handleSubmitWThen(event){
event.preventDefault();
axiosInstance.post('/token/obtain/', {
username: this.state.username,
password: this.state.password
}).then(
result => {
axiosInstance.defaults.headers['Authorization'] = "JWT " + result.data.access;
localStorage.setItem('access_token', result.data.access);
localStorage.setItem('refresh_token', result.data.refresh);
}
).catch (error => {
throw error;
})
}
render() {
return (
<div>
Login
<form onSubmit={this.handleSubmitWThen}>
...
</form>
</div>
)
}
}
This code works, so you can totally ignore the following ES7 async/await version if you so choose. You could also use try to use React hooks, but they seem to still be not entirely ready as of June 2019.
Sidenote:,.then()
, and.catch()
do work together pretty well. I just prefer putting everything is standard.finally()
blocks.try/catch/finally
Let’s use the newer ES7 async/await with try/catch/finally blocks instead to stay on top of things. They work great, normally. However, as always, there’s another caveat.
We have to configure webpack.
But first, the async handleSubmit.
// djsr/frontend/src/components/login.js
...
async handleSubmit(event) {
event.preventDefault();
try {
const data = await axiosInstance.post('/token/obtain/', {
username: this.state.username,
password: this.state.password
});
axiosInstance.defaults.headers['Authorization'] = "JWT " + data.access;
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
return data;
} catch (error) {
throw error;
}
}
...
render() {
return (
<div>
Login
<form onSubmit={this.handleSubmit}>
...
</form>
</div>
)
}
}
Just need to add
async
at the beginning when declaring the method, and at the part you want the code to wait, add an await
. It’s that easy. The rest of the code can be written like it’s synchronous, and that includes errors. Very clean. That cleanliness makes it my personal preference. Don’t neglect to change the onSubmit
back to handleSubmit
.
Babel again
Since we’re using a custom Webpack configuration, using async/await won’t work without some additional elbow grease. Right now, the console will show us a big red
ReferenceError: regeneratorRuntime is not defined
error and React will break. Googling around will lead you to various StackOverflow Q&As leading to the answer. Just add babel-polyfill
to the entry line of webpack.config.js
or import it. We had better install that package.$ npm install --save-dev babel-polyfill
+ babel-polyfill@6.26.0
The cleanest way is to just add that as an entry point. So let’s do that.
// webpack.config.js
const path = require('path');
module.exports = {
mode: "development",
entry: ['babel-polyfill', path.resolve(__dirname, 'djsr/frontend/src/index.js')],
...
}
Yes, you can have multiple entries for entry. Just make sure the final entry in the list is for the actual
index.js
file.Ok. So now we should be able to use Async/await in our components.
Build it and run the server.
$ npm run build
$ python djsr/manage.py runserver
Login again and navigate to
/hello/
and check the console — we should see that it now runs succesfully with the async/await method working on login. Cool.
GETting from a Protected API endpoint
With async working with Login.js, storing the token properly and including it in the headers, it can now be used as authentication for accessing protected API endpoints in Django.
Finally. All this work to get right here.
hello.js
is nearly ready. Set free the commented-out lines and add async + await.// djsr/frontend/src/components/hello.js
...
async getMessage(){
try {
let response = await axiosInstance.get('/hello/');
const message = response.data.hello;
this.setState({
message: message,
});
return message;
}catch(error){
console.log("Error: ", JSON.stringify(error, null, 4));
throw error;
}
}
...
Test it out.
Unless you test
/hello/
within 5 minutes of logging in, you’ll get a 401 Unauthorized rejection. You can verify it in the Django console or the browser console. Unauthorized: /api/hello/
"GET /api/hello/ HTTP/1.1" 401 183
It’s a bit vague, but this 401 is due to the access token we’re sending along with the request exceeding its short 5 minute lifespan. DRF rejects the request with a 401 error.
It's kind of like forgetting the water is boiling for spaghetti, and you forget, and then the water has completely evaporated and you have to start all over again.
Login one more time, get a fresh token, and you’ll see the message “world” load.
Should a user have to login every 5 minutes? No way. Let’s fix that.
Axios interceptor
With instance and refresh token in hand, let’s add the interceptor. It isn’t as robust as I would like, so if anybody can suggest improvements, please drop them in the comments.
// djsr/frontend/src/axiosApi.js
import axios from 'axios'
const axiosInstance = axios.create({
baseURL: 'http://127.0.0.1:8000/api/',
timeout: 5000,
headers: {
'Authorization': "JWT " + localStorage.getItem('access_token'),
'Content-Type': 'application/json',
'accept': 'application/json'
}
});
axiosInstance.interceptors.response.use(
response => response,
error => {
const originalRequest = error.config;
if (error.response.status === 401 && error.response.statusText === "Unauthorized") {
const refresh_token = localStorage.getItem('refresh_token');
return axiosInstance
.post('/token/refresh/', {refresh: refresh_token})
.then((response) => {
localStorage.setItem('access_token', response.data.access);
localStorage.setItem('refresh_token', response.data.refresh);
axiosInstance.defaults.headers['Authorization'] = "JWT " + response.data.access;
originalRequest.headers['Authorization'] = "JWT " + response.data.access;
return axiosInstance(originalRequest);
})
.catch(err => {
console.log(err)
});
}
return Promise.reject(error);
}
);
export default axiosInstance
Every single time a request is made with the Axios instance we created earlier, if there are no errors, it just works as expected.
When there are errors, first we get the configuration from the errant request to use later. Then, we must see what kind of error code is being sent along with it. Django Rest Framework sends a 401 Unauthorized error status when a user isn’t authorized to view a protected view.
If the error is a 401, we need to refresh it, which means we need to have the refresh token handy to get a new token pair. POST the token with the custom AxiosInstance and barring any surprises, we’ll have a pair of refreshed tokens to save to localStorage.
New tokens in hand, send on the original request, and it should work. In theory.
Build it and refresh
/hello/
to see. Take a look at what happens in the Django dev server console:Unauthorized: /api/hello/
"GET /api/hello/ HTTP/1.1" 401 183
"POST /api/token/refresh/ HTTP/1.1" 200 491
"GET /api/hello/ HTTP/1.1" 200 17
Note that sometimes I have had problems using the interceptor method when the request itself has errors, and it results in a bit of a loop.
There you have it. Users can login and access protected data with their access token getting refreshed each time it expires. Not easy.
Signup
Let’s flesh out Signup with a functional
handleSubmit()
method to actually talk to the backend. It’s very similar to the Login component.// djsr/frontend/src/components/signup.js
...
async handleSubmit(event) {
event.preventDefault();
try {
const response = await axiosInstance.post('/user/create/', {
username: this.state.username,
email: this.state.email,
password: this.state.password
});
return response;
} catch (error) {
console.log(error.stack);
}
}
...
This seems to work. Now, try to sign up again in a fresh browser or clear your cookies. Private window may even work. Pay attention to the dev server console.
Unauthorized: /api/user/create/
"POST /api/user/create/ HTTP/1.1" 401 183
Bad Request: /api/token/refresh/
"POST /api/token/refresh/ HTTP/1.1" 400 43
But wait, we set
permission_classes
to AllowAny
for the CustomUserCreate
view. Shouldn’t it work? It worked fine for CURL.# djsr/authentication/views.py
class CustomUserCreate(APIView):
permission_classes = (permissions.AllowAny,)
def post(self, request, format='json'):
serializer = CustomUserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
if user:
json = serializer.data
return Response(json, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Turns out, this
CustomUserCreate
view was missing something.# djsr/authentication/views.py
class CustomUserCreate(APIView):
permission_classes = (permissions.AllowAny,)
authentication_classes = ()
def post(self, request, format='json'):
serializer = CustomUserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
if user:
json = serializer.data
return Response(json, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
We need to specify an empty list or tuple for authentication_classes in addition to setting permission_classes to convince DRF to open up a view to the public. Try again. No more 401 Unauthorized error.
Errors
Speaking of errors, how can we handle errors from DRF?
At a minimum we should present the field errors for the Signup component. The quickest way to do that is to combine
setState
with some ternary operators within the rendered HTML. // djsr/frontend/src/components/signup.js
...
async handleSubmit(event) {
event.preventDefault();
try {
const response = await axiosInstance.post('/user/create/', {
username: this.state.username,
email: this.state.email,
password: this.state.password
});
return response;
} catch (error) {
console.log(error.stack);
this.setState({
errors:error.response.data
});
}
}
render() {
return (
<div>
Signup
<form onSubmit={this.handleSubmit}>
<label>
Username:
<input name="username" type="text" value={this.state.username} onChange={this.handleChange}/>
{ this.state.errors.username ? this.state.errors.username : null}
</label>
<label>
Email:
<input name="email" type="email" value={this.state.email} onChange={this.handleChange}/>
{ this.state.errors.email ? this.state.errors.email : null}
</label>
<label>
Password:
<input name="password" type="password" value={this.state.password} onChange={this.handleChange}/>
{ this.state.errors.password ? this.state.errors.password : null}
</label>
<input type="submit" value="Submit"/>
</form>
</div>
)
}
...
For a properly made DRF API View, when encountering errors, it will return those errors in JSON form in the response. We log the error and set the state directly to the JSON object containing the error messages.
Every time the state is set, it triggers a re-render of the component — in this case that would render the error messages.
{ this.state.errors.password ? this.state.errors.password : null}
Ternary operators like this are done in this format:
{ Boolean ? (content to show if True) : (content to show if False)
So if a password doesn’t pass the password validator on the backed Serializer, it would send an error which we store in the state at state.errors.password and if that is present, it shows the error text. Otherwise it shows nothing.
Question for the readers: Is there a different way to build forms with error handling based on some kind of formset for Django and React? Right now it’s quite a manual approach, isn’t it?
Now that we can login, signup, get and refresh tokens, handle basic errors and view content with proper authorization. It’s time for the final step: Logging Out.
GitHub code for section 2–3 lives here.
According to the JWT documentation, there isn’t really a way to log out in the conventional sense. The tokens cannot be forced to expire before their time, and if you delete the token in a users’s localStorage, but they somehow come up with a token that hasn’t yet expired(even that same one), they are able once again to access the website as a logged-in user.
There are a few ways to tackle logging out.
Just delete the tokens in localStorage1 + blacklist that token1 + blacklist all tokens for the user
Deleting the tokens in localStorage alone means every token is still valid to use on the site.
The way we have set the project up for this tutorial, each time a user needs to use their refresh token to get a fresh access token, they are issued a fresh PAIR of tokens. A new refresh token with a new validity included, and the old pair is . The number of tokens per user will grow very quickly, so blacklisting whatever token is currently in use doesn’t do much more than merely deleting the token... Unless of course you blacklist each old token after rotation, which you should.
Finally, blacklisting every single token for a user would force them to login again on every single device, not just the device they’re currently logged into.
What to do?
Let’s tackle option 2 as an exercise. First, take a look at settings.py. Ah, it looks like we are NOT blacklisting after rotation. Better change that by adding the blacklist app from django-rest-framework-simplejwt and setting
BLACKLIST_AFTER_ROTATION
to True.# djsr/djsr/settings.py
INSTALLED_APPS = (
...
'rest_framework_simplejwt.token_blacklist',
...
}
...
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=14),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUTH_HEADER_TYPES': ('JWT',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
}
The blacklist relies on saving blacklisted tokens in the database, so you know you have a migration to run.
$ python djsr/manage.py migrate
Now each time a token pair gets refreshed, the old ones get blacklisted.
First, an API view for blacking out tokens:
# djsr/authentication/views.py
...
from rest_framework_simplejwt.tokens import RefreshToken
...
class LogoutAndBlacklistRefreshTokenForUserView(APIView):
permission_classes = (permissions.AllowAny,)
authentication_classes = ()
def post(self, request):
try:
refresh_token = request.data["refresh_token"]
token = RefreshToken(refresh_token)
token.blacklist()
return Response(status=status.HTTP_205_RESET_CONTENT)
except Exception as e:
return Response(status=status.HTTP_400_BAD_REQUEST)
The view accepts a POSTed refresh_token, uses that to create a RefreshToken object for access to the blacklist class method, and blacklists it.
Add it to
urls.py.
# djsr/authentication/urls.py
from .views import ObtainTokenPairWithColorView, CustomUserCreate, HelloWorldView, LogoutAndBlacklistRefreshTokenForUserView
urlpatterns = [
...
path('blacklist/', LogoutAndBlacklistRefreshTokenForUserView.as_view(), name='blacklist')
]
Next we need to add a button to our navbar to delete the localStorage tokens and to post the token to a blackout API view, which we will make shortly. This will go in App.js for now, but making a dedicated Nav component at this point also makes sense.
// djsr/frontend/src/components/App.js
...
import axiosInstance from "../axiosApi";
class App extends Component {
constructor() {
super();
this.handleLogout = this.handleLogout.bind(this);
}
async handleLogout() {
try {
const response = await axiosInstance.post('/blacklist/', {
"refresh_token": localStorage.getItem("refresh_token")
});
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
axiosInstance.defaults.headers['Authorization'] = null;
return response;
}
catch (e) {
console.log(e);
}
};
render() {
return (
<div className="site">
<nav>
<Link className={"nav-link"} to={"/"}>Home</Link>
<Link className={"nav-link"} to={"/login/"}>Login</Link>
<Link className={"nav-link"} to={"/signup/"}>Signup</Link>
<Link className={"nav-link"} to={"/hello/"}>Hello</Link>
<button onClick={this.handleLogout}>Logout</button>
</nav>
...
The button triggers
handleLogout()
when clicked. Handle logout posts the refresh token to the blackout API View to black it out, and then deletes access and refresh tokens from localStorage
, while resetting the Authorization header for the axios instance. Need to do both, otherwise Axios will still be able to get authorized access to protected view.Build it, run it, and test it by viewing
/hello/
before logging in, while logged in, and after logging out. Clicking the logout button doesn’t trigger any kind of global refresh for the site, and clicking the link to the /hello/
page also doesn’t refresh the component if you’re already there, so you’ll may have to manually refresh to see the message disappear.That’s it!
Thanks for coding along with me! Follow me on Hackernoon @Toruitas or on Twitter: @Stuart_Leitch as I work on stuff like this, machine learning, philosophy in the digital age, and Creative Coding at my startup Lollipop.ai and at University of the Art London's Creative Computing Institute.
If you’ve made it this far, you’ve got a React frontend hosted by and interacting with a nice Django Rest Framework based API backend using JWT for authorization. It’s got essentials like Client-side routing, automatic token refreshing, and field errors. Plus nice-to-haves like token blacklisting and ES7 async/await syntax.
Well done.
If you want to use this as a starting point, do it! The GitHub repo is here
For potential followup articles, we could expand this by refactoring and including Redux for state management — it’s what I actually use regularly but would be too much to fit into a single article. I also want to explore how to adapt Django and React to using GraphQL.