This tutorial covers usage of djangorestframework-simplejwt
library to allow JWT authentication with separate frontend and two-factor authentication via email.
While you can use this tutorial for any Django project, it is recommended that you follow our Django Project Tutorial for beginners
Also, we don't cover docker-compose.yml file and some other stuff which you can find in the Django Project Tutorial so we can focus on implementing JWT Authentication.
Source repository: https://github.com/appliku/djangojwt2fa
In this article:
Install requirements¶
Make sure you have the following lines in requirements.txt
:
Django==3.2.4
djangorestframework==3.12.4
djangorestframework-simplejwt==4.7.1
django-environ==0.4.5
django-storages==1.11.1
django-cors-headers==3.7.0
django-braces==1.14.0
django-extensions==3.1.3
psycopg2-binary==2.8.6
arrow==1.1.0
gunicorn==20.1.0
pytz==2021.1
Note: there can be other packages as well, these are directly required or recommended packages to have.
Django Settings¶
Add the following code to settings.py
of your project:
# REST FRAMEWORK
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
Add the token_blacklist
app to INSTALLED_APPS (or THIRD_PARTY_APPS
if you use Djangito project template):
INSTALLED_APPS = (
'rest_framework_simplejwt.token_blacklist',
}
This configures Django REST Framework to use JWTAuthentication backend.
In the project's urls.py
(adjancent to settings.py
) add the following imports and urls_patterns
:
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
Why we need these endpoits:
The token_obtain_pair
is needed for "login" process, when user provides username and password and gets access_token
and refresh_token
.
The token_refresh
is needed when the access_token
expires and with the refresh_token
user gets new access_token
. Although in our tutorial we'll make refresh_token
also refreshed with proper settings.
Django SimpleJWT Settings¶
The library djangorestframework-simplejwt
provides a configuration object, that you should put to project's settings.py
. Here is example from their docs:
# Django project settings.py
from datetime import timedelta
...
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': settings.SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
We don't need to have them all in this object, we'll specify only several and the rest will be default.
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=3),
'SIGNING_KEY': env('SIMPLE_JWT_SIGNING_KEY', default=None) or SECRET_KEY,
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True
}
With these settings we'll have access_token valid for 30 minutes, refresh_token valid for 3 days. Singing key(the one used to issue tokens) will be specified via the dedicated environment variable or default to SECRET_KEY. Refresh tokens will be rotated when refresh endpoint is accessed. Old refresh token will be blacklisted on refresh.
Now let's apply run our app and apply migrations.
First let's run docker-compose up db -d
in order for our db
initialize.
Then run docker-compose run web python manage.py migrate
to apply migrations.
Expected output of this command should be a lot of "Applying... " lines and no errors.
Let's create a superuser.
If you are not using Djangitos template then run docker-compose run web python manage.py createsuperuser
command.
If you are using Djangitos template or followed our tutorials then you can use more convenient command docker-compose run web python manage.py makesuperuser
that will generate superuser account and random password without asking any questions.
This will be expected output for this command:
Note: you will need this password, another run of the same command will not show password nor it wll create another user!
Now we can run the development server of our project:
docker-compose up
This will spin up all backing services and our application in web
container.
These should be last lines of expected output of that command:
Let's open admin panel of our Django Project in a browser. In order to do that open this URL: http://0.0.0.0:8060/admin/login/?next=/admin/
Type admin@example.com
in the Email Address field and password from output of makesuperuser
command.
Once you were able to login into the admin panel, let's make sure that we have the Blacklist app installed. On the main page of the admin panel you should see a block called "TOKEN BLACKLIST" and two models Blacklisted tokens and Outstanding tokens.
Testing Django SimpleJWT token_obtain_pair endpoint¶
Let's use CURL command to issue token pair.
Make sure to put replace the password from example with the password that was generated by makesuperuser
command.
curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "F1i779jMXKus"}' \
http://0.0.0.0:8060/api/token/
Output for this command should look like this:
{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMjg5NDY5NiwianRpIjoiYjEwOTdkMWI0ZDJlNDAxODg3MzViMDgxNjg1YzM4NjkiLCJ1c2VyX2lkIjoiYWIxNTI3MmQtMmRmNC00ODZhLTk0ODUtMWEwODk2OGM0MTVhIn0.1syTNRfrqqwdQlQjr9aBT35ZFeV1UkAWWIug309ubZc","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIyODA4NTk2LCJqdGkiOiJiNWY1MzNmOTBhMTI0NThlOGMwMTgzODUxOTJjZDZjZiIsInVzZXJfaWQiOiJhYjE1MjcyZC0yZGY0LTQ4NmEtOTQ4NS0xYTA4OTY4YzQxNWEifQ.3TbxEthvwqHuaFcgem3hCXvyhFeIgu2OI62a3Hsqjxk"}
As you can see the response from server contains the "refresh", with refresh token and "access" with access token.
Now let's try to refresh our tokens.
Replace refresh token with the one you got from the last command.
curl \
-X POST -H "Content-Type: application/json" \
-d '{"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMjg5NDY5NiwianRpIjoiYjEwOTdkMWI0ZDJlNDAxODg3MzViMDgxNjg1YzM4NjkiLCJ1c2VyX2lkIjoiYWIxNTI3MmQtMmRmNC00ODZhLTk0ODUtMWEwODk2OGM0MTVhIn0.1syTNRfrqqwdQlQjr9aBT35ZFeV1UkAWWIug309ubZc"}' \
http://0.0.0.0:8060/api/token/refresh/
The output should be similar to previous one, again an object with "access" and "refresh" keys.
{"access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIyODEwOTIxLCJqdGkiOiIyMDcyMTc5NmVmMjc0MjgxYTc3ZjI0MTM1ZWY3YjcyNiIsInVzZXJfaWQiOiJhYjE1MjcyZC0yZGY0LTQ4NmEtOTQ4NS0xYTA4OTY4YzQxNWEifQ.SjMVjCeLMhFV_QI20tCjo3gvJcpSkswAlrAUSJlJZFE","refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMzA2ODMyMSwianRpIjoiODczYTFlMjg5NmI3NDlhOGExNDYxYTk2MjQwYmQzMzAiLCJ1c2VyX2lkIjoiYWIxNTI3MmQtMmRmNC00ODZhLTk0ODUtMWEwODk2OGM0MTVhIn0.eNfz8oR2-beQ4SouGJUtRGlRxbAhUL38IYsuQiveowE"}
Now run the same command again and verify that old refresh token doesn't work anymore:
curl \
-X POST -H "Content-Type: application/json" \
-d '{"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMjg5NDY5NiwianRpIjoiYjEwOTdkMWI0ZDJlNDAxODg3MzViMDgxNjg1YzM4NjkiLCJ1c2VyX2lkIjoiYWIxNTI3MmQtMmRmNC00ODZhLTk0ODUtMWEwODk2OGM0MTVhIn0.1syTNRfrqqwdQlQjr9aBT35ZFeV1UkAWWIug309ubZc"}' \
http://0.0.0.0:8060/api/token/refresh/
{"detail":"Token is blacklisted","code":"token_not_valid"}
It means that both blacklist app is installed and settings are in place to rotate and blacklist old refresh tokens.
You can go to the Django Admin and verify that refresh token is blacklisted.
Django JWT Authentication Logout¶
Up until this point we putted all code in place in order to perform login scenario.
But now we should implement logout functionality.
Let's talk about it for a second so you can have better understanding of what should be done here.
With JWT authentication access_token is issued for a very short period of time and it is always valid until it expires.
To perform authentication the access_token
is used, not refresh_token
.
When access_token
expires, client should use the refresh endpoint in order to get a fresh access_token
. We also issue a new refresh_token
so the authenticated "session" is extended. In other words, if user keeps using our app on a regular basis, they should always have a valid refresh_token
and there be no need for login flow.
After new access_token
is received it should be used for all API calls that require authentication.
Next access of refresh endpoint should use the latest refresh_token, because all the previous ones will be invalid.
Now what happens if a user wants to log out?
The client can forget both tokens. For example on frontend we could remove both access and refresh tokens from localStorage.
But if the refresh_token was stolen, then another client can keep using that token Indefinitely.
In order to prevent that let's make a logout API endpoint, which client will call to invlidate current refresh_token or all refresh_tokens assigned for current user.
Create Django Logout JWT APIView¶
If you are using Djangitos project template or followed our tutorial, then you should have an app usermodel
. Let's put our logout view in this app. If you don't have this app, create one by running docker-compose run web python manage.py startapp usermodel
and add it to INSTALLED_APPS
in settings.py
.
Open usermodel/views.py
and add the following code:
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken, BlacklistedToken
from rest_framework_simplejwt.tokens import RefreshToken
class APILogoutView(APIView):
permission_classes = (IsAuthenticated,)
def post(self, request, *args, **kwargs):
if self.request.data.get('all'):
token: OutstandingToken
for token in OutstandingToken.objects.filter(user=request.user):
_, _ = BlacklistedToken.objects.get_or_create(token=token)
return Response({"status": "OK, goodbye, all refresh tokens blacklisted"})
refresh_token = self.request.data.get('refresh_token')
token = RefreshToken(token=refresh_token)
token.blacklist()
return Response({"status": "OK, goodbye"})
In our root urls.py
(adjacent to settings.py
) add a line for this view and an import, so the whole file look something like this:
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from usermodel.views import APILogoutView
urlpatterns = [
path('sns/', include('ses_sns.urls')),
path('admin/', admin.site.urls),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('logout_token/', APILogoutView.as_view(), name='logout_token'),
]
Let's test it.
First let's go through login flow and obtain our tokens as we did before.
curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "F1i779jMXKus"}' \
http://0.0.0.0:8060/api/token/
We'll get our tokens.
{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMzA3MDk3NywianRpIjoiZDA3MmEyMmQ3NjRlNDVmMGJlOTZlYWU0NGZlYjkyMTUiLCJ1c2VyX2lkIjoiYWIxNTI3MmQtMmRmNC00ODZhLTk0ODUtMWEwODk2OGM0MTVhIn0.j5GABClzwwikOZ3g1UzHx4vCCOGsGcqMPnXjkqrB7zc","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIyODEzNTc3LCJqdGkiOiI3N2M2MGFjZjkzMTY0YmEwYWVlOTk2OGFiNWQ4YjZlYyIsInVzZXJfaWQiOiJhYjE1MjcyZC0yZGY0LTQ4NmEtOTQ4NS0xYTA4OTY4YzQxNWEifQ._DR7jnevGgUYQ_gdoaflywCma2Z67h1qj_RzTiPnSc8"}
Let's invalidate our refresh_token with our new APILogoutView
:
curl \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIyODEzNTc3LCJqdGkiOiI3N2M2MGFjZjkzMTY0YmEwYWVlOTk2OGFiNWQ4YjZlYyIsInVzZXJfaWQiOiJhYjE1MjcyZC0yZGY0LTQ4NmEtOTQ4NS0xYTA4OTY4YzQxNWEifQ._DR7jnevGgUYQ_gdoaflywCma2Z67h1qj_RzTiPnSc8" \
-d '{"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMzA3MDk3NywianRpIjoiZDA3MmEyMmQ3NjRlNDVmMGJlOTZlYWU0NGZlYjkyMTUiLCJ1c2VyX2lkIjoiYWIxNTI3MmQtMmRmNC00ODZhLTk0ODUtMWEwODk2OGM0MTVhIn0.j5GABClzwwikOZ3g1UzHx4vCCOGsGcqMPnXjkqrB7zc"}' \
http://0.0.0.0:8060/logout_token/
Since this view requires authentication we should send the Authorization
header which value should consist of "Bearer " and our access token. As post data we should send a JSON object with refresh_token
.
In response we'll get {"status":"OK, goodbye"}
.
Let's go to Django Admin and see that we have a blacklisted token http://0.0.0.0:8060/admin/token_blacklist/blacklistedtoken/
Pick the most recent record, click on it and click the edit button near the token dropdown. A pop-up window will open where you can see the actual token. Compare it to your refresh token, they should match.
Django SimpleJWT blacklist all refresh_tokens for the current user¶
In your frontend interface you might want to give your users a way to logout all other devices/browsers that logged in with the same account.
In order to do that, instead of sending the refresh_token
in the POST data, we can send a different object with the all
key, like this:
curl \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIyODEzNTc3LCJqdGkiOiI3N2M2MGFjZjkzMTY0YmEwYWVlOTk2OGFiNWQ4YjZlYyIsInVzZXJfaWQiOiJhYjE1MjcyZC0yZGY0LTQ4NmEtOTQ4NS0xYTA4OTY4YzQxNWEifQ._DR7jnevGgUYQ_gdoaflywCma2Z67h1qj_RzTiPnSc8" \
-d '{"all": "1"}' \
http://0.0.0.0:8060/logout_token/
The response will be
{"status":"OK, goodbye, all refresh tokens blacklisted"}
Since refresh tokens in SimpleJWT have the user
foreign key we can find all keys that were issued for this user and blacklist them.
Pro tip: Similarly you might want to blacklist all refresh tokens in the password reset flow.