Build a Passwordless Authentication System Using Django, Twilio Verify, and SendGrid

July 18, 2022
Written by
Ashi Garg
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Mia Adjei
Twilion

Build a Passwordless Authentication System Using Django, Twilio Verify, and SendGrid

Requiring passwords to sign up for a service has many demerits, such as a high chance of passwords being stolen and requiring users to remember passwords all the time. On the contrary, a passwordless authentication system has many benefits. For instance, it saves users from being a victim of the most common attack—the Brute Force Attack. Additionally, many users have a tendency to use the same password for multiple websites/applications, which then can lead to a Credential Stuffing Attack. A passwordless authentication system helps save users from such an attack as well.

A passwordless authentication system lets users access the applications by verifying their identity using a secure token, biometric signature or any other secure proof of identity which is not knowledge based or does not require any private information.

In this tutorial, you will learn how to create a passwordless authentication system using Twilio Verify, SendGrid, Django, and Python. The proposed authentication system will require users to verify their identity using a one-time password.

Prerequisites

In order to follow along with this tutorial, you will need the following:

  • A Twilio account and a SendGrid account
  • Working knowledge of Python
  • Basic knowledge of Django Model-View-Template (MVT) structure

Setting up the project

Let’s start by creating a virtual environment for our project. Navigate to where you would like to set up your project. Create a new directory for your project, and change into the directory.

If you are on Windows, run the commands below to create a virtual environment:

python -m venv myvenv

Navigate to the Scripts folder.

cd myvenv/Scripts

Start the virtual environment.

. ./activate

Then, navigate back to the main folder.

However, if you are working on Linux or macOS, run these commands instead.

python3 -m venv myvenv
source myvenv/bin/activate

Next, install Django and Twilio in the virtual environment.

pip install django twilio

Now, let's create a Django project named “myshop”.

django-admin startproject myshop

We will be creating a shopping app as part of this tutorial. Inside this shopping app, we will create different apps to manage different tasks as a part of this project “myshop”.

Navigate to the myshop folder.

cd myshop

Now, let's start by creating our first app which will manage:

  • User registration
  • User login
  • User verification using a one-time password (OTP) every time user tries to log in
python manage.py startapp verification 

Add the verification app in the "INSTALLED_APPS" field (inside myshop >> settings.py):


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'verification'
]

After this, we are all set to jump into the next steps and start the actual work!

Create a custom user model

For the purpose of creating a passwordless user authentication system, we need to create a custom user model. To do that navigate to: myshop >> verification >> models.py.

Create a NewUser class in the models.py file which inherits from AbstractBaseUser and PermissionsMixin.

Note: Inheriting from AbstractBaseUser provides the core implementation of a user model, and inheriting from PermissionsMixin provides our model access to all the methods and database fields necessary to support Django’s permission model.

Add the code snippet below, which defines all the required fields:

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.core.validators import RegexValidator


class NewUser(AbstractBaseUser, PermissionsMixin):

    email = models.EmailField(unique=True)
    username = models.CharField(max_length=150, unique=True)
    phone_regex = RegexValidator(regex=r'^\+?1?\d{9,15}$', message="Please enter a valid phone number")
    phone_number = models.CharField(validators=[phone_regex], max_length=17, blank=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=False)
   
    objects = NewUserAccountManager()
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username', 'phone_number']

    def __str__(self):
        return self.email

In the code snippet above, we:

  • Set the is_staff and is_active fields to False by default.
  • Define the USERNAME_FIELD as email. (By doing so, we are setting email as a unique identifier) for our User model.
  • Define the REQUIRED_FIELDS, for example: username and phone number.

All objects for this class (NewUser) come from the NewUserAccountManager. As a next step, let’s create the NewUserAccountManager class in the models.py file. This class will define all the methods required to create a user. It will inherit from BaseUserManager.

This class will have two methods: create_user and create_superuser.

