DRF JWT Authentication

Django

Summary: In this tutorial, you will learn to implement JSON Web Token (JWT) authentication in Django rest framework with the help of the examples.

What is Token Authentication?

Token authentication is a method of authenticating a user by providing a token, which is generated by the server and sent to the client, and is then included in subsequent requests made by the client.

The server can then use the token to verify the identity of the user and authorize their actions. Tokens are often used in place of traditional username and password authentication, as they can be easily revoked or expired, and are more secure.

Tokens can also be used for API authentication, and can be sent to the client in the form of a JWT (JSON Web Token).

What is JSON Web Token?

A common example of a token is a JSON Web Token (JWT). A JWT is a JSON object that is encoded and signed by the server, and can be decoded and verified by the client.

The JWT typically contains information about the user, such as their user ID, and is often used to authenticate the user for a specific period of time.

Here is an example of a JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The JWT is composed of 3 parts, separated by dots(.). Each part is encoded in Base64.

The first part is the header, which contains information about the algorithm used to sign the token.

The second part is the payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional metadata.

The third part is the signature, which is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn’t changed along the way.

How to implement JWT in Django Rest Framework?

To implement JSON Web Tokens (JWT) in Django Rest Framework (DRF), you can use the djangorestframework-jwt package.

Here is an example of how you can implement JSON Web Tokens (JWT) authentication in Django Rest Framework (DRF) using the djangorestframework-jwt package:

1. Install the djangorestframework-jwt and pyjwt package:

pip install djangorestframework-jwt
pip install pyjwt

2. Add 'rest_framework_jwt' to your INSTALLED_APPS setting in the settings.py file:

INSTALLED_APPS = [
    # ...
    'rest_framework_jwt',
    # ...
]

3. Add 'rest_framework.authentication.JSONWebTokenAuthentication' to the AUTHENTICATION_CLASSES setting in your settings.py file:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.JSONWebTokenAuthentication',
    ]
}

4. Create a view or viewset that will handle issuing the token. In this view, you will need to authenticate the user (for example, by checking their credentials against the database) and then use the jwt.encode() method from the pyjwt library to generate the token.

from rest_framework import status
from rest_framework.response import Response
from rest_framework.decorators import api_view
import jwt
from django.conf import settings
from django.contrib.auth import authenticate

@api_view(['POST'])
def login(request):
    # Perform authentication, such as checking the user's credentials
    user = authenticate(request, username=request.data.get("username"), password=request.data.get("password"))
    if user is not None:
        # Generate and return the JWT
        payload = {'id': user.id, 'username': user.username}
        jwt_token = {'token': jwt.encode(payload, settings.SECRET_KEY).decode('utf-8')}
        return Response(jwt_token)
    else:
        return Response({"error": "Invalid credentials"}, status=status.HTTP_400_BAD_REQUEST)

In this example, the @api_view(['POST']) decorator is used to specify that the login() view will handle only POST requests.

The view takes the user’s credentials from the request, authenticates the user, and then generates and returns a JWT if the authentication is successful.

5. You can also create a view or viewset that handle token refresh, where you can check the expiry time of the token, if it is expired then you can issue a new token with a new expiry time.

from rest_framework import status
from rest_framework.response import Response
from rest_framework.decorators import api_view, authentication_classes, permission_classes
import jwt
from django.conf import settings
from django.contrib.auth import authenticate

@api_view(['POST'])
@authentication_classes([JSONWebTokenAuthentication])
def refresh_token(request):
    token = request.META.get('HTTP_AUTHORIZATION', '').split()[1]
    try:
        payload = jwt.decode(token, settings.SECRET_KEY)
    except jwt.ExpiredSignatureError:
        return Response({'error': 'Token expired'}, status=status.HTTP_401_UNAUTHORIZED)
    except jwt.DecodeError:
        return Response({'error': 'Invalid token'}, status=status.HTTP_401_UNAUTHORIZED)
    except:
        return Response({'error': 'Invalid token'}, status=status.HTTP_401_UNAUTHORIZED)
    user = User.objects.get(id=payload['id'])
    if user:
        payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(days=1)
        new_token = jwt.encode(payload, settings.SECRET_KEY)
        return Response({'token': new_token.decode('utf-8')})

In this example, the @api_view(['POST']) decorator is used to specify that the refresh_token() view will handle only POST requests and @authentication_classes([JSONWebTokenAuthentication]) is used to specify that this view will require authentication to access.

The view takes the current token from the Authorization header and decode it with the jwt.decode() method to validate it and extract the payload, if the token is expired or invalid it will return an appropriate response to the client.

If the token is valid, it will create a new payload with a new expiry time, encode it with the same secret key used to sign the original token, and return the new token to the client in the format {'token': new_token}.

