Open In App

OTP Verification in Django REST Framework using JWT and Cryptography

Last Updated : 08 Aug, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

In this article, we'll explore how to implement OTP verification in Django REST Framework using JWT and cryptography. We will guide you through the process of setting up your Django application to support OTP-based authentication, demonstrate how to integrate JWT for secure token management and show how cryptographic techniques can enhance the overall security of your authentication.

You can check this article to learn how to create a basic API using DRF.

Setting up the Project

Create a virtual environment, and Install Required Packages

pip install django djangorestframework cryptography django-environ pyjwt

App structure

image

Setting.py

Now, there's some additional setup required. We need to configure Rest Framework, JWT authentication, and Email settings. The code is used to configure JWT settings and also the configuration of email functionality. Paste the above code and don't forget to put your own email address and password. A good practice will be to put them in the .env file and load them using the Django-environ package.

Python
# settings.py file
from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(days=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=50),
}

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

# for email functionality
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = <Your Email Address >
EMAIL_HOST_PASSWORD = <Your Password >
EMAIL_USE_TLS = True

serializers.py

Now, in the views.py file, we will create a view to handle the logic of the forgot password. In this view, we will take user's email address and send an OTP in user's mail. So let's create a serializer to take email of the user.

Python
# serializers.py

from rest_framework import serializers

class ForgotPasswordSerializer(serializers.Serializer):
    email = serializers.CharField(max_length=100)

views.py

In the views.py, add the logic to process the user's email, create a token and send token on the mail.

Here, we will first get the email entered by the user and check in the User model if our user already exists. If not, then it will throw an exception. If yes, then we will create a random number using the random module. Then we will create a payload which will be a dictionary and we will pass it to the create token function. The payload contains the user id, user email, otp itself and the expiry. After successfully getting the token, we will send the otp to the user’s email and then send a json response containing the token to the frontend again.

We have used a create_token() function but we have not implemented it yet. We will add the encryption logic of user's detail in this function. The function will take the payload, encrypt the data and returns a token.

Python
# views.py
import random
import datetime

from django.shortcuts import get_object_or_404
from django.core.mail import send_mail
from django.conf import settings
from django.contrib.auth.models import User

from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.response import Response

from .security import create_token, decrypt_token
from .serializers import ForgotPasswordSerializer, CheckOTPSerializer


class ForgetPasswordView(GenericAPIView):
    serializer_class = ForgotPasswordSerializer
    def post(self, request, *args, **kwargs):
        serilaizer = self.serializer_class(data=request.data)
        serilaizer.is_valid(raise_exception=True)

        email = serilaizer.validated_data['email']
        user = get_object_or_404(User, email=email)
        otp = str(random.randint(100000, 999999))
        print(otp)
        payload = {
            'user_id': user.id,
            'email': user.email,
            'otp': otp,
            'exp': datetime.datetime.now() + datetime.timedelta(minutes=5)
        }
        token = create_token(payload)

        send_mail(
            'OTP for Forget Password',
            f'Your Otp is {otp}',
            settings.EMAIL_HOST_USER,
            [user.email],
        )
        return Response({
            'token': token
        }, status=200)

urls.py

Add an url endpoint for the view:

Python
# urls.py
from django.urls import path
from .views import ForgetPasswordView
urlpatterns = [
    path('forgot_password', ForgetPasswordView.as_view(), name='forgot-password'),
]

security.py

Let's now add the logic of create_token and decrypt_token methods. Create a file called security.py file and add the given code:

In the create_token() method, we have used the cryptogrophy module as well as the pyjwt module for creating an enhanced and secured feature for otp verification.

