1. Django Custom User
2. DRF serializers and auth
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
Part 1: Django Backend
$ mkdir django-jwt-react
$ cd django-jwt-react
pipenv
.PS: Poetry is better but harder to get that first venv up and running
$ pipenv --python 3.7
$ pipenv install django djangorestframework djangorestframework-simplejwt
djangorestframework-jwt
but it is no longer maintained. Use djangorestframework-simplejwt instead.$ pipenv shell
$ django-admin startproject djsr
django-jwt-react/
directory.-django-jwt-react/
--djsr/
---djsr/
----__init__.py
----settings.py
----urls.py
----wsgi.py
---manage.py
--Pipfile
--Pipfile.lock
$ python djsr/manage.py startapp authentication
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'
]
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.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)
settings.py
we configure CustomUser
as our AUTH_USER_MODEL
.# djsr/djsr/settings.py
# ...
# Custom user model
AUTH_USER_MODEL = "authentication.CustomUser"
$ python djsr/manage.py makemigrations
$ python djsr/manage.py migrate
$ python djsr/manage.py createsuperuser
$ python djsr/manage.py runserver
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
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',
}
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.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.# 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'))
]
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'),
]
$ 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"}
$ 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"}
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.{
"typ": "JWT",
"alg": "HS256"
}
{
"token_type": "refresh",
"exp": 1561622244,
"jti": "ae2e3b6db5244e249bf02e0b1b751ff3",
"user_id": 1
}
CustomUser
model. First, go into the admin panel 127.0.0.1:8000/admin/
and choose a color. # 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
# 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
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'),
]
$ 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"}
get_user_info()
function. Don't use it like that.CustomUser
model, but we need to make a serializer for it, and put that in a view with a URL.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
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. 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.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)
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'),
]
$ 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"}
# djsr/authentication/views.py
...
class HelloWorldView(APIView):
def get(self, request):
return Response(data={"hello":"world"}, status=status.HTTP_200_OK)
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')
]
$ 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" -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"}
“JWT”
in the AUTH_HEADER_TYPES
. “Authorization: JWT “ + access token
. Or whatever you set AUTH_HEADER_TYPES
as. Otherwise, no dice. This will become important when we connect the frontend. {"hello":"world"}
! $ 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"}]}
Unauthorized: /api/hello/
[13/Jun/2019 12:53:29] "GET /api/hello/ HTTP/1.1" 401 183
Part 2: React Frontend
2–1) Installing React inside our Django project as a standalone app
$ cd djsr
$ python manage.py startapp frontend
INSTALLED_APPS
in settings.py
.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>
// djsr/fontend/static/frontend/style.css
#root{
background-color:rebeccapurple;
}
# 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'))
]
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)
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.# djsr/frontend/urls.py
urlpatterns = [
path('', index_view), # for the empty url
url(r'^.*/$', index_view) # for all other urls
]
$ cd ../.. (so you're in project root: django-jwt-react)
$ python djsr/manage.py runserver
package.json
file. The answers to the questions here don’t matter much, so just leave them at pretty much the default.$ npm init
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.djsr
+-- authentication/
+-- djsr/
+-- frontend/
| +-- migrations/
| +-- src/
| +-- static/
| | +-- frontend/
| | | +-- public/
| | | +-- style.css
| +-- templates/
| | +-- frontend/
| | | +-- index.html
+--db.sqlite3
+--manage.py
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>
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.
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/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/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. .babelrc
file in the project root, next to package.json
.{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
$ npm install --save-dev webpack webpack-cli babel-loader
+ babel-loader@8.0.6
+ webpack-cli@3.3.4
+ webpack@4.35.0
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).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"]}
},
}
],
},
};
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..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! $ npm install --save react react-dom
+ react@16.8.6
+ react-dom@16.8.6
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'));
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.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;
className
rather than class. 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"
}
}
$ npm run build
djsr/frontend/public/
. Is there a main.js
file there? There should be.$ python djsr/manage.py runserver
2–2) Preparing React
react-router-dom
. 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.$ npm install --save react-router-dom
+ react-router-dom@5.0.1
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
. 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'));
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.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;
// 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;
// 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;
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). // 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;
<a href=””>
, use React router’s <Link>
component, which we have to for React router to work correctly with the history API. For now, add the class “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;
}
<Link>
s do what they’re supposed to do.$ npm run build
$ python djsr/manage.py runserver
// 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;
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.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
.handleChange
method, using one line of code. Not exactly a Python one-liner, but it scratches that itch.this
to each class method in the constructor, or else this will be undefined in the callback. Very annoying to forget. // 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;
$ npm run build
$ pythong djsr/manage.py runserver
2–3) Axios for requests and tokens
$ npm install --save axios
+ axios@0.19.0
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'
}
});
.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. 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;
/login/
endpoint and try to login:$ npm run build
$ python djsr/manage.py runserver
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.// 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;
constructor()
method. We set message in the state
, and later on in render
, we render whatever the message is. componentDidMount
method is fired. Since we want to get this message RIGHT AWAY, this is the right place to trigger our GET request. 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;
/hello/
and see what happens.message = response.data.hello
without waiting for the response.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.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;
}
}
...
.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>
)
}
}
Sidenote:,.then()
, and.catch()
do work together pretty well. I just prefer putting everything is standard.finally()
blocks.try/catch/finally
// 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>
)
}
}
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
.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
// webpack.config.js
const path = require('path');
module.exports = {
mode: "development",
entry: ['babel-polyfill', path.resolve(__dirname, 'djsr/frontend/src/index.js')],
...
}
index.js
file.$ npm run build
$ python djsr/manage.py runserver
/hello/
and check the console — we should see that it now runs succesfully with the async/await method working on login. Cool.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;
}
}
...
/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
// 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
/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
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);
}
}
...
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
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)
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)
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>
)
}
...
{ this.state.errors.password ? this.state.errors.password : null}
{ Boolean ? (content to show if True) : (content to show if False)
2–4) Logging out and blacklisting tokens
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',
}
$ python djsr/manage.py migrate
# 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)
urls.py.
# djsr/authentication/urls.py
from .views import ObtainTokenPairWithColorView, CustomUserCreate, HelloWorldView, LogoutAndBlacklistRefreshTokenForUserView
urlpatterns = [
...
path('blacklist/', LogoutAndBlacklistRefreshTokenForUserView.as_view(), name='blacklist')
]
// 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>
...
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./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.