6. Add another view to test the JWT authentication. For example:

from rest_framework import status
from rest_framework.response import Response
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from django.contrib.auth.models import User

@api_view(['GET'])
@authentication_classes([JSONWebTokenAuthentication])
def protected_endpoint(request):
    user = request.user
    if user.is_authenticated:
        return Response({'message': 'Access granted'})
    else:
        return Response({'error': 'Access denied'}, status=status.HTTP_401_UNAUTHORIZED)

6. Then you need to map the views in your urls.py file:

from django.urls import path
from .views import login, refresh_token, protected_endpoint

urlpatterns = [
    path('login/', login, name='login'),
    path('token-refresh/', refresh_token, name='token_refresh'),
    path('protected-endpoint/', protected_endpoint, name='protected_endpoint'),
]

You also need to include this URL patterns in your main urls.py:

from django.urls import path, include

urlpatterns = [
    # ...
    path('api/', include('your_app_name.urls')),
    # ...
]

7. For token expiration and refresh token mechanism, you can set the JWT_EXPIRATION_DELTA in your settings.py file. This is the time in seconds after which the token will expire. For example, to set the expiration time to 1 day, you can set it as follows:

JWT_EXPIRATION_DELTA = datetime.timedelta(days=1)

8. On the client side, include the token in the Authorization header of subsequent requests in the format Authorization: Bearer <token>. The JSONWebTokenAuthentication class will then use the token to authenticate the user for those requests.

How to use the above example?

Here is an example of how a client can consume the login() and refresh_token() views described above using the requests library in Python:

import requests

# Send a request to the login endpoint with the user's credentials
response = requests.post('http://localhost:8000/login/', json={'username': 'john', 'password': 'password123'})

# Extract the JWT from the response
token = response.json()['token']

# Use the JWT to authenticate future requests
headers = {'Authorization': 'Bearer ' + token}

# Send a request to a protected endpoint
response = requests.get('http://localhost:8000/protected-endpoint/', headers=headers)

# check if token expired
if response.status_code == 401:
    # Send a request to the token refresh endpoint
    response = requests.post('http://localhost:8000/token-refresh/', headers=headers)
    # Extract the new JWT from the response
    new_token = response.json()['token']
    # Update the headers with the new JWT
    headers = {'Authorization': 'Bearer ' + new_token}
    # Send the request again
    response = requests.get('http://localhost:8000/protected-endpoint/', headers=headers)

# Print the response from the protected endpoint
print(response.json())

In this example, the client first sends a POST request to the /login/ endpoint with the user’s credentials in the request body. If the authentication is successful, the server will respond with a JWT in the format {'token': token}. The client then extracts the JWT from the response and stores it in a variable.

The client then uses the JWT to authenticate future requests by adding the token to the Authorization header in the format Bearer token.

The client then makes a request to a protected endpoint, if the response status code is 401 (Unauthorized) that means the token has expired, the client will send a POST request to the /token-refresh/ endpoint to refresh the token, then extract the new JWT from the response, update the headers with the new JWT and makes the request again.

This is a basic example, in a production-level application, you should also handle errors and validation, and also you need to implement retry mechanism in case of network failure and handle the exception.

How to decode JWT token in DRF?

To validate the token and decode the payload, you can use jwt.decode() method from the pyjwt library.

For example, you can create a file named utils.py and put the decode_token() function in it:

import jwt
from django.conf import settings

def decode_token(token):
    try:
        payload = jwt.decode(token, settings.SECRET_KEY)
    except jwt.ExpiredSignatureError:
        # Token has expired, return an appropriate response
        pass
    except jwt.DecodeError:
        # Token is invalid, return an appropriate response
        pass
    return payload

Then in your views or viewsets, you can import the decode_token() function and use it as needed:

from .utils import decode_token

class MyViewSet(viewsets.ModelViewSet):
    authentication_classes = [JSONWebTokenAuthentication]

    def list(self, request, *args, **kwargs):
        payload = decode_token(request.META.get('HTTP_AUTHORIZATION', '').split()[1])
        if payload:
            # Perform some action
            pass
        else:
            # Return an appropriate response
            pass

The decode_token() is a helper function that can be called in different views or viewsets that need to validate and decode a JWT token.

It can help you to abstract the details of decoding a token and handle the error raised by jwt.decode() method, and return appropriate response to the client.

This is just a sample code, you should implement it with your own custom logic and also need to handle error cases and validation.

It is important to keep the secret key used to sign the token safe, and to use HTTPS to prevent eavesdropping and man-in-the-middle attacks when transmitting the token.

It is also important to take into consideration token expiration and refresh token mechanism to avoid security issues.

Leave a Reply

Your email address will not be published. Required fields are marked *