Although we do not need passwords for our regular users, we do need a password for our Django admin. So, for that we will check if the password is None. If it is None (while creating a user we will pass in the value as None), then we will set the password as unusable password.

In the case of the create_superuser method, we will set certain fields as True (is_staff, is_superuser, is_active). And additionally, we will set a password for the admin/superuser.

Add the following code snippet to models.py, just above the code you added for the NewUser class:

class NewUserAccountManager(BaseUserManager):

    def create_superuser(self,username, email, phone_number,password, **other_fields):
        other_fields.setdefault('is_staff', True)
        other_fields.setdefault('is_superuser', True)
        other_fields.setdefault('is_active', True)
      
        if other_fields.get('is_staff') is not True:
            raise ValueError('Superuser must be assigned to is_staff=True')
       
        if other_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must be assigned to is_superuser=True')
        user =  self.create_user(username,email, phone_number, password, **other_fields)
        user.set_password(password)
        user.save()
        return user

    def create_user(self, username, email, phone_number,password,**other_fields):
        if not email:
            raise ValueError('Email address is required!')
        email = self.normalize_email(email)
        if password is not None:
            user = self.model(username=username,email=email, phone_number=phone_number,password=password, **other_fields)
            user.save()
        else:
            user = self.model(username=username,email=email, phone_number=phone_number, password=password,**other_fields)
            user.set_unusable_password()
            user.save()

        return user

Finally, register the NewUser model inside the verification >> admin.py file.

from django.contrib import admin
from .models import NewUser

admin.site.register(NewUser)

Define the custom user model in settings.py

Add AUTH_USER_MODEL = 'verification.NewUser' in the myshop >> settings.py file to register your newly created custom user model:

AUTH_USER_MODEL = 'verification.NewUser'

Now let's make and perform migrations. Run the following code on your command line:

python manage.py makemigrations
python manage.py migrate

Now let's try creating a superuser!

Run the command below:

python manage.py createsuperuser

Specify the username, email address, and password:

The user enters their information and password to create a super user in the command prompt

After you have created a superuser, run the Django server with the following command:

python manage.py runserver

By default, the server will run on port 8000. Navigate to http://127.0.0.1:8000/admin/ in your browser, and use your superuser credentials to log in to the Django admin. Once you log in, you will see this table:

Screenshot of django administration page

If you click on "New users", you will see that a New users table has been created with an admin user.

Django administration page showing the admin user is added to the users table

Create Django forms

Now that we are done with user model creation, let's continue by creating our registration form.

Create a forms.py file in the verifications folder. Create a class called RegisterForm and define all the required fields we need in the registration form.

Quick note about Django forms: Using Django forms makes it easy for us to do validation of all the fields that we need in the form.

According to the Django docs: A Django form instance has an is_valid() method which validates all the fields. When we call this method and all the fields are validated correctly, then it returns True and places all the form data in its cleaned_data attribute.

Add the following code to forms.py:

from django import forms
from django.contrib.auth import get_user_model
User = get_user_model()

class RegisterForm(forms.Form):
    username = forms.CharField(label="Username")
    email = forms.EmailField(label="Email")
    phone_number = forms.CharField(label="Enter your phone number here")

Although the input validation is already checked with the help of Django forms, let’s now create three methods inside our class to check if the user with the given email, username, or phone_number already exists.

Update your RegisterForm class with the following highlighted lines:


class RegisterForm(forms.Form):
    username = forms.CharField(label="Username")
    email = forms.EmailField(label="Email")
    phone_number = forms.CharField(label="Enter your phone number here")
   
    def clean_username(self):
        username = self.cleaned_data.get('username')
        user_details = User.objects.filter(username=username)
        if user_details.exists():
            raise forms.ValidationError("Username is already taken")
        return username

    def clean_email(self):
        email = self.cleaned_data.get('email')
        user_details = User.objects.filter(email=email)
        if user_details.exists():
            raise forms.ValidationError("There is already an account with this email , please try logging in!")
        return email
    
    def clean_phone_number(self):
        phone_number = self.cleaned_data.get('phone_number')
        user_details = User.objects.filter(phone_number=phone_number)
        if user_details.exists():
            raise forms.ValidationError("An account with this phone number already exists! Please try registering using a different phone number or try logging in!")
        return phone_number

