How to Verify Phone Numbers in a Django Application with Twilio Verify

September 27, 2021
Written by
Reviewed by

How to Verify Phone Numbers in a Django Application with Twilio Verify

When building a user registration and authentication system for your web application, you run the risk of not properly detecting fake or duplicate accounts. A very effective way to reduce this risk is to require users to verify their accounts right after they register.

In this tutorial I’m going to show you how to extend the Django authentication system to include an SMS verification step in the user registration flow, using the Twilio Verify service. Among other things, you will learn how to:

  • Customize the user database model
  • Ask for additional information in the user registration page
  • Send and check SMS verification codes with the Twilio Verify service
  • Prevent access to parts of the application to users that haven’t verified their accounts

Project demonstration

Ready to begin? Let’s go!

Prerequisites

To follow this tutorial you need the following items:

Project setup

This tutorial will show you how to build this project step-by-step. If you prefer to download or clone the complete project instead, see the django-verify repository on GitHub.

In this section you are going to set up a brand new Django project. To keep things nicely organized, open a terminal or command prompt, find a suitable place and create a new directory where the project you are about to create will live:

mkdir django-verify
cd django-verify

Creating a virtual environment

Following Python best practices, you are going to create a virtual environment to install the Python dependencies needed for this project.

If you are using a Unix or Mac OS system, open a terminal and enter the following commands to create and activate your virtual environment:

python3 -m venv venv
source venv/bin/activate

If you are following the tutorial on Windows, enter the following commands in a command prompt window:

python -m venv venv
venv\Scripts\activate

Now you are ready to install the Python dependencies used by this project:

pip install django twilio python-dotenv

The two Python packages that are needed by this project are:

Creating a Django project

In this step you are going to create a brand new Django web application. Enter the following commands in the same terminal you used to create and activate the virtual environment:

django-admin startproject config .
django-admin startapp core

The first command above creates a Django project and puts the project configuration files in the config subdirectory. The next command defines a Django application called core. After you run this second command, you will also see a subdirectory with that name added to the project. This is where you will build the logic that registers, logs in, and verifies users.

In the previous section, the python-dotenv package was installed. This package is very useful to import configuration information into environment variables. To integrate it with the Django application, add it to the manage.py file, as shown below:


#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
from dotenv import load_dotenv  # ← new


def main():
    """Run administrative tasks."""
    load_dotenv()  # ← new
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
    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()

With this addition, the Django application will automatically import configuration variables from a file named .env located in the project directory. This file does not exist yet, but you will be adding it in a moment.

Twilio Verify service configuration

You are now going to configure the Verify service in your Twilio account. You will need this service near the end of the tutorial, but it is best to get this done now and have it out of the way.

Log in to your Twilio Console and type Verify in the search box. Once in the Verify section of the console, click on Services. This page shows the list of verification services currently in use. In the context of Twilio Verify, a “service” is an entity that represents an application that requires verification. Click the “Create Service Now” button or the blue “+” button to create a new service, and then enter a friendly name for it. For example, django-verify.

Create a Twilio Verify service

Note that the name that you choose for your service will appear in the SMS messages that are sent to users when they are verified.

The Verify service that you create will be assigned a “Service SID” and shown in the General Settings page for the service:

Verify service settings

Copy the SID assigned to the Verify service to the clipboard, and then paste it into a new file called .env (note the leading dot) as follows:

TWILIO_VERIFY_SERVICE_SID=XXXXX

Replace XXXXX with your assigned SID.

In addition to the Service SID, to work with Verify you also need your Twilio account SID and Auth Token, which you can get from the Twilio Console page:

Twilio account SID and auth token

Open the .env file again and add two more variables as shown below:

TWILIO_VERIFY_SERVICE_SID=XXXXX
TWILIO_ACCOUNT_SID=XXXXX
TWILIO_AUTH_TOKEN=XXXXX

I suggest that you go over all the configuration settings for your Verify service. On this page you can change the length of the numeric codes, which delivery channels you want to use, and more.

Adding a homepage

To make sure that everything is in order, you are going to add a homepage to this project. The view is going to be called index, and will simply render a template. Enter the following code in core/views.py:

from django.shortcuts import render


def index(request):
    return render(request, 'index.html')

The templates of this application are going to be stored in a templates subdirectory. Create this directory now:

mkdir templates

Django needs to be configured to look for templates in this directory. Open file config/settings.py, find the TEMPLATES dictionary, and edit the DIRS key as follows:


TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  # ← edit this line
        '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',
            ],
        },
    },
]

