Django Rest Framework Custom JWT Authentication Backend
JWT Implementation for DRF with Phone Number
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.
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.
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