Similarly, let’s now create a login form. Create a class called LoginForm just below your code for RegisterForm, and define the required fields. (For the purpose of this tutorial, we only require one field for the login form — the email field.)

Additionally, create one method to check if the account with the given email address exists or not:

class LoginForm(forms.Form):
    email = forms.EmailField(label="Email")
    def clean_email(self):
        email = self.cleaned_data.get('email')
        user_details = User.objects.filter(email=email)
        if not user_details.exists():
            raise forms.ValidationError("No account exists with this email. Try registering")
        return email

Create registration view

Now head to the verification >> views.py file and create a registration view.

from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, get_user_model
from .forms import RegisterForm

User = get_user_model()

def register_page(request):
    form = RegisterForm(request.POST or None)
    context = {
        "form": form
    }

    if form.is_valid():
        username = form.cleaned_data.get("username")
        email = form.cleaned_data.get("email")
        phone_number = form.cleaned_data.get("phone_number")
        new_user = User.objects.create_user(username, email, phone_number, password=None)
        return redirect("/login")
    return render(request, "auth/register.html", context)

Here is what is happening in the code snippet above:

  • In the register_page view, we create a form instance (of the RegisterForm class, which we created above).
  • We check if the form is valid or not before processing the form data.
  • If the form is valid, we process the form data.
  • Finally, we call the create_user method and save the user in the database.
  • After that, we redirect the user to the login page. (We will create it afterwards.)
  • Render the form using auth/register.html file (yet to create).

Before creating other views (login view, view to generate OTP and check OTP), we need to set up our SendGrid and Twilio accounts.

Setup Twilio and SendGrid accounts

  • Go to the SendGrid website.
  • For this tutorial, select a free plan, provide required details, and sign up!

After signing up, navigate to the Dashboard, and go to : Settings >> Sender Authentication.

Perform the sender authentication (single sender verification in this case) by verifying your email address. After verification, create a new sender by filling in the required details.

Now, go to Settings >> API Keys, and create an API key. Give your API key a name, and select "Full Access". After creating the API key, copy it and save it in a secure place because it will not be revealed to you again.

Now it's time to create an email template.

  • Go to Email API >> Dynamic Templates.
  • Click on “Create a Dynamic Template”.
  • Give your template a name. (Example: User-verification)
  • After that, select your template name and click on “Add Version”.
  • Choose a blank Email Template for now.
  • Select “Code Editor”.

Change the code in the editor to the HTML below, and save:

<html>
  <head>
    <style type="text/css">
      body, p, div {
        font-family: Helvetica, Arial, sans-serif;
        font-size: 14px;
      }
      a {
        text-decoration: none;
      }
    </style>
    <title></title>
  </head>
  <body>
  <center>
   
    <p>
      The verification code is: <strong>  {{twilio_code}} </strong>
    </p>
    <p>
     
    <span style="font-size: 10px;"><a href=".">Email preferences</a></span>
  </center>
  </body>
</html>

In this code, we use the template variable: {{twilio_code}} to send an OTP to the user.

Now, click on "Settings" on the left side of the template.

SendGrid Dynamic Version settings

In the Settings panel, give the template version a name (Example: User Verification v1 or simply version-1). Additionally, you can specify a suitable subject for the email. For instance: ‘Your One Time Password (OTP) for myshop’. You can also try sending test emails, by specifying more than one email address, such as: test1@gmail.com, test2@gmail.com, test3@gmail.com.

