INTRODUCTION
One-Time Password or One-Time Pin (OTP) is a security feature used to provide an extra layer of security for authenticating a user. A one-time password (OTP) as the name implies is a password that is used to validate one login session. This means that a user cannot log in more than once with it. Oftentimes, a One-time password expires after a short period. It is an automatically generated string of characters or integers. There are several reasons developers integrate One-time passwords in their applications. These include:
Email validation
Enhanced security for user authentication
Protection against password theft
Improved user experience with two-factor authentication (2FA)
There are various effective ways to deliver One-Time Passwords (OTPs). Some of these include:
Emails
Verified phone numbers
Simple Mail Transfer Protocol(SMTP) services. Like
- Gmail SMTP Server
- Yahoo SMTP Server
- Outlook SMTP Server
- Amazon SES SMTP Server
- Mailgun SMTP Server
- Sendgrid SMTP Server
- Zoho SMTP Server
- AOL SMTP Server
- Verizon SMTP Server
- AT&T SMTP Server
In this article, OTP will be implemented using a Zoho SMTP server.
PREREQUISITES
Python
Django Rest Framework (DRF)
SET UP THE PROJECT ENVIRONMENT
There are some basic setups you need before starting a Python project. Create and activate a virtual environment. Follow these steps to create a virtual environment with the code below.
- Create a virtual environment
python -m venv venv
For versions of Python below 3.12.1, use the code below
python3 -m venv {venv name}
The number ‘3’ is the major difference between the commands. Henceforth, always add ‘3’ to each Python command you run if you use a lower version of Python as this article will be using python3.12.1. You can choose to name your virtual environment with whatever name you like but for simplicity's sake, venv is used.
- Activate the virtual environment
venv/Scripts/activate
- Install Django and Django Rest Framework (DRF) within the virtual environment. Install django with this command below
pip install django
- And Django Rest Framework (DRF) with the command below
pip install djangorestframework
- Install rest framework jwt
pip install djangorestframework-simplejwt
BUILD A SAMPLE PROJECT
- Create a sample project to test the OTP implementation.
django-admin startproject sampleproject
This will create a folder called
sampleproject
that contains the basic structure of a django project in it.
- Optionally, rename the nested
sampleproject
tocore
to simplify the project structure.
Rename-Item -Path "C:\Users\Desktop\otp-implementation\sampleproject\sampleproject" -NewName "core"
- Navigate to
manage.py
file and configure your project to point to the core.
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') #change this to point core
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
Save and exit
- Navigate to
settings.py
file and update theROOT_URLCONF
ROOT_URLCONF = 'core.urls'
- Update
WSGI_APPLICATION
to point to core insettings.py
Save and exitWSGI_APPLICATION = 'core.wsgi.application'
- Navigate to
settings.py
in corecd core
- Configure DRF to use simple jwt in
settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}
Save and exit.
- Open
urls.py
file incore
and add SimpleJWT URLs.
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
...
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
...
]
Save and exit
- Navigate into your project directory
cd sampleproject
- Create an
accounts
app within your project directory.
django-admin startapp accounts
This will create a folder called
accounts
with the basic structure of an app in it.
- Navigate to the
core
directory
cd core
- Include
accounts
to the list ofINSTALLED_APPS
insettings.py
# ... other configurations
INSTALLED_APPS = [
'daphne',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'accounts', #new
'rest_framework', #new
'rest_framework_simplejwt', #new
]
Save and exit
- Navigate to the
accounts
directory
cd ../accounts
- Create a sample model for
accounts
inmodels.py
file
class User(AbstractUser):
""" Database model for users in the system """
username = None
email = models.EmailField(max_length=255, unique=True)
full_name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now=True)
updated_at =models.DateTimeField(auto_now_add=True)
objects = UserProfileManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def __str__(self):
""" Return string representation of our user """
return self.email
Save and exit
This is a sample user model that inherits from the Abstract model and creates a column in the database column for username, email and password. The database can be structured to the desired fields of your choice
- Run migrations to create a migration file, which represent the changes made to the database model
python manage.py makemigrations
- Apply these changes to the database
python manage.py migrate
The command above will apply all changes to the database
- Create a
serialializer.py
file within your app directory.
from django.contrib.auth import authenticate
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()
class RegisterUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['pk', 'full_name', 'email', 'password']
extra_kwargs = {
'password': {'write_only': True},
'pk': {'read_only': True},
}
def create(self, validated_data):
password = validated_data.pop('password', None)
instance = self.Meta.model(**validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
The serializer class inherits from the base Modelserializer. It serializes the
User
model that was created in themodel.py
and for the fields, serializespk
,full_name
,password
. Theextra_kwargs
ensures that the password is write_only and thepk
isread_only
.
Save and exit
- Open the
views.py
file within theaccounts
directory, to create a register user logic.
from rest_framework.response import Response
from rest_framework import generics, status
from rest_framework.exceptions import ValidationError
from rest_framework_simplejwt.tokens import RefreshToken
from .serializers import RegisterUserSerializer
from django.contrib.auth import get_user_model
User = get_user_model()
class UserCreateAPIView(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = RegisterUserSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
user = serializer.save()
refresh = RefreshToken.for_user(user)
user.save()
return Response({
'user': {
'id': user.id,
'email': user.email,
'full_name': user.full_name,
},
'token': {
'refresh': str(refresh),
'access': str(refresh.access_token),
},
}, status=status.HTTP_201_CREATED)
except ValidationError as e:
if 'email' in str(e) and 'unique' in str(e):
return Response({'error': 'User with this email already exists.'}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
user_create = UserCreateAPIView.as_view()
This is a sample logic to register a new user, It inherits from the
generics.CreateAPIView
, fetches allUser
from the database then uses theRegisterUserSerializer
that was created in the serializers.py file to serialize and deserialize all incoming and outgoing data. There is atry and except
block that handles errors more gracefully. Within thetry
block, the logic for creating a user is implemented, also a simple_jwt token is used for the token-based authentication. Theexcept
block catches possible errors and failures, It handles errors when the email is not unique and other possible server errors. It is best practice to handle errors in your code and one way this can be done in Python is to use thetry
andexcept
block
- Navigate to your
settings.py
file withincore
cd ../core
- Open the
settings.py
file and configure your accounts user
AUTH_USER_MODEL = 'accounts.User'
This allows django to use the new custom user class which is
accounts.User
Save and exit
- Include accounts' url to the
sampleproject
urls.py
in theaccounts
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/accounts/', include('accounts.urls')), #new
path('api/accounts/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/accounts/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
Save and exit
- Run your server to make sure everything is working fine with the command below
python manage.py runserver
Navigate to your browser to see if your server is running as it should.
Implementing OTP In Django Rest Framework
Do the basic Zoho setup to integrate OTP on your django rest framework.
Set up Zoho
Go to the official Zoho site OFFICIAL WEBSITE and create an account if you do not have one. Configure your Zoho server with your preferred name and get the credentials needed for SMTP configuration. This includes port
, host
, password
etc. This is sensitive information that you should not expose to the general public. Using a .env
file to store sensitive information like these is best practice.
Set UP Environment Variables
python-dotenv
is a Python third-party package that can be used to load files directly from your local machine. It reads key-value pairs from a .env file Install python-dotenv
using pip
pip install python-dotenv
Create a .env
file in your project directory and add your Zoho credentials to it. This .env
file should not be committed to your version control system (git) for security reasons. The content of your .env
file should look like the code block below
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.zoho.eu
EMAIL_HOST_PASSWORD=yourpassword
EMAIL_HOST_USER=yourmail@zohomail.eu
EMAIL_PORT=465
EMAIL_USE_TLS=1
sender=sampleproject@zohomail.eu
This is a sample configuration for the zoho mail server. You should replace the placeholders with your actual Zoho mail server credentials.
Since your configuration is obtained from environment variables, you have to set those environment variables yourself. add Python-dotenv to your application to make it load the configuration from a .env
in your settings.py
within your project directory with the code below:
from dotenv import load_dotenv
load_dotenv()
The code above imported the
load_dotenv
fromdotenv
and then loaded the dotenv withload_dotenv()
. Theload_dotenv()
takes environment variables from the .env file and loads them into the environment. This is done at the beginning of your settings.py file to ensure that the environment variables are loaded before the rest of the settings are configured.
Proceed to load the environment variables in your settings.py
file. This can be done close to where the database configuration is. The configuration can be done with the code block below
import os
# ... other configurations
# Email configuration
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND')
EMAIL_USE_TLS = bool(os.getenv('EMAIL_USE_TLS'))
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
EMAIL_PORT = os.getenv('EMAIL_PORT')
sender = os.getenv('sender')
auth = os.getenv('auth')
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL')
In the code block above,
os. getenv
is used to load the environment variables as it came from the actual environment
Save and exit.
IMPLEMENT OTP IN AUTH
Create A Sample Function To Generate and Send OTP
Python has a built-in library used in sending emails via a Simple Mail Transfer Protocol (SMTP) called smtplib. It provides classes and methods to create email messages, connect to an SMTP server, and send these messages Install this package with the code below:
pip install smtplib
Navigate to your accounts.py
directory
cd accounts
Update the models.py
within your accounts
directory to include the OTP field.
from django.db import models
class User(AbstractUser):
# other fields
otp = models.CharField(max_length=6, blank=True, null=True)
otp_verified = models.BooleanField(default=False)
Save and exit
Create a new file named utils.py
and add the following code to it:
import random
import string
def generate_otp(length=6):
characters = string.digits
otp = ''.join(random.choice(characters) for _ in range(length))
return otp
The function above generates a random One-Time Password (OTP) consisting of the length of six(6) numeric digits. It uses the random module to select random digits from the string.digits collection and joins them into a string. The function then returns this string as the OTP.
Within your utils.py
create another function that handles sending of emails using Zoho serve with the code below
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
import smtplib
from core.settings import auth, sender,EMAIL_HOST, EMAIL_PORT
from email.mime.text import MIMEText
def Send_email_with_zoho_server(to_email, message):
print('hello you!')
msg = MIMEText(message)
msg['Subject'] = "OTP from Sample Projects"
msg['From'] = sender
to=[to_email],
server = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT)
server.login(sender, auth)
server.sendmail(sender, to, msg.as_string())
server.quit()
In the code block above, the built-in Python SMTP library was imported. the email credentials that were configured in the
settings.py
file were imported. A sample function that sends emails was created. At the end of the function, a connection with the email host and port was created, when the process is done running, the server gets quit with theserver.quit()
method.
Save and exit.
In UserCreateAPIView
in the views.py
file, call the functions created in your utils.py
. This will send an OTP to a user's email when an account is created. Navigate to your views.py
within the accounts
app and consume the generate_otp
and Send_email_with_zoho_server
created in the utils.py
file
# ... other importations
from .utils import generate_otp, Send_email_with_zoho_server
User = get_user_model()
class UserCreateAPIView(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = RegisterUserSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
user = serializer.save()
refresh = RefreshToken.for_user(user)
otp = generate_otp() #new
user.otp = otp #new
Send_email_with_zoho_server( #new
to_email=user.email,
message=f'Your OTP for account verification is: {otp}' #new
)
user.save()
return Response({
'user': {
'id': user.id,
'email': user.email,
'full_name': user.full_name,
},
'token': {
'refresh': str(refresh),
'access': str(refresh.access_token),
},
}, status=status.HTTP_201_CREATED)
except ValidationError as e:
if 'email' in str(e) and 'unique' in str(e):
return Response({'error': 'User with this email already exists.'}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
user_create = UserCreateAPIView.as_view()
In the above code, we have added the
generate_otp
andSend_email_with_zoho_server
which handle the OTP generation and sending the OTP to the user's email respectively. The OTP is generated and saved in the user model. The email is sent to the user with the OTP using theSend_email_with_zoho_server
The OTP is sent in the email body.
Validate OTP
Create a function that would be used to validate OTP in accounts
views.py
. With the code below
# ...Other importations
from django.utils import timezone
class ValidateOTP(APIView):
def post(self, request):
email = request.data.get('email', '')
otp = request.data.get('otp', '')
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return Response({'error': 'User with this email does not exist.'}, status=status.HTTP_404_NOT_FOUND)
print(user.otp)
if user.otp == otp:
# check if token has expired
time_difference = max(user.created_at, user.updated_at)
mins_difference = (
timezone.now() - time_difference
).total_seconds() / 60
if mins_difference > 2:
response = {
"response_status": "error",
"status_code": status.HTTP_400_BAD_REQUEST,
"message": "OTP token expired. Try again.",
}
return Response(response, status=status.HTTP_400_BAD_REQUEST)
user.otp_verified = True # Mark OTP as verified
user.otp = None
user.save()
# Authenticate the user and create or get an authentication token
# token, _ = Token.objects.get_or_create(user=user)
return Response({'message': 'Account verified successfully'}, status=status.HTTP_200_OK)
else:
return Response({'error': 'Invalid OTP.'}, status=status.HTTP_400_BAD_REQUEST)
The
ValidateOTP
function handles OTP validation. It checks if the user's OTP matches the OTP sent, the function also handles OTP expiration sets the lifespan to 60 seconds and marks the OTP as verified.
Resend OTP
Since the ValidateOTP
function handles a very short lifespan of the OTP, create a function that would be used to resend OTP in accounts
views.py
. With the code block below:
class ResendOtpView(APIView):
def patch(self, request):
"""Resends a new OTP to the registered Email ID
Payload:
{
"email": "user@example.com"
}
"""
email = request.data.get('email')
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
response = {'response_status':'error',
'status_code':status.HTTP_404_NOT_FOUND,
'message':'User does not exist'}
return Response(response, status=status.HTTP_404_NOT_FOUND)
if user.otp is None:
response = {
'message': "Your account already verified"
}
return Response(response, status=status.HTTP_400_BAD_REQUEST)
otp = generate_otp()
user.otp=otp
user.save()
Send_email_with_zoho_server(message=otp, to_email=email)
response ={
'message': 'new otp has been sent to your email!',
'status': status.HTTP_200_OK
}
return Response(response, status=status.HTTP_200_OK)
The
ResendOtpView
basically handles the logic to resend OTP. A User might need to be sent another OTP when the initial OTP expires. This function is called upon to handle resend.
Save and exit.
Update urls.py file.
from django.urls import path
from . import views
from search.views import SearchView
from .views import ValidateOTP, ResendOtpView, UserLoginAPIView, Testemail, PasswordResetAPIView, validate_password_reset_otp
# from search import SearchView
urlpatterns = [
path('create/', views.user_create, name='user_create'),
path('validate-otp/', ValidateOTP.as_view(), name='validate-otp'),
path('resend-otp/', ResendOtpView.as_view(), name='resend-otp'),
]
The
urls.py
file imported the path fromdjango.urls
and from views, the functions we created earlierValidateOTP
,ResendOtpView
,ResendOtpView
were imported. Theurlpatterns
is a list of routes mapped to the logic.
Save and exit. Run your server
python manage.py runserver
Now you can test your API using Postman or any other API testing tool.
#IMAGE
Conclusion
There are several reasons OTP is integrated into an application.
Security: OTP adds an extra layer of security to the application. It ensures that only authorized users can access the application.
Verification: OTP helps to verify the user's email address or phone number. This ensures that the user's contact information is valid and can be used for future communication.
Prevents Automated Attacks: OTP prevents automated attacks like brute force attacks. It ensures that only a human can access the application.
This article works you through a step-by-step guide on how to implement OTP in your DRF project. The richness of the dotenv was explored. For more information about the dotenv, visit the official website