Django Rest Framework Custom JWT Authentication Backend

JWT Implementation for DRF with Phone Number

Baysan
CodeX
4 min readFeb 4, 2023

--

Hi folks. In this text, I am going to write about how we can implement custom backend in our Django applications and implement JWT. I had a task for one of my freelance jobs to provide functionality for log-in with phone number or username.

Photo by Kelly Sikkema on Unsplash

Roadmap

Firstly, we are going to create our own User model. Then we will create a class to extend BaseAuthentication class. This custom authentication class will be used by IsAuthenticated permission. Also, we will need an APIView to obtain/generate JWT tokens which we call it ObtainTokenView .

Also, I am not going to demonstrate step by step the easiest steps like how to create a project etc. I will be showing the examples on the project that I created for my test.

Custom User Model

I created a User model which is inherited AbstractUser model.

# apps/management/models.py

from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
MALE = 'Male'
FEMALE = 'Female'
GENDER_IN_CHOICES = [
(MALE, 'Male'),
(FEMALE, 'Female'),
]
phone_number = models.CharField(max_length=20, blank=True, null=True, unique=True)
gender = models.CharField(max_length=6, choices=GENDER_IN_CHOICES, null=True, blank=True)
country = models.CharField(max_length=120, null=True, blank=True)
city = models.CharField(max_length=120, null=True, blank=True)
state = models.CharField(max_length=120, null=True, blank=True)
is_approved_to_be_in_touch = models.BooleanField(default=False)

Now we need to set default User model in our settings.py file.

# settings.py

# ...

# custom user model
AUTH_USER_MODEL = "management.User"

# ...

If we need to use User class to use ORM methods etc. we should use get_user_model function.

# any file that needs to use User model

from django.contrib.auth import get_user_model

User = get_user_model()

You can check the documentation below to read something official.

Custom Authentication Backend

We can think that the class below will be used when the app get a request that is need to be authenticated for checking user is authenticated or not. The class below check HTTP_AUTHORIZATION header of request for extracting the token.

The logic in our authentication mechanism is to check is there a user with the username in request body. If there is no user with username, class will filter users by using their phone_number .

It would be nice to remind that the class will be used to check does request have a JWT or not.

# apps/management/authentication.py

from datetime import datetime, timedelta

import jwt
from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework import authentication
from rest_framework.exceptions import AuthenticationFailed, ParseError

User = get_user_model()


class JWTAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
# Extract the JWT from the Authorization header
jwt_token = request.META.get('HTTP_AUTHORIZATION')
if jwt_token is None:
return None

jwt_token = JWTAuthentication.get_the_token_from_header(jwt_token) # clean the token

# Decode the JWT and verify its signature
try:
payload = jwt.decode(jwt_token, settings.SECRET_KEY, algorithms=['HS256'])
except jwt.exceptions.InvalidSignatureError:
raise AuthenticationFailed('Invalid signature')
except:
raise ParseError()

# Get the user from the database
username_or_phone_number = payload.get('user_identifier')
if username_or_phone_number is None:
raise AuthenticationFailed('User identifier not found in JWT')

user = User.objects.filter(username=username_or_phone_number).first()
if user is None:
user = User.objects.filter(phone_number=username_or_phone_number).first()
if user is None:
raise AuthenticationFailed('User not found')

# Return the user and token payload
return user, payload

def authenticate_header(self, request):
return 'Bearer'

@classmethod
def create_jwt(cls, user):
# Create the JWT payload
payload = {
'user_identifier': user.username,
'exp': int((datetime.now() + timedelta(hours=settings.JWT_CONF['TOKEN_LIFETIME_HOURS'])).timestamp()),
# set the expiration time for 5 hour from now
'iat': datetime.now().timestamp(),
'username': user.username,
'phone_number': user.phone_number
}

# Encode the JWT with your secret key
jwt_token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')

return jwt_token

@classmethod
def get_the_token_from_header(cls, token):
token = token.replace('Bearer', '').replace(' ', '') # clean the token
return token

To activate our authentication class we need to set it as a default authentication class of rest framework.

# settings.py

# ...

# REST FRAMEWORK
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'apps.management.authentication.JWTAuthentication',
]
}

# ...

You can check the link below to learn more about BaseAuthentication class.

Obtain Token API Serializer

We will use a simple serializer for our login view.

# apps/management/api/serializers.py

from rest_framework import serializers

class ObtainTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()

Obtain Token API View

The view below will be used to generate JWT token. We try to get user instance by using username or phone_number.

from django.contrib.auth import get_user_model
from rest_framework import views, permissions, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from apps.management.api.serializers import ObtainTokenSerializer
from apps.management.api.serializers import UserSerializer
from apps.management.authentication import JWTAuthentication

User = get_user_model()

class ObtainTokenView(views.APIView):
permission_classes = [permissions.AllowAny]
serializer_class = ObtainTokenSerializer

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)

username_or_phone_number = serializer.validated_data.get('username')
password = serializer.validated_data.get('password')

user = User.objects.filter(username=username_or_phone_number).first()
if user is None:
user = User.objects.filter(phone_number=username_or_phone_number).first()

if user is None or not user.check_password(password):
return Response({'message': 'Invalid credentials'}, status=status.HTTP_400_BAD_REQUEST)

# Generate the JWT token
jwt_token = JWTAuthentication.create_jwt(user)

return Response({'token': jwt_token})

Test The View

We see the successfull login attempts below.

Image by Author | Login via phone number
Image by Author | Login via username

Now you can use the token in Authorization header of request.

AUTHORIZATION: Bearer <TOKEN>

Finally

We have successfully created our own Authentication Backend for providing functionality to be logged-in via username (email) or phone number. Hopefully, it was helpful for you. I know it is a bit short but I do not think that it is necessary to write unnecessary descriptions just for making it long. Especially in the age that we have AI boosted tools.

Kind regards

--

--

Baysan
CodeX
Writer for

Lifelong learner & Developer. I use technology that helps me. mebaysan.com