After creating the template version, a template id will be created. (You will use this for the email integration later in the tutorial.)

Now create a Twilio account by visiting Twilio’s website.

  • Create a Twilio Verify service by navigating to Verify >> Services, and clicking the "Create new" button.
  • Give the service a friendly name (Example: ‘Passwordless-auth’), and enable Email as a verification channel.
  • Create an email integration by navigating to Verify >> Email Integration in your Twilio account console and clicking the Create Email Integration button. Give your email integration a name, and click the Continue button.
  • You will see a form where you will be required to add your SendGrid API key, template id (You can get your template id by logging in to the: SendGrid's website and navigating to Email API >> Dynamic Templates. Here you will find Template Id under your template name), from email (make sure it is the same as the email you verified in the SendGrid account), and from name. Enter these values and click the Save button.
  • Navigate to Verify >> Services again, select the service you created, head to the Email tab, and enable the verification channel. Then, update the integration setting by selecting the email integration you just created.
  • Finally, assign the service in the email integration.

Set up the environment file

Next, you need to define the environment variables for your project.

Install “python-dotenv” in the activated virtual environment.

pip install python-dotenv

Next, create an .env file in the myshop project folder (inside the main myshop folder: myshop >> myshop) and add the following variables:

SECRET_KEY = "add secret key here"
EMAIL_HOST = "smtp.sendgrid.net" 
EMAIL_PORT = 587 
EMAIL_USE_TLS = True 
EMAIL_HOST_USER = "apikey"
EMAIL_HOST_PASSWORD = "add sendgrid api key here"
DEFAULT_FROM_EMAIL = "add from email address here"
LOGIN_REDIRECT_URL = "/home"
ACCOUNT_SID = 'add account sid here'
AUTH_TOKEN = "add auth token here"
SERVICE_ID = 'add service id here'
TEMPLATE_ID = 'add template id here'

Add all the variables and their values in the .env file, including your secret key.

Instructions to get the values for .env file:

  • The value of SECRET_KEY is present by default in the settings.py file of your project ( myshop >> myshop >> settings.py).
  • Add the SendGrid API key, which you created in the Setup Twilio and SendGrid accounts section, as the value for: EMAIL_HOST_PASSWORD.
  • You can find values of ACCOUNT_SID and AUTH_TOKEN under the “Account Info” section by visiting Twilio’s console.

Account Info box with two read-only fields "Account SID" and "Auth Token"
  • To get the SERVICE_ID, visit Twilio console and navigate to Verify >> Services. You can now copy Service SID from the services table.

Twilio Services page displaying a verify service with read only field "Service SID"
  • Lastly, follow the instructions given in the section Setup Twilio and SendGrid accounts to get the value for TEMPLATE_ID.

Now, import os in settings.py, and do the initialisation to read environment variables from the .env file:


from pathlib import Path
from dotenv import load_dotenv
import os 

load_dotenv()

Replace the value of the secret key and other values in myshop >> settings.py with the format os.getenv('KEY_NAME'), as shown below:

##email details
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_PORT = os.getenv('EMAIL_PORT')
EMAIL_USE_TLS = os.getenv('EMAIL_TLS')
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL')

##other details
LOGIN_REDIRECT_URL = os.getenv('LOGIN_REDIRECT_URL')
ACCOUNT_SID = os.getenv('ACCOUNT_SID')
AUTH_TOKEN = os.getenv('AUTH_TOKEN')
SERVICE_ID = os.getenv('SERVICE_ID')
TEMPLATE_ID = os.getenv('TEMPLATE_ID') 

Create a custom authentication backend

Since we want to authenticate users without using a password, we need to create a custom authentication backend.

Some useful points about authentication backend in Django: According to Django official docs, an authentication backend is a class which implements two required methods: get_user(user_id) and authenticate(request, **credentials).

The authenticate() method should check the credentials it gets and return a user object that matches those credentials if the credentials are valid. If they’re not valid, it should return None.

To do this, create a Python file called auth_backend.py in the verification directory. In that file, create two methods: get_user() and authenticate(), as required:

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model

class PasswordlessAuthBackend(ModelBackend):
    """Log in to Django without providing a password.
    """
    def authenticate(self, request, email):
        User = get_user_model()
        try:
            user = User.objects.get(email=email)
            return user
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        User = get_user_model()
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

By default Django uses django.contrib.auth.backends.ModelBackend as a backend. Since we created a custom authentication backend, we need to add it to our settings.py.  

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'verification.auth_backend.PasswordlessAuthBackend',
)

