narancs's blog

Creating token obtain and token refresh APIs in Django

Introduction

This is the second part of my JWT authentication with Django and React series. In the first part we have set up a basic Django project and added a custom User model. In this part I will explain the following:

  • How to configure JWT authentication in Django using REST Framework’s Simple JWT package
  • How to create token obtain API, that will provide access token and refresh token to the user if correct login credentials are provided
  • How to create token refresh API, that will provide new access token to the user if a valid refresh token is provided

Let’s get started!

Configuring JWT authentication in Django

In the first part of the series, we have already installed the required packages, but if you need them, then run the following command (make sure you are in the Python virtual environment if you have create one):

				
					pip install djangorestframework djangorestframework-simplejwt
				
			

Then we need to add the authentication class to settings.py. I will use JWTStatelessUserAuthentication backend instead of the default JWTAuthentication backend. This does not perform a database query to look up a user instance. This is better for performance and scalability, and also makes it possible to use the same token for multiple Django apps that share the same token secret (e.g. in micro-service architecture).

Add the following configuration in settings.py:
				
					REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTStatelessUserAuthentication",
    )
}
				
			

The Simple JWT package has implementations of the token obtain and token refresh APIs. So at this point, we could just add the default implementations of the token obtain and token refresh APIs to our users app in urls.py. However, there are two things that I don’t like about that:

  • First, if we add the urls to the users app, then later on when we add other apps as well, all our urls can be scattered around the project. Not to mention that the URL paths will not be common either.
    • Imagine that you access APIs in users app at /users/api/v1/token, and APIs in an other app at /otherapp/api/v1/someapi
    • We want /api/v1 to be the common starting point
  • Second, the default implementations of these APIs return the refresh token in the request body. That means, that from the front-end part of our application we have to take care of storing the token via JavaScript. We can either store in in localStorage or in a Cookie that do not have the httpOnly attribute. Both of these will expose the refresh token to XSS attacks.

Let’s tackle both of this problems.

Creating the token obtain and refresh APIs

First, let’s create a new app called api, so we can collect all our APIs in this app, and make it available at a standard URL path: /api/v1.

				
					python manage.py startapp api
				
			

Add the app to DEVELOPED_APPS in settings.py:

				
					DEVELOPED_APPS = ["users", "api"]
				
			

Now we can start adding URLs to the api app. Create urls.py inside the api app directory, and add the following code:

				
					from django.urls import path
from rest_framework_simplejwt.views import (TokenObtainPairView,
                                            TokenRefreshView)
urlpatterns = [
    path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]

				
			

As you can see, we have imported the default implementations of the TokenObtainPairView and TokenRefreshView from REST Framework’s Simple JWT package. To be able to access the URLs, we also need to include them in the root urls.py file in config directory:

				
					from django.contrib import admin
from django.urls import include, path
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("api.urls")),
]
				
			

If we save the changes we should be able to access the APIs. Open http://localhost:8000/api/v1/token/ and enter the super user credentials that you created in the first part of the series (or you can create a super user now).

DRF Simple JWT default Token Obtain API

We receive an access token and a refresh token as a response. As mentioned before, getting the refresh token in the response body means that we have to store the token in a place that will be vulnerable to XSS attacks.

We need to create our custom token obtain endpoint that returns the access token in the body, but includes the refresh token in a secure, httpOnly cookie.

Creating custom TokenObtainPairView to securely return refresh token

Let’s open views.py in users app and create our first view:

				
					from rest_framework_simplejwt.views import TokenObtainPairView

class CustomTokenObtainPairView(TokenObtainPairView):
    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)
        refresh_token = response.data.pop("refresh", None)
        response.set_cookie(
            key="refresh_token",
            value=refresh_token,
            httponly=True,
            secure=True,
            samesite="None",
        )
        return response

				
			

We have imported the default TokenObtainPairView, and created a subclass of it. In the subclass we have overridden the post method to do the following:

  • We get the original response by calling the super class’s post method
    • The benefit of this is that we will still get the serializer’s validation that happens in the default view, so for example if we provide no credentials, it will return an appropriate response with error message(s).
  • We pop the refresh token from the response.data, and put it int a secure, httpOnly cookie
  • We return the modified response

We are done with our TokenObtainPairView. This can be used by the front-end application to authenticate the user using username and password.

We need to include it in api/urls.py:

				
					from django.urls import path
from users import views as users_views
urlpatterns = [
    path('token/', users_views.CustomTokenObtainPairView.as_view(),
         name='token_obtain_pair'),
]
				
			

Let’s test it out at http://localhost:8000/api/v1/token/:

As we can see, the refresh token is no longer part of the response body. Instead, it is returned as a response cookie with httOnly: true and secure: true attributes. We can also verify that the cookie is stored in our browser:

The next thing we need to do is to fix the TokenRefreshView. The default implementation takes the refresh token from POST data, and if it is valid, it returns a new access token. However, we just modified the token obtain view to return the refresh token as a Cookie. Therefore, we need to change the TokenRefreshView to take the refresh token from the request Cookie, and not from the POST data.

Creating custom TokenRefreshView to obtain refresh token from Cookie

Open users/views.py. We will need to import a few things for this one:

				
					from rest_framework import status
from rest_framework.response import Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework_simplejwt.views import TokenRefreshView
				
			

Then we can write the view:

				
					
class CustomTokenRefreshView(TokenRefreshView):
    def post(self, request, *args, **kwargs):
            refresh_token = request.COOKIES.get("refresh_token", None)
            serializer = self.get_serializer(data={"refresh": refresh_token})
            try:
                serializer.is_valid(raise_exception=True)
            except TokenError as e:
                raise InvalidToken(e.args[0])
            return Response(serializer.validated_data, status=status.HTTP_200_OK)
				
			

What this does:

  • gets the refresh_token from the cookies
  • passes it as the value of “refresh” into the serializer
  • checks if the serializer is valid
    • if not: return the error(s)
    • otherwise, return validated_data (that will be the new access token for the user)

Add it to api/urls.py:

				
					urlpatterns = [
    path(
        "token/",
        users_views.CustomTokenObtainPairView.as_view(),
        name="token_obtain_pair",
    ),
    path(
        "token/refresh/",
        users_views.CustomTokenRefreshView.as_view(),
        name="token_refresh",
    ),
]

				
			

We can test it at http://localhost:8000/api/v1/token/refresh/ url. It is still showing an input field as we would need to pass in some value as POST data, but remember that we have overridden the API. That means we can just click POST and our refresh token will be sent automatically via cookie (if it is set) and we will get a successful response with our new access token.

As we can see, the refresh token was included in the Request Cookies (this is automatically done by the browser). The API successfully returned a new access token for the user.

We are finished with our views. We tested both the token obtain and the token refresh APIs, and they work as expected. Now we can login (obtain token pairs), and also refresh our access token as a user.

The next thing we can do is to customize the JWT token claims.

Customizing JWT token claims

JWT claims are the pieces of information contained within a JWT token. These claims are encoded in the token itself and typically include information such as the identity of the user (subject), expiration time, issuer, audience, and any other custom or standard claims.

If we check our access token and decode it at https://jwt.io/, we will see that payload contains the following information in the payload:

				
					{
  "token_type": "access",
  "exp": 1714237994,
  "iat": 1714236537,
  "jti": "4d45f96a2bf6423583ca9b599eab213b",
  "user_id": 1
}
				
			

I would like to include the email address, first name and last name of the user in the payload as well. When we create the front-end application, we can decode the token there and use these values on the front-end (e.g. to display a welcome message with the users name).

To add custom data to the JWT token, we need to override the default serializer provided by the Simple JWT package. Lets create serializers.py in users app, and add our serializer:

				
					from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        token["first_name"] = user.first_name
        token["last_name"] = user.last_name
        token["email"] = user.email
        return token

				
			

Then we need to include it in views.py and specify the serializer for the token obtain view:

				
					...
from .serializers import MyTokenObtainPairSerializer

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer
...
				
			

Let’s test it. Get a new access token from the API and decode it at https://jwt.io/.

				
					{
  "token_type": "access",
  "exp": 1714238843,
  "iat": 1714238543,
  "jti": "8a62021cd94547e0b073fb557b16dbd5",
  "user_id": 1,
  "first_name": "",
  "last_name": "",
  "email": "lajos.seyler@gmail.com"
}
				
			

It works. The custom claims are not part of the generated access tokens.

Conclusion

We have configured Django to use JWT authentication. Then we successfully create customized token obtain and token refresh APIs.

  • The token obtain API returns an access token in the request body and a refresh token as a secure, httpOnly cookie.

  • The token refresh API returns a new access token to the user if a valid refresh token is provided in a request cookie.

Finally, we customized the JWT token to include additional claims. The front-end will be able to use this information after decoding the token.

You can find all the changes done since part 1 in the following pull request: https://github.com/narancs93/django-jwt-auth/pull/6

Now users can login and they can refresh their access token if it is expired. But how are they going to sign up?

We need to set up a page where users can sign up for our application. However, I want to make sure that users cannot login until they confirm they own the email address they entered. So, I plan to send an email to that address with a link to activate their account. Only after they click that link and verify their email will their account be activated. This will be part of the next part of the series (coming soon).

Table of Contents

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

Related posts

0
Would love your thoughts, please comment.x
()
x