Django Registration Wizardry: Strengthening Security with Email OTP Verification

Django Registration Wizardry: Strengthening Security with Email OTP Verification

ยท

7 min read

1. Introduction

Django is a python-based high level web framework. It's an open source framework and is free to use. If we talk about the architecture, it follows the MVT (Model View Template) architecture. In this article, I assume that you already have some basic knowledge about Django and know about Django's inbuilt user authentication system. This article will walk you through the step by step process of creating a Registration System with Django and Strengthening the Security with Email OTP verification. We'll be creating a REST API for the same using Django REST Framework.

2. Setting Up Our Django Project

We'll start from level zero. I consider creating a virtual environment before starting any project with any tech stack a very good practice to avoid any dependency conflicts. So, let's create our virtual environment first.

python -m venv env

Once we have our virtual environment ready, we'll activate it using the following command. Assuming you're using a Windows system.

env/Scripts/activate

Now, we'll install Django and Django REST Framework to get started with the project

pip install django djangorestframework

Now, we have Django installed in our virtual environment. So, we'll create a Django project using the conventional command

django-admin startproject django_registration

Now, we'll enter the Django project's root directory and create an app for our system.

cd django_registration
python manage.py startapp users

Now we'll add our app and rest_framework to the INSTALLED_APPS sections of the settings.py file.

#settings.py
INSTALLED_APPS = [
    ...
    'users',
    'rest_framework',
    ...
]

Now, we have setup the basic things for our registration system.

We know Django provides it's own User Model for handling all the authentication related things. The inbuilt User Model has the following fields by default

  • username

  • first_name

  • last_name

  • email

  • password

  • Other fields like groups, user_permissions, is_staff, is_active, is_superuser etc

Our goal is to create a registration system that verifies the users using an OTP sent on their email. The former line itself is self-explanatory about the extra fields that we have to add to our User Model. One is a Boolean field, we'll name that is_verified and the other one is Char Field and we'll name that OTP.

So, for that we'll employ the AbstractUser class provided by Django. It basically allows you to add extra fields to the existing User model without completely overriding it. So, in our users app's models.py, we'll write the following code.

# users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    is_verified = models.BooleanField(default=False)
    otp = models.CharField(max_length=6, blank=True, null=True)

Since we use a custom user model now, we'll have to include it in the settings.py file, so that Django will use and treat our custom model for the authentication and authorization purposes. We'll do that using the following line of code,

#settings.py
AUTH_USER_MODEL = 'users.CustomUser'

Since, we're using DRF, we'll have to create a serializer for our User Model. Talk is cheap, right? So, here we go with the code for that as well

#users/serializers.py
from rest_framework import serializers
from .models import CustomUser

class CustomUserSerializer(serializers.ModelSerializer): 
    class Meta:
        model = CustomUser
        fields = ('id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_verified', 'otp')

Done with serialization? Let's move ahead. The next task now is to create our registration API View. So, let's go.

#users/views.py
from rest_framework.-views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import CustomUserSerializer

class UserRegistration(APIView):
    def post(self, request, *args, **kwargs):
        serializer = CustomUserSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()
            # Todo - Generate an OTP and save that to the instance
            user.save()
            # Remember, the user saved here is unverified. Based on that flag, we can restrict access to him.
            # Todo -- add a function to send an email with OTP
            return Response({'message': 'User registered successfully. OTP sent to your email.'}, status=status.HTTP_201_CREATED)  
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

So, we're done with our registration thing, right? No, we ain't. We're yet to implement the logic for verifying a user using an OTP. So, the plan now is, when we save a user once he fills his details, we'll generate an OTP, save it to the otp field of the instance and use Django's send_mail functionality from django.core.mail to send that generated and saved OTP over mail. To keep the code clean, we'll keep the OTP generation and email sending logic in a separate file, but before that, we have to configure our email settings in the settings.py file. So, let's go.

#settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'your-email-host'
EMAIL_PORT = 'your-email-port'
EMAIL_USE_TLS = True  
EMAIL_HOST_USER ='your-email-host-user'
EMAIL_HOST_PASSWORD = 'your-email-host-password'
DEFAULT_FROM_EMAIL = 'your-default-from-email' 

"""
A small comment here to suggest that it's a good practice to keep your
sensitive information in the environment variables.
"""

Now, we're good to write the code for OTP generation and email sending.

#users/utils.py
import random
from django.core.mail import send_mail
from django.conf import settings

def generate_otp(length=6):
    otp_chars = "0123456789" 
    otp = ''.join(random.choice(otp_chars) for _ in range(length))
    return otp

def send_otp_email(email, otp):
    subject = 'Verification OTP'
    message = f'Your OTP for email verification is: {otp}. Please use this OTP to verify your email.'
    from_email = settings.EMAIL_HOST_USER
    to_email = [email]
    send_mail(subject, message, from_email, to_email)

So, basically, in generate_otp we're generating a random 6 character string. The second function is for sending the email to the user. It'd receive the recipient's email address and the otp as an argument. Now what to do with these functions and where to call them is the major question, right? Let us unravel the challenge. Time to edit the API View we created.

#users/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import CustomUserSerializer
from .utils import generate_otp, send_otp_email

class UserRegistration(APIView):
    def post(self, request, *args, **kwargs):
        serializer = CustomUserSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()

            otp = generate_otp()
            user.otp = otp
            user.save()

            send_otp_email(user.email, otp)

            return Response({'message': 'User registered successfully. OTP sent to your email.'}, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

So, what we'll do here is that after validating and saving the serialized data, we'd call our generate_otp buddy to assist us and generate us an OTP, which we'd just write to the user instance's otp field and then save the user instance.

Once saved, we have to send it to the user as well, right? So that he sends this OTP back and we match it with the saved OTP. If they match, we'll say the user is verified and if they don't we say the user is unverified. So, let's first add a URL path for this API.

#users/urls.py
from django.urls import path
from .views import UserRegistration

urlpatterns = [
    path('register/', UserRegistration.as_view(), name='register')
]

Now, we can register a user, but but but. We're yet to write the code for receiving the OTP and verifying the user. So, let's just do that first. Let's edit our views.py file to add that

#users/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import CustomUserSerializer
from .utils import generate_otp, send_otp_email
from .models import CustomUser

class VerifyOTP(APIView):
    def post(self, request):
        email = request.data.get('email')
        otp_entered = request.data.get('otp')

        try:
            user = CustomUser.objects.get(email=email, otp=otp_entered)
            user.verified = True

            user.save()

            return Response({'message': 'Email verified successfully.'}, 
                              status=status.HTTP_200_OK)
        except CustomUser.DoesNotExist:
            return Response({'detail': 'Invalid OTP'}, status=status.HTTP_400_BAD_REQUEST)

class UserRegistration(APIView):
    def post(self, request, *args, **kwargs):
        serializer = CustomUserSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()

            otp = generate_otp()
            user.otp = otp
            user.save()

            send_otp_email(user.email, otp)

            return Response({'message': 'User registered successfully. OTP sent to your email.'}, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

So, we're almost done with our registration system. We just need to map our VerifyOTP API View to a URL and also include our users app's URLs to the project URLs.

#users/urls.py
from django.urls import path
from .views import UserRegistration, VerifyOTP

urlpatterns = [
    path('register/', UserRegistration.as_view(), name='register'),
    path('verify/', VerifyOTP.as_view(), name='verify')
]
#django_registration/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/users/', include('users.urls'), name='users')
]
# Use the admin site to verify whether everything is working fine.

With this, we're all set to use our registration system with email verification system into our projects. Make sure to use the latest versions of the dependencies used.

Comment down if you have any doubts. Also, let me know if I can help with anything else related to Django, DRF, Node or React.

Peace. Dot!

ย