Now, we are all set to create the remaining views!

Create login view

To create a login view, we first need to create a send_code() function which will send an OTP to the user.

  • Create a file called verify.py in the verification directory.
  • Import Client from twilio.rest, and import all required environment variables.
  • Add the SendOTP class with the send_code() method.
from twilio.rest import Client
from django.conf import settings
class SendOTP:
    def send_code(receiver):
        account_sid = settings.ACCOUNT_SID
        auth_token = settings.AUTH_TOKEN
        client = Client(account_sid, auth_token)

        verification = client.verify \
                     .services(settings.SERVICE_ID) \
                     .verifications \
                      .create(channel_configuration={
                           'template_id': settings.TEMPLATE_ID,
                           'from': settings.DEFAULT_FROM_EMAIL,
                           'from_name': 'Ashi Garg'
                       }, to=receiver, channel='email')

        return verification.status

Now, in verification >> views.py, import uuid, the LoginForm that you created, and the SendOTP class from the verify.py file:


import uuid
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, get_user_model
from django.contrib import messages
from .forms import RegisterForm, LoginForm
from .verify import SendOTP

Just below the register_page() view, add the following login_page() view:

def login_page(request):
    form = LoginForm(request.POST or None)
    context = {
    "form": form
    }
    if form.is_valid():
        email = form.cleaned_data.get('email')
        try:
            new = User.objects.get(email=email)
            ## if user exists
            ##first: send otp to the user 
            SendOTP.send_code(email)
            ##second:redirect to the page to enter otp 
            temp = uuid.uuid4()
            return redirect("/otp/{}/{}".format(new.pk, temp))
        except Exception as e:
            messages.error(request, "No such user exists!") 
    
    return render(request, "auth/login.html", context)

After the OTP is sent to the user, we now need to verify the OTP.

For that we need two views: one which will display the OTP form, and another which will check the OTP.  

Create generate_otp view

def generate_otp(request, pk, uuid):
    return render(request, 'otp.html')

This view will just render the OTP form when the user is redirected to the url: /otp/pk/uuid after logging in, where pk is the primary key of the user and uuid is the temporary generated uuid.

Create check_otp view

Now, to check the OTP, we need to create a CheckOTP class and check_otp() function, which will verify the OTP given the email address and OTP.

Create a check_code.py file inside verification directory and add the following code:

import os
from twilio.rest import Client
from django.conf import settings

# Find your Account SID and Auth Token at twilio.com/console
# and set the environment variables. See http://twil.io/secure

class CheckOTP:
    
    def check_otp(email, secret):
        account_sid = settings.ACCOUNT_SID
        auth_token = settings.AUTH_TOKEN
        client = Client(account_sid, auth_token)
        verification_check = client.verify \
                           .services(settings.SERVICE_ID)\
                           .verification_checks \
                           .create(to=email, code=secret)

        return verification_check.status

Now let's create our check_otp view in views.py which will:

  • Get the email address and OTP entered by the user.
  • Pass those values to our check OTP function.
  • If the OTP is correct, it will authenticate the user and redirect the user to our home page.

At the top of views.py, add the CheckOTP class to your list of imports:

from .check_code import CheckOTP

Then, below your other views, add a view for check_otp:


