⤆ Go Back

Django REST Framework and JWT Authentication

JSON Web Token(JWT) is an authentication strategy used by client/server applications. In this post, we will see how we can integrate JWT in Django REST Framework APIs.

JWT is a token that has to be attached to every request made by the client.

1curl http://127.0.0.1:8000/hello/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzODI4NDMxLCJqdGkiOiI3ZjU5OTdiNzE1MGQ0NjU3OWRjMmI0OTE2NzA5N2U3YiIsInVzZXJfaWQiOjF9.Ju70kdcaHKn1Qaz8H42zrOYk0Jx9kIckTn9Xx7vhikY'

JWT is designed in such a way that, a client exchange access token and refresh token for username and password.

  1. Access Tokens: are short-lived, generally 5 min, and can be modified.
  2. Refresh Tokens: are comparatively long-lived, generally 12 hours. It can be compared to session token, which Django generally uses. A refresh token is used to get the new access token, once an access token is expired.

Why would we need Refresh token, we can work with access token only? It's a Security feature that provides more security in terms of cache stealing or related attacks. And also because a JWT token, holds information. If you look closely at the above example of JWT token, you can see that token is divided into 3 parts.

1xxxxxx.yyyyyy.zzzzzz

Those are the parts that compose the JWT tokens. Those three parts are distinctive,

1header.payload.signature

So, from above example we have,

1header=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2payload=eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzODI4NDMxLCJqdGkiOiI3ZjU5OTdiNzE1MGQ0NjU3OWRjMmI0OTE2NzA5N2U3YiIsInVzZXJfaWQiOjF9
3signature=Ju70kdcaHKn1Qaz8H42zrOYk0Jx9kIckTn9Xx7vhikY

If you are thinking, what are those weird-looking text, it's standard text-encoded in Base64. If we decode them, we get,

header

1{
2 "typ": "JWT",
3 "alg": "HS256"
4}

payload

1{
2 "token_type": "access",
3 "exp": 1543828431,
4 "jti": "7f5997b7150d46579dc2b49167097e7b",
5 "user_id": 1
6}

signature

Signature is not encoded in base64. Signature is a token, which is created by JWT backend by combining the encoded header + encoded payload + SECRET_KEY of Django. If someone tries to change the header or the payload to poke around the database, then the signature would change. The only way of checking and validating the JWT token and signature is with the help of SECRET_KEY. That's why people say, store your SECRET_KEY key in the env variable and never push your production SECRET_KEY on Github public repo.

That's the basic of JWT. If you want to go deep, I would suggest referring to this site. https://jwt.io/

Let's start with Django's implementation.

Setting up a virtual environment for the project

Never forgot to create a virtual environment for a Django project. Create a folder for your project and move into it. :

1mkdir django-jwt && cd $_

Install dependency for the python environment, and create a virtual environment.

1sudo apt-get install python3-venv
2python3 -m venv venv
3source venv/bin/activate

Now pull the dependencies for Django and rest framework.

1pip install django djangorestframework djangorestframework_simplejwt

When it completes, you can create a Django project.

1django-admin startproject django_jwt .

To create an app in Django, run this command,

1python manage.py startapp jwt_api

After creating this app, your folder structure should look like this,

1(venv) viral@sangani: $ tree -d -L 1
2.
3├── django_jwt
4├── jwt_api
5└── venv
6
73 directories

Now let's inform Django about this newly created app, move todjango_jwt/settings.py, and add the app in INSTALLED_APPS.

1INSTALLED_APPS = [
2 'django.contrib.admin',
3 'django.contrib.auth',
4 'django.contrib.contenttypes',
5 'django.contrib.sessions',
6 'django.contrib.messages',
7 'django.contrib.staticfiles',
8 'jwt_api', # activate the new app
9
10]

Add these lines in django_jwt/settings.py.

1REST_FRAMEWORK = {
2 'DEFAULT_AUTHENTICATION_CLASSES': [
3 'rest_framework_simplejwt.authentication.JWTAuthentication',
4 ],
5}

