How to create blacklist for JWT tokens in Django
Recently, in one of the projects, we had to provide the user with the opportunity to change the password, so that they logged out from all other devices at the same time. Because authentication was done on JWT tokens, what happened was that it was impossible to manually expire the token after creation, as it is stateless and stored on the client side.
In this article, we will analyze the generation of a JWT token with the possibility of blacklisting it using an empty project as an example, and we will also test the result using Postman.
Briefly about JWT
JSON Web Token (JWT) is a JSON object that is defined in the open standard RFC 7519. It is considered one of the secure ways to transfer information between two parties. Its main feature is that all the necessary authentication data is stored in the token itself. It consists of 3 main parts: header, payload and signature. Header is a JSON object that contains information about the token type and encryption method:
header = { "alg": "HS256", "typ": "JWT"}
Payload usually stores the user ID, the lifetime of the token, or any other information at the discretion of the issuer. However, there are reserved field names, and it is not recommended to change their purpose:
- iss: a string with the unique identifier of the party generating the token.
- sub: a string that is the unique identifier of the party about which information is contained in this token (subject).
- aud: an array of case-sensitive strings or a URI that is a list of the recipients of this token.
- exp: A time in Unix Time format that determines when the token will become invalid (expiration).
- nbf: opposite to the exp key, this is a Unix Time that determines when the token will become valid (not before).
- jti: a string that specifies the unique identifier for this token (JWT ID).
- iat: a time in Unix Time format that specifies when the token was created.
Signature is formed as follows:
1. Header and Payload are converted to base64 format.
2. Next, they are connected in one line through a dot.
3. According to the algorithm specified in the header, the received string is hashed based on the secret key.
The result of this algorithm is a signature. To get the JWT itself, you need to connect the header, payload and signature through the dot.
Authentication through JWT.
Typically, a user receives a JWT upon registration or first login. They save it on their device and, on subsequent calls to the API, pass this token with all requests. As a general rule, the token is placed in the request header. Having received a token, the application first checks its signature. After verifying that the signature is valid, the application extracts information about the user from the payload part and authorizes the user based on it.
Token lifetime
The lifetime of the token is a very important point when using JWT. There is no universal answer to this question, it all depends on the service. However, 2 points must be taken into account:
1. If the token lifetime is too long, it can cause security issues. For example, if an attacker manages to compromise a user’s token, they can use it until its lifetime expires.
2. A short token lifetime can lead to excessive load on the server, as the user will have to constantly refresh the old token (request a new one)
Hence the need to give the user an opportunity to reset all their tokens. For example, to give them the opportunity to change the password or log out from all devices if the token is compromised by an attacker.
There are several ways to revoke existing tokens, such as issuing tokens based on a unique user ID, or creating a blacklist for issued tokens.
Let’s take a look at the example about how to blacklist tokens on Django using the django rest framework and the Simple JWT library. Note that the Simple JWT library immediately provides us with a convenient Black List application that we will use.
Initial project setup
Let’s create an empty project with the django-admin startproject jwt_auth_project command. Let’s immediately create an application for working with users with the python manage.py startapp users command and register it in INSTALLED_APPS in the settings.py file:
INSTALLED_APPS = [
…
'users.apps.UsersConfig',
]
Let’s create a virtualized environment, install the djangorestframework and djangorestframework-simplejwt libraries and write in the settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
Set permissions only for authenticated users in REST_FRAMEWORK and specify the class provided by the simplejwt library as the authentication backend.
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=2),
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True
}
The settings for simplejwt are also written in the settings.py file. In this article, we will not go into detail about each of them, as they are all described in detail in the documentation. Note that we chose 5 minutes for the token lifetime, and 2 days for the refresh token.
After that, we should update INSTALLED_APPS:
INSTALLED_APPS = [
…
'users.apps.UsersConfig',
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
]
In the users application, we will create a urls.py file and register it in the jwt_auth_project/urls.py file:
from django.contrib import admin
from django.urls.conf import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/users/', include('users.urls'))
]
Next, we need to write a custom manager for the future user model. In the users application, create a managers.py file and type the following code:
from typing import Any, Type, Union
from django.contrib.auth.base_user import BaseUserManagerclass UserManager(BaseUserManager):"""
Manager for overriding users model
"""use_in_migrations = Truedef create_user(self, email: str, password: str, **kwargs: Union[str, Any]) -> Type[BaseUserManager]:"""
Managers method for default user creation
"""if not email:
raise ValueError("Please, input email address")email = self.normalize_email(email)
user = self.model(email=email, **kwargs)
user.set_password(password)
user.save(using=self._db)return userdef create_superuser(self, email: str, password: str, **params: Union[str, Any]) -> Type[BaseUserManager]:"""
Managers method for superuser creation
"""params.setdefault("is_staff", True)
params.setdefault("is_superuser", True)
params.setdefault("is_active", True)if params.get("is_staff") is not True:
raise ValueError("superuser must have a is_staff=True")
if params.get("is_superuser") is not True:
raise ValueError("superuser must have a is_superuser=True")return self.create_user(email, password, **params)
Now we can create our own user model in the users/models.py file:
from typing import List
from django.db import models
from django.contrib.auth.models import AbstractUser
from users.managers import UserManager
from rest_framework_simplejwt.tokens import RefreshTokenclass User(AbstractUser):
"""
[User]
Overridden user class with custom manager.
"""
username = None
# Email field will be used to identify user in the system
email = models.EmailField(unique=True)# Specifies which field is used to login
USERNAME_FIELD = "email"
REQUIRED_FIELDS: List = []
# Specifies which manager to use for this model
objects = UserManager()class Meta:
verbose_name = "Пользователь"
verbose_name_plural = "Пользователи"
app_label = 'users'@property
def access_token(self) -> str:
"""
Allows you to get an access token from an instance of the User model
:return: str
"""
return str(RefreshToken.for_user(self).access_token)@property
def refresh_token(self) -> str:
"""
Allows you to get a refere token from an instance of the User model.
:return: str
"""
return str(RefreshToken.for_user(self))def __str__(self) -> str:
"""
:returns:
[str]: Responsible for the correct display of the object.
"""return self.email
The next step is to tell Django which user model to use for authentication. To do this, write the following line in the settings file:
AUTH_USER_MODEL = "users.User"
Now you can start the server with python manage.py runserver create and run migrations with python manage.py makemigrations and python manage.py migrate. After that, the necessary tables for further work will be created in our database.
Receiving tokens, user registration, user information
At this stage, we are all set to write the main API points. Let’s create a users/serializers.py file and write the main serializers there:
from typing import Dict
from rest_framework import serializers
from users.models import Userclass RegistrationSerializer(serializers.ModelSerializer):
"""
Serializer for new user registration
"""
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True
)
access_token = serializers.CharField(max_length=255, read_only=True)
refresh_token = serializers.CharField(max_length=255, read_only=True)class Meta:
model = User
fields = ['email', 'first_name', 'last_name', 'password', 'access_token', 'refresh_token']def create(self, validated_data: Dict) -> User:
# The method from the custom manager is used
return User.objects.create_user(**validated_data)class UserInfoSerializer(serializers.ModelSerializer):
"""
Serializer to get basic information about the user
"""
class Meta:
model = User
fields = ['email', 'first_name', 'last_name']
After that, in the users/views.py file, we will write a view for registering and returning information about the user:
from rest_framework import status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from users.serializers import RegistrationSerializer, UserInfoSerializerclass RegistrationAPIView(APIView):# All users must have access to registration
permission_classes = [AllowAny]
serializer_class = RegistrationSerializerdef post(self, request: Request) -> Response:serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()return Response(serializer.data, status=status.HTTP_201_CREATED)class UserInfoAPIView(APIView):serializer_class = UserInfoSerializer
permission_classes = [IsAuthenticated]def get(self, request: Request) -> Response:return Response(self.serializer_class(request.user).data, status=status.HTTP_200_OK)
Now we need to define the routes for our views in the users/urls.py file:
from django.urls.conf import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from users.views import RegistrationAPIView, UserInfoAPIView, ResetTokenAPIView
urlpatterns = [
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('registartion/', RegistrationAPIView.as_view(), name='registartion'),
path('info/', UserInfoAPIView.as_view(), name='info'),
]
Note that the rest_framework_simplejwt library provides us with views for obtaining a token and a refresh and there is no need to write them manually.
Adding existing tokens to the “Black List”
Now the user can register in our application and get information about themselves. What is left for us to do is to give them the opportunity to log out from all devices (blacklist the tokens). The simple_jwt library provides us with two models, OutstandingToken and BlacklistedToken. We will use them to blacklist tokens.
To do this, let’s write another view in the users/views.py file:
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken, BlacklistedToken
class ResetTokenAPIView(APIView):
"""
Adding all refresh tokens in black list
"""def post(self, request: Request) -> Response:
tokens = OutstandingToken.objects.filter(user_id=request.user.id)
for token in tokens:
t, _ = BlacklistedToken.objects.get_or_create(token=token)return Response(status=status.HTTP_205_RESET_CONTENT)
And register it in users/urls.py:
from users.views import RegistrationAPIView, UserInfoAPIView, ResetTokenAPIViewurlpatterns = [
...
path('reset-all-token/', ResetTokenAPIView.as_view(), name='reset-all-token')
]
Testing the resulting API with Postman
Now we can test the resulting API using Postman. First, we need to send the following request:
Note that because we defined access_token and refresh_token as dynamic properties in the User model and specified them in the serializer, we do not need to additionally request them after registration.
After registration, the user can get information about their account. For this, the Authorization header with the value Bearer {access_token} must be added to the request:
___________________________________________________________________
Written by PurplePlane
Found our articles interesting and want to partner with us or join our team? Feel free to write at manager@purpleplane-it.com