def check_otp(request):
    otp =request.POST.get("secret")
    email = request.POST.get("email")
    otp_status= CheckOTP.check_otp(email, otp) 
    if otp_status == "approved":
        user = authenticate(request, email=email) 
      
        if user is not None:
           login(request, user, backend='verification.auth_backend.PasswordlessAuthBackend')
           return redirect("/home")
        else:
            messages.error(request, "Wrong OTP!") 

    print("otp via form: {}".format(otp))
    return render(request, "otp.html")

Note that in the login method, we are specifically defining the authentication backend we want to use as verification.auth_backend.PasswordlessAuthBackend.

Create home view

Now, the last view that we need to create is the home view. It will just render the home_page.html template and show premium content if the user is authenticated — otherwise it will display a simple message: "Please login to continue".

def home_page(request):
    return render(request, "home_page.html")

Connect URLs to their respective views

In myshop >> urls.py, we will:

  • Import all the required views that you created.
  • Define the paths associated with all the views.

Add the following code below to urls.py:

from django.contrib import admin
from django.urls import path
from verification.views import register_page, home_page, login_page, generate_otp, check_otp

urlpatterns = [
    path('admin/', admin.site.urls),
    path('register/', register_page, name="register"),
    path('check/', check_otp, name="check_otp"),
    path('login/', login_page,  name="login"),
    path('otp/<int:pk>/<uuid>/', generate_otp),
    path('home/', home_page)
]

Create templates

Before creating a templates folder, we need to define it in our settings.py file, so that Django can locate it.

Go to myshop >> settings.py, and add templates inside the DIRS field:


TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Now create a templates folder in the main project folder:

File explorer showing directories named myshop, templates and auth in the myshop directory

Further, inside the templates folder, create an auth folder where you will add two new files: register.html and login.html.

Create register.html

This will be just a basic Django form with the fields we defined in our forms.py file. Copy the following code into auth/register.html.

<!DOCTYPE html>
  <html> 
    <head>
  </head> 
  <body style="background-color:cadetblue;"> 
    <h1 style="text-align:center;"> Register yourself for an awesome shopping experience !</h1>
    <br>
    <form method="POST" style="text-align:center;"> {% csrf_token %}
   <div class="container" style="background-color: grey; height: 400px; width:600px; text-align:center; border: 5px solid black; margin: 0 auto;">
   
   </br>
  </br>
  </br>
    {{ form.as_p }}
   
    <button type="submit" style="text-align:center;">Submit</button>
      <p><b>Already have an account? </b><a href="{% url 'login' %}">Sign in</a>.</p>
   </div>
  </body> 
  </html>

Note that we have used the {{ form.as_p }} method to render the form as a paragraph. You can also use {{ form.as_table }} or {{ form.as_ul }} depending upon the use case.

Create login.html

Just like auth/register.html, auth/login.html will also display a basic Django form with the fields defined in forms.py. The only difference is that here we will display a message if there is an error in the value entered by the user in the form.

Copy the following code into auth/login.html:

<!DOCTYPE html>
<html> 
  <head>
</head> 
<body style="background-color:khaki;">
    {% if messages %}

{% for message in messages %}
 {% if message.tags == "error" %}
 
    <div class="alert alert-danger" >
        <h1 style="border: 5px red;">  {{ message }} </h1>
</div>
{% else %} 
<div class="alert alert-{{ message.tags }}">
  <h1 style="border: 5px red;">  {{ message }} </h1>
</div>
{% endif %}
{% endfor %}
{% endif %}

   
  <h1 style="text-align:center;"> Welcome Back!!</h1>
  <h2 style="text-align: center;"> Login to continue</h2>
  <div class="container" style="background-color: grey; height: 400px; width:600px; text-align:center; border: 5px solid black; margin: 0 auto;">
   