Next, you’ll create a base template that will define the page layout of the application. Create a file named base.html in the templates directory and enter the following HTML code in it:

<!doctype html>
<html>
  <head>
    <title>Django and Twilio Verify Demo</title>
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>

Now create the homepage HTML template in a file called index.html, also located in the templates folder:

{% extends 'base.html' %}

{% block content %}
  <h1>Django and Twilio Verify Demo</h1>
  <p>Hello!</p>
{% endblock %}

The core application needs to define its list of public URLs (only one so far). Create a file named core/urls.py and enter the following code in it:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

The URL definition above needs to be added to the project-wide URL configuration. Open file config/urls.py and enter the following definitions:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('core.urls')),
]

Running the application

A very basic version of the Django application is now ready to be tested. Go back to your terminal and start the web server with the following command:

python manage.py runserver

The output of this command will include some warnings in red regarding database migrations that haven’t been applied. Since the application does not need a database yet, it is safe to ignore this error for now. The database will be created in the next section and at that point, this warning will go away.

Type http://localhost:8000 into the address bar of your web browser to confirm that your Django project is up and running. This is what you should see:

Basic Django application

User model

The default User model provided by Django has the expected username and password fields already defined, but in this project users will need to provide their phone number for verification purposes. For that reason, the user model needs to be customized.

Open the file core/models.py and enter the following code in it:

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    phone = models.TextField(max_length=20, blank=False)
    is_verified = models.BooleanField(default=False)

This is an extended User model that inherits from the default base user implemented by Django. The two fields in this model are added to those that exist in the base class. The phone field is going to store the phone number provided by the user, and the is_verified field is going to be useful for the application to check the verification status.

Now we need to tell Django that we want to use this model instead of the default one. Open config/settings.py, find the INSTALLED_APPS dictionary, and add the core application as a last element:


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'core',  # ← new 
]

Then scroll down to the end of the file and add the following variable:

AUTH_USER_MODEL = 'core.User'

Save the settings file. The last step is to generate a database migration to add the changes in the new User model to the application’s database:

python manage.py makemigrations
python manage.py migrate

The makemigrations command looks at the model definitions in the application and generates a database migration script that includes the new User model we created above. The migrate command then initializes the Django database by applying all the database migrations.

If you are still running the Django web server from the previous section, stop it by pressing Ctrl-C and then restart it. From now on and until the end of this tutorial, you can leave the Django web server running while you continue working. Every time you make edits to the source code, the server will automatically update itself and restart to incorporate the changes.

Registration page

The user model is now configured to accept a phone number in addition to the standard fields required by the Django authentication system. To request the phone number from the user during registration, the registration page needs to be customized.

Let’s begin by writing the user registration view. Open core/views.py in your text editor or IDE and add the register view:


from django.shortcuts import render, redirect
from .forms import UserCreationForm


def index(request):
    return render(request, 'index.html')


def register(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('index')
    else:
        form = UserCreationForm()
    return render(request, 'register.html', {'form': form})

The registration view works with a form object of class UserCreationForm. This view will be called as a GET request when the registration page needs to be displayed, and as a POST request when the user is submitting their information through the form.

In the GET request case an empty form object is created and rendered to the page, through the register.html template. For the POST request, the form is validated, the user’s information is saved to the database, and finally the user is redirected to the index page.

This view uses two things that do not exist yet: the form class and the HTML template. You’ll define the form class in a new file you’ll need to create called core/forms.py:

from django import forms
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm
from .models import User


class UserCreationForm(BaseUserCreationForm):
    phone = forms.CharField(max_length=20, required=True, help_text='Phone number')

    class Meta:
        model = User
        fields = ('username', 'phone', 'password1', 'password2')

There is no need to create a full form for this class, because Django’s auth module provides a user registration form that just needs the phone number added. So for this form, the UserCreationForm from Django is used as a base class, and only the phone field is defined as an extension to the default fields. The form class needs to include a reference to the user model class, and the complete list of fields that will be presented to the user.

The last element that is necessary to complete the user registration flow is to create the HTML template that will render the page in the browser. Create a templates/register.html file and copy the following contents to it:

{% extends 'base.html' %}

{% block content %}
  <h1>Register</h1>
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Register</button>
  </form>
{% endblock %}

To make the registration page part of the application, add a /register/ URL to the core application. This is the updated core/urls.py file:


from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('register/', views.register),  # ← new entry
]

Make sure the Django web server is running and then open http://localhost:8000/register/ in your web browser to see the registration page in action:

Customized registration form

This is great, right? The styling is simple because no CSS classes have been added, but Django provides a very robust registration page, to which this project only adds the phone field.