Now lets understand the functionality.

  • Load the .env file using django-environ. The .env file must contain the SECRET key present in the settings.
  • Generate a key using Fernet class. This key will change every time you save and reload the server. To avoid it you can keep in the .env file.
  • cipher_suite=Fernet(key) will create a cipher suit object which has the encrypt and decrypt functions.
  • The create_token function accepts the payload generated earlier and first creates a JWT token out of it using the SECRET_KEY of the django project.
  • This token is then encrypted using the cipher_suite.encrypt() method and now, the encrypted jwt token and thus now we return encrypted_token .
  • This adds a two level security to our data. The JWT token which has the actual data and the encryption which creates a cipher text of the JWT token itself. Hence even after sending to the frontend no one can view or decode the token.
Python
from cryptography.fernet import Fernet
import jwt
import os
import environ
from django.conf import settings

environ.Env.read_env(os.path.join(settings.BASE_DIR, '.env'))
env = environ.Env()

key = Fernet.generate_key()
cipher_suite = Fernet(key)
JWT_SECRET = env("SECRET_KEY")


def create_token(payload):
    token = jwt.encode(payload, JWT_SECRET, algorithm='HS256')
    encrypted_token = cipher_suite.encrypt(token.encode()).decode()
    return encrypted_token


def decrypt_token(enc_token):
    try:
        dec_token = cipher_suite.decrypt(enc_token.encode()).decode()
        payload = jwt.decode(dec_token, JWT_SECRET, algorithms=['HS256'])
        return {'payload': payload, 'status': True}
    except:
        return {'status': False}

Some More Steps

To verify the otp, we will write a separate route named 'CheckOTPView'. In this view, we will take otp and token from the user. Let's create a serializer for the same in your serializers.py file.

Python
# ....

class CheckOTPSerializer(serializers.Serializer):
    otp = serializers.CharField(max_length=6)
    token = serializers.CharField()

Now, add the logic for otp verification in your views.py file.

Python
#...

class CheckOTPView(GenericAPIView):
    serializer_class = CheckOTPSerializer
    def post(self, request, *args, **kwargs):
        
        serialzier = self.serializer_class(data=request.data)
        serialzier.is_valid(raise_exception=True)

        otp = serialzier.validated_data['otp']
        enc_token = serialzier.validated_data['token']

        data = decrypt_token(enc_token)
        if data['status']:
            otp_real = data['payload']['otp']
            if otp == otp_real:
                email = data['payload']['email']
                user = User.objects.get(email=email)
                access_token = str(RefreshToken.for_user(user).access_token)

                return Response(
                    {
                        'access_token': access_token,
                        'status': True,
                    }, status=status.HTTP_200_OK)
            else:
                return Response({
                    'message': 'OTP didnt matched....'
                }, status=status.HTTP_400_BAD_REQUEST)

        else:
            return Response({
                'message': 'OTP expired...Try Again!!',
                'status': False
            }, status=status.HTTP_400_BAD_REQUEST)


Urls.py: Now, add the url route for this view.

Python
# urls.py

# ...

urlpatterns = [
    #....
    path('check_otp', CheckOTPView.as_view(), name='check-otp')
]

In the above code, we have taken the token and otp from the user. Upon successful verification, we are sending access token, else an error response.

The frontend application will save the encrypted token in the local_storage or in use state and whenever the user enters the otp it will send a HTTP POST request to the server and it will include the encrypted token and the otp entered by the user.

  • On receiving the token it will send it to the decrypt_token function.
  • The function will first decrypt the token and this decrypted token will be decoded using jtw.decode() method through which will get the original payload.
  • If the user fails to enter the token within the given time frame of 5 minutes the jwt.decode() will through an exception using which we can identify in the Otp was entered before or after the time ends.
  • If entered later then, we will send a response with status as 400.
  • Else, the Otp entered by the user and present in the payload will be checked and if correct then we will find the user using the User model and generate a new access token using the RefreshToken class .
  • Finally, we will send the access token to the frontend with status 200 OK.

Output:

Screenshot-2024-08-07-123823
Forget Password Endpoint


Screenshot-2024-08-07-123934
Check OTP Endpoint



Next Article
Practice Tags :

Similar Reads