</br>
</br>
<form method="POST" style="text-align:center;"> {% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-default" style="text-align:center; background-color:white;">Submit</button>
<p><b> Create an account? </b><a href="{% url 'register' %}">Register here</a>.</p>
</div>
</form>
</body>
</html>

Create otp.html

Next, create a file called otp.html inside the templates folder. Note that this file is outside of the auth folder.

This page will have 2 fields — email address and OTP. After the user submits the form, the user is redirected to the “/check” url.

Add the following code to otp.html:

<!DOCTYPE html>
<html>
<body style="background-color:khaki;" >
  <div class="container" style="background-color: grey; height: 400px; width:600px; text-align:center; border: 5px solid black; margin: 0 auto;">
<h2> We have sent an OTP to your registered email address. Please confirm the OTP to continue.</h2>
    <form method="POST" action=" {% url 'check_otp' %} " style="text-align:center;"> {% csrf_token %}
  {% if messages %}
  
  {% for message in messages %}
   {% if message.tags == "error" %}
   
      <div class="alert alert-danger">
      {{ message }}
  </div>
  {% else %} 
  <div class="alert alert-{{ message.tags }}">
      {{ message }}
  </div>
  {% endif %}
{% endfor %}
{% endif %}
    <h2> <strong> Enter email address</strong></h2>
    <input type="email" name="email" required>
    <h2><strong> Enter OTP</strong></h2>
  <input type="text" name="secret" inputmode="numeric" autocomplete="one-time-code" pattern="\d{6}" required>
</br>
</br>
  <button type="submit" class="btn btn-default" style="text-align:center; background-color:white;">Submit</button>
</form>
</div>
</body>
</html>

Create home_page.html

Next, create another file in the templates folder called home_page.html.

Here, we will check if the user is authenticated. If the user is authenticated, then we will show them the premium content. Otherwise we will simply display a message: "Please login to continue.

Add the following code to home_page.html:

<!DOCTYPE html>
<html> 
  <head>
</head> 
<body style="background-color: grey;">
{% if request.user.is_authenticated %}
          <div class="container">
         <div class="row" style="position: relative; left:300px;">
          <div class="col">
            <h1 style="text-align:center;margin: 0 auto; "> Welcome to the online shop! ! </h1>
            <img src='https://static.vecteezy.com/system/resources/previews/000/172/891/original/online-shopping-center-free-vector.jpg' style=" text-align: centre; display:block; margin-left: auto; margin-right: auto; border: 5px black; height: 600px; width: 800px;"/>
          </div>
        </div>


        </div>
        {%else %}
        <div class="container" style="position: relative; left:300px;">
       <h1>Please login, to continue</h1>
        </div>
        {% endif %}

      </body>
      </html>

Testing

Now, let’s test our authentication system!

To restart your application, navigate to the folder which contains the manage.py file, and run the following command:

python manage.py runserver

Access the registration page at http://127.0.0.1:8000/register/, and register a user.

Registration page prompting users to enter their username, email, and phone number

Note: For the purpose of this tutorial, the template is kept basic, but you can make changes in the UI as required.

Now if everything works well, you will be redirected to the login page:

Login page

After you click on the Submit button, you will be redirected to the page where you need to enter the OTP you received at your registered email address.

The user is prompted to enter their email address again along with the OTP to continue logging in

You will receive an email like this:

Email received via sendgrid showing verification code

After submitting the correct email address and OTP, you will be redirected to the home page.

Home page with graphic of a laptop modeled as a storefront labeled "online shopping center".

If you are not authenticated, a simple message will be displayed like this:

Please login, to continue.

Conclusion

Congratulations! You have successfully created a passwordless authentication system. The source code for the passwordless authentication system can be found here: Passwordless authentication system.

What’s next?

  • Add the logout view
  • Add features in the home page
  • Improve upon the design of the template

Ashi Garg is a Computer Science Engineer. Her interests include developing systems that are safe and easy to use and keeping herself abreast with new technological trends. She can be reached via email and LinkedIn.