This will tell the REST framework to use JWT Authentication instead of Basic Authentication. Add this in django_jwt/urls.py

1from django.contrib import admin
2from django.urls import path, include
3
4urlpatterns = [
5 path('admin/', admin.site.urls),
6 path('api/', include("jwt_api.urls"))
7]

Now let's create an actual endpoint in jwt_api.urls.py.

1from rest_framework_simplejwt.views import (
2 TokenObtainPairView,
3 TokenRefreshView,
4)
5from django.urls import path
6urlpatterns = [
7 path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
8 path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
9]

For this post, we will create a simple API view, as shown below.

1# jwt_api/views.py
2from rest_framework.views import APIView
3from rest_framework.response import Response
4from rest_framework.permissions import IsAuthenticated
5
6
7class HelloView(APIView):
8 permission_classes = (IsAuthenticated,)
9
10 def get(self, request):
11 content = {'message': 'Hello, World!'}
12 return Response(content)

This view will only return data if the user authenticated because we have added permission_classes = (IsAuthenticated,). This uses DEFAULT_AUTHENTICATION_CLASSES from settings.py, which we have defined and use that authentication method.

Add this URL in urls.py. So now our final jwt_api/urls.py becomes,

1from rest_framework_simplejwt.views import (
2 TokenObtainPairView,
3 TokenRefreshView,
4)
5from django.urls import path
6from jwt_api.views import HelloView
7
8urlpatterns = [
9 path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
10 path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
11 path('hello/', HelloView.as_view(), name='hello'),
12]

Now to test this API, we have to create a user. To do so, make migrations and create a superuser.

1python manage.py makemigrations
2python manage.py migrate
3python manage.py createsuperuser

Once this is set up, we can start the server and test the user endpoint.

1python manage.py runserver

Since I am using Linux, I will be using httpie for testing, you can use Postman, insomnia or simply use cURL.

The first step is to authenticate and obtain tokens. For this, we will use /api/token/ endpoint. This endpoint is provided by djangorestframework_simplejwt library itself. You can see the documentation here.

Run this command in the terminal.

1http post http://127.0.0.1:8000/api/token/ username=admin password=admin

This is the response I get:

1HTTP/1.1 200 OK
2Allow: POST, OPTIONS
3Content-Length: 438
4Content-Type: application/json
5Date: Fri, 29 May 2020 20:07:54 GMT
6Server: WSGIServer/0.2 CPython/3.6.9
7Vary: Accept
8X-Content-Type-Options: nosniff
9X-Frame-Options: DENY
10
11{
12 "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTkwNzgzMTc0LCJqdGkiOiJiOTk1MjQ2MmM2ZWY0NmQ0ODg3MDcxMjIxYWYwYThjMCIsInVzZXJfaWQiOjF9.U5ogKl5VrPShucnd4hlR0glpwQEd-86EWfIcbiPEcsY",
13 "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU5MDg2OTI3NCwianRpIjoiMWU4ZWRiMDUzNWI5NDZhNWExOWFiOTRkMWEwYzkxY2EiLCJ1c2VyX2lkIjoxfQ.71QeI7nR9XqvBVOiOyE1hFryE_MAZn0VjUHR1qCu-34"
14}

Usually, we store these tokens on the client-side, in localStorage. To access the protected URL, we will attach the access token without request in the header like this,

1http http://127.0.0.1:8000/api/hello/ "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTkwNzgzMTc0LCJqdGkiOiJiOTk1MjQ2MmM2ZWY0NmQ0ODg3MDcxMjIxYWYwYThjMCIsInVzZXJfaWQiOjF9.U5ogKl5VrPShucnd4hlR0glpwQEd-86EWfIcbiPEcsY"

From the above request, I got:

1HTTP/1.1 200 OK
2Allow: GET, HEAD, OPTIONS
3Content-Length: 27
4Content-Type: application/json
5Date: Fri, 29 May 2020 20:10:42 GMT
6Server: WSGIServer/0.2 CPython/3.6.9
7Vary: Accept
8X-Content-Type-Options: nosniff
9X-Frame-Options: DENY
10
11{
12 "message": "Hello, World!"
13}

If someone alters the token, and try to get the data, then the response will be,

1HTTP/1.1 401 Unauthorized
2Allow: GET, HEAD, OPTIONS
3Content-Length: 183
4Content-Type: application/json
5Date: Fri, 29 May 2020 20:12:10 GMT
6Server: WSGIServer/0.2 CPython/3.6.9
7Vary: Accept
8WWW-Authenticate: Bearer realm="api"
9X-Content-Type-Options: nosniff
10X-Frame-Options: DENY
11
12{
13 "code": "token_not_valid",
14 "detail": "Given token not valid for any token type",
15 "messages": [
16 {
17 "message": "Token is invalid or expired",
18 "token_class": "AccessToken",
19 "token_type": "access"
20 }
21 ]
22}

After 5 min, If you try the same token, we will receive the same response as above. In that case, the client has to use refresh token and ask for a new access token and store it in localStorage again. The endpoint refresh token in /api/token/refresh/.

Here is the example,

1http post http://127.0.0.1:8001/api/token/refresh/ refresh=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU5MDg2OTI3NCwianRpIjoiMWU4ZWRiMDUzNWI5NDZhNWExOWFiOTRkMWEwYzkxY2EiLCJ1c2VyX2lkIjoxfQ.71QeI7nR9XqvBVOiOyE1hFryE_MAZn0VjUHR1qCu-34

In this case, you will receive only the access token as shown below,

1HTTP/1.1 200 OK
2Allow: POST, OPTIONS
3Content-Length: 218
4Content-Type: application/json
5Date: Fri, 29 May 2020 20:14:31 GMT
6Server: WSGIServer/0.2 CPython/3.6.9
7Vary: Accept
8X-Content-Type-Options: nosniff
9X-Frame-Options: DENY
10
11{
12 "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTkwNzgzNTcxLCJqdGkiOiI1MGJkYjAyMTVhZWE0MzdhOTJjZWRhZDMzOGJlYWZmYiIsInVzZXJfaWQiOjF9.1lHTWwyZZpvFtnVkbr55v411kVl8YjAVikcilSd6wF8"
13}

Now the client can use this token to access all the other resources.

Note: By client, I am referring to the Client-side framework, Eg: ReactJS, AngularJS, simple VanillaJS, or 3rd party applications.

Whether tokens are opaque or not is usually defined by the implementation. Common implementations allow for direct authorization checks against an access token. That is, when an access token is passed to a server managing a resource, the server can read the information contained in the token and decide itself whether the user is authorized or not (no checks against an authorization server are needed). This is one of the reasons tokens must be signed (using JWS, for instance). On the other hand, refresh tokens usually require a check against the authorization server. This split way of handling authorization checks allows for three things:

-> Improved access patterns against the authorization server (lower load, faster checks)
-> Shorter windows of access for leaked access tokens (these expire quickly, reducing the chance of a leaked token allowing access to a protected resource)
-> Sliding-sessions (see below)

Sliding-sessions

Sliding-sessions are sessions that expire after a period of inactivity. As you can imagine, this is easily implemented using access tokens and refresh tokens. When a user acts, a new access token is issued. If the user uses an expired access token, the session is considered inactive, and a new access token is required. This token can be obtained with a refresh token, or a new authentication round is required. The requirements of the development team define this. Security considerations

Refresh tokens are long-lived. This means when a client gets a refresh token from a server, this token must be stored securely to keep it from being used by potential attackers. If a refresh token is leaked, it may be used to obtain new access tokens (and access protected resources) until it is either blacklisted or expires (which may take a long time). Refresh tokens must be issued to a single authenticated client to prevent the use of leaked tokens by other parties. Access tokens must be kept secret, but as you may imagine, security considerations are less strict due to their shorter life.

Reference : https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/