Django Registration Wizardry: Strengthening Security with Email OTP Verification
Table of contents
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!
Subscribe to my newsletter
Read articles from Musaib Altaf directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Musaib Altaf
Musaib Altaf
๐ Hey there! I'm Musaib Altaf, a passionate Full Stack Engineer and Product Engineering Consultant based in Sopore, Kashmir, India, currently working remotely at Polynomial.AI. ๐จโ๐ป With a strong foundation in Computer Science and Engineering from the IIIT Bhubaneswar, I specialize in crafting robust web solutions using technologies like Node.js, React.js, Express.js, Socket.io, Django and Django REST Framework. My experience spans from internships to full-time roles, honing skills in Node.js, React.js, Express.js, Socket.io, Django, Python, Redis, and API testing using Pytest. ๐ ๏ธ Previous engagements include roles at Shiksha Sopan IIT Kanpur, SkroPay, Pune International Literary Festival, and Mirchal Sir's Tutorials, where I've contributed as a Full Stack Engineer, automating API testing, developing full-stack web applications, and leading development teams. ๐ Passionate about mental health advocacy, I co-founded Your Friendd, an online mental health consultation platform, aiming to provide accessible counseling to those in need. Additionally, I've collaborated on projects like PILF 2022, integrating backend technologies and deploying web applications successfully. ๐ Beyond coding, I volunteered as a Web Development Lead at GDSC IIIT-BH, conducting educational sessions on cutting-edge web technologies. Serving as the Secretary at TARS IIIT-Bh, I was dedicated to fostering scientific innovation. Let's connect and explore opportunities to innovate and create impactful solutions together!