Want to know the best thing about this form? It is already hooked up with the database to register new users, thanks to all the logic inherited from the Django base form class. If you fill out the form and press the “Register” button, the new user will be added to the database. But keep in mind that after the user form submission is accepted and saved, the user is redirected to the index page, which currently doesn’t have any concept of a logged in user. In the next section you’ll add the login and logout flows.

Login and Logout pages

To be able to work with logged in users the application needs to have content that is only available after the user logs in. With the next change, you’ll protect the index page so that it redirects unauthenticated users to a login page. Below you can see an updated core/views.py with the changes highlighted:


from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from .forms import UserCreationForm


@login_required
def index(request):
    return render(request, 'index.html')


def register(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('index')
    else:
        form = UserCreationForm()
    return render(request, 'register.html', {'form': form})

As you can see, protecting a view so that only logged in users can access it just requires adding the login_required decorator, which comes built-in with Django. This decorator will let users that are logged in access the view, but will redirect users who are not logged in to a login page. The URL of the login page is configured in config/settings.py. Add the following line at the bottom of the file:

 

LOGIN_URL = '/login/'

The index.html template can now be expanded to show the username of the logged in user with a button to log out of the application. Here is the updated template:

{% extends 'base.html' %}

{% block content %}
  <h1>Django and Twilio Verify Demo</h1>
  <p>Hello {{ request.user.username }}, you are a verified user!</p>
  <form method="POST" action="/logout/">
    {% csrf_token %}
    <input type="submit" value="Log out">
  </form>
{% endblock %}

The login and logout views are provided by Django. All that is required is to define URLs for these two views. Here is the updated core/urls.py:


from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('register/', views.register),
    path('login/', auth_views.LoginView.as_view(template_name='login.html')),
    path('logout/', auth_views.LogoutView.as_view(template_name='logout.html')),
]

The two new views need templates, given in the template_name arguments. Create a new file called templates/login.html. This template receives a form variable set to the login form and renders it to the page:

{% extends 'base.html' %}

{% block content %}
  <h1>Log in</h1>
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Log in</button>
  </form>
  <p>Don't have an account? <a href="/register/">Register</a> now!</p>
{% endblock %}

Note that as a nice touch, the login page includes a link to the registration page built earlier.

The templates/logout.html template just tells the user that they have been logged out, and offers a link to the index page. Create this file, then paste the following code into it:

{% extends 'base.html' %}

{% block content %}
  <h1>You are logged out</h1>
  <p><a href="/">Home</a></p>
{% endblock %}

With these changes, a basic user registration and login system is now in place. Ready to try it out? Make sure the Django web server is running, and then open a browser tab on the http://localhost:8000 URL. Since this URL is now protected by the login_required decorator, Django will send you to the login page, where you can enter your username and password to gain access. If you haven’t registered an account yet, click on the “Register” link to do it now.

Login and logout demonstration

Phone verification

The project is now able to register, login, and logout users. The final part of this tutorial is to add the SMS verification as an additional step right after registering. Registered users will be able to log in and out of the application, but they won’t be able to access the index page until they verify their accounts.

Integration with Twilio Verify

The most convenient way to integrate the Django application with Twilio Verify is to create a verify.py module in the core directory that implements the two functions of the service needed by the application. The first function sends a verification code to the user, while the second function checks a verification code once the user provides it back to the application.

Here is the core/verify.py module:

import os
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException

client = Client(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])
verify = client.verify.services(os.environ['TWILIO_VERIFY_SERVICE_SID'])


def send(phone):
    verify.verifications.create(to=phone, channel='sms')


def check(phone, code):
    try:
        result = verify.verification_checks.create(to=phone, code=code)
    except TwilioRestException:
        print('no')
        return False
    return result.status == 'approved'

The two functions that the application will use to interface with the Verify service are going to be send() and check().

Sending a verification code

To send a verification code to the user, the application needs to call the send() function defined above and pass the phone number of the user as the only argument. This can be done in the registration view, immediately after the user is saved to the database. Here is the updated code for core/views.py:


from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from .forms import UserCreationForm
from . import verify


@login_required
def index(request):
    return render(request, 'index.html')


def register(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            verify.send(form.cleaned_data.get('phone'))
            return redirect('index')
    else:
        form = UserCreationForm()
    return render(request, 'register.html', {'form': form})

With this small addition, each time a user is registered, a code will be sent by SMS to the phone number entered during registration. Here is the code that I received when I registered a new user with my phone number:

Example verification SMS

Twilio likes to see phone numbers given in E.164 format, which includes a plus sign prefix and the country code. As an example, a number from the United States would be given as +1AAABBBCCCC’, where AAA is the area code and BBB-CCCC` is the local number. In a real world application, the phone field in the form will ensure that the number is formatted correctly. In this example application you’ll have to enter the number in the correct format.

If you are using a Twilio trial account, remember that Twilio only sends SMS to phone numbers that are verified on the account. This restriction does not exist in paid accounts.

Accepting a verification code from the user

The user is now receiving a code by SMS, so the next step is to create a route that the user can use to input this number to get it verified. Add the following function to the core/views.py file:


from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from .forms import UserCreationForm, VerifyForm
from . import verify

# … no changes to index() and register() view functions

@login_required
def verify_code(request):
    if request.method == 'POST':
        form = VerifyForm(request.POST)
        if form.is_valid():
            code = form.cleaned_data.get('code')
            if verify.check(request.user.phone, code):
                request.user.is_verified = True
                request.user.save()
                return redirect('index')
    else:
        form = VerifyForm()
    return render(request, 'verify.html', {'form': form})

The verify_code view will display a VerifyForm when it executes as a GET request, and will accept a code from the user when executing as a POST request. In the latter case, the code that is received from the user is sent to the verify.check() function, and if the verification succeeds, the user model’s is_verified attribute is set to True and saved.

Add the form class used by this view at the bottom of core/forms.py module:

class VerifyForm(forms.Form):
    code = forms.CharField(max_length=8, required=True, help_text='Enter code')

This form has just one field, where the user enters the numeric code received by SMS.

The form will be rendered as part of an HTML page. Create a file called templates/verify.html which defines the layout of this page:

{% extends 'base.html' %}

{% block content %}
  <h1>Verify your account</h1>
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Verify</button>
  </form>
{% endblock %}

Finally, a URL needs to be defined for this new view. Below is the updated core/urls.py file:


from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('register/', views.register),
    path('login/', auth_views.LoginView.as_view(template_name='login.html')),
    path('logout/', auth_views.LogoutView.as_view(template_name='logout.html')),
    path('verify/', views.verify_code),  # ← new
]

With the application in its current state you can already check how the verification codes work. Navigate to http://localhost:8000/register/ on your browser and create a new user account, using your phone number in the E.164 format. Once the registration is complete you will receive an SMS with the numeric code that verifies your new account.

Now navigate to http://localhost:8000/verify/, log into the application with the new account, and finally enter the verification code. If you enter an invalid code, the form will display again. Once you enter the correct code you will be redirected to the index page.

Preventing access to unverified accounts

The verification solution is working great, but it is not integrated into the registration flow yet. In this section you are going to add the final bit of functionality to create a seamless experience for the new user.

You’ve seen Django’s login_required decorator, which prevents access to a page when the user is not logged in. Now you will create a second decorator called verification_required that will work in a similar way, but will redirect the user to the verification page when the account hasn’t been verified yet. Enter the following code in a new file called core/decorators.py:

from django.contrib.auth.decorators import user_passes_test


def verification_required(f):
    return user_passes_test(lambda u: u.is_verified, login_url='/verify')(f)

This decorator uses the same logic that login_required uses, but it checks the is_verified attribute of the user instead of is_authenticated, and redirects to the /verify/ page instead of /login/.

Add the new decorator to the index()` function in core/views.py:


from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from .forms import UserCreationForm, VerifyForm
from . import verify
from .decorators import verification_required  # ← new


@login_required
@verification_required  # ← new
def index(request):
    return render(request, 'index.html')

# … no changes beyond this point

With this small change, access to the index page is now prevented if the user’s is_verified attribute is False. When the user logs in and is redirected to this page, the decorator will intercept the request and instead redirect to the verification page, giving the user a chance to verify the account before they are allowed to access the page.

Defining the login_required and verification_required decorators separately gives the application control over what pages only require a login and what pages require the account to be verified.

Next steps

I hope you found this tutorial useful! If you are looking for ideas on how to extend the project, here are some:

  • Add an option to resend the verification code. This can be a very useful addition, because Twilio Verify codes expire after 10 minutes.
  • Change the project to require a phone verification step after each log in instead of only after the user registers a new account. This is essentially a form of two-factor authentication (2FA).
  • Give the user the option to receive the code via phone call.

To learn about all the Twilio Verify service has to offer, check out the documentation.

I’d love to see what you build with Django and Twilio Verify!

Miguel Grinberg is a Principal Software Engineer for Technical Content at Twilio. Reach out to him at mgrinberg [at] twilio [dot] com if you have a cool project you’d like to share on this blog!