Verify Phone Numbers in Symfony 4 PHP with Authy and Twilio SMS

May 29, 2019
Written by
Oluyemi Olususi
Contributor
Opinions expressed by Twilio contributors are their own

Copy of Generic Blog Header 3-2.png

Introduction

One of the most appropriate ways to ensure that your application’s database contains only valid phone numbers stored against each user, is by properly verifying the phone number during the registration process. This amongst other things will ensure sanity in your application, reduce the number of false or fraudulent registrations and easily convert this data for marketing purposes.

In this tutorial, I will show you how to verify phone numbers in a Symfony 4 project by leveraging Twilio’s Verfiy API. Together we will build an application that will capture users’ phone numbers and use Twilio to send a 6 digit code through SMS. After receiving this code, the user will be required to enter it for proper verification.

Once we are done with the step-by-step process of implementing this feature, you will have learned how to structure a proper registration flow that takes phone number verification into consideration.

Prerequisite

A reasonable knowledge of object-oriented programming with PHP will help you get the best out of this tutorial. To make the tutorial less difficult for Symfony newbies, I will endeavor to break down any complex implementation or logic.

You will need to ensure that you have Composer installed globally on your computer. Lastly, an existing Twilio account will help you get up to speed in no time. In case you don’t have an account already, don’t worry, we will set up a new one later in this post.

What we’ll build

As mentioned earlier the application that will be built in this post will be used to capture the phone number of users during the registration process. The details of the users will not be persisted into the database until his or her phone number is verified.

To successfully implement this flow, we will leverage one service specifically made available by Twilio named Authy API. This service can be used to verify phone numbers by sending codes via voice and SMS. But for the sake of this tutorial, we will send a 6-digit code via SMS.

Here is what the verification workflow will look like:

  • Create a new Authy application in order to obtain an API KEY
  • Send a verification token via SMS once a user registers
  • Check the verification token and then proceed to save user’s details into the database.

With this properly covered, you will have an application that can seamlessly capture and confirm user phone numbers as part of the onboarding process as shown below:

Getting started by installing and setting up a new Symfony project

To begin, we will use Composer to create a new Symfony application. Start by opening a terminal and navigate to the preferred folder for project development on your computer and run the following command:

$ composer create-project symfony/website-skeleton phone-verify-twilio

The preceding command will create the application in a new folder named phone-verify-twilio and install the relevant dependencies for the project. Once the installation is complete, change directory into the new project folder and start the built-in web server as shown below:

// Change directory
$ cd phone-verify-twilio

// Start the server
$ php bin/console server:run 

Open your browser and navigate to http://localhost:8000 to view the welcome page. The version displayed here is the current one for Symfony as at the time of writing:

Symfony welcome screen

Installing Other Dependencies

Next, as stated earlier we will be interacting with the Authy API by leveraging some of its awesome features for the purpose of verifying phone numbers from within our web application.

In order to give our application a proper structure and to effortlessly make an HTTP request to the Authy API, we will need to make use of the Authy PHP Helper Library. To proceed, hit CTRL + C on your keyboard in order to stop the development server and run the command below to install Authy PHP client for this project:

$ composer require authy/php

This will install the latest version of the tool within the project.

Twilio Account Setup

A Twilio account is required for you to have access to the APIs and other services offered by Twilio. Click here to get started with a free Twilio account.

Next, go ahead and verify your phone number and proceed to the dashboard once you are done. You will have a similar page like the one shown here:

Then, click on the All Products and Services icon and scroll down to select Authy:

From the Authy API page, click on the Get Started button and verify your phone number so you can use Authy.

Once you are done with your phone number verification, click on Applications from the sidebar. This will show you the list of all Authy applications that you have created so far. Considering that you don’t have any application created at the moment, click on the Create Application button:

Next, give your new application a friendly name. I have named mine verify-app and click on Create:

Authy create new application

Once this process is completed, you have successfully created your Authy application with the necessary credentials required for a connection. Note, you will have to return to this page later for the API KEY.

Authy settings page

Creating an Entity Class

During the process of registration, we intend to capture the details of any of our users and verify their phone number before proceeding to save their details in the database. Let’s begin by creating a model that represents a User. To do this, we’ll leverage the maker bundle that comes installed with Symfony to generate a new User entity. Run the following command for that purpose:

$ php bin/console make:entity User

The command above will create two new files src/Entity/User.php and src/Repository/UserRepository.php. Open the User.php file and configure it like this:

// ./src/Entity/User.php

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;



/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @UniqueEntity(fields="email", message="Email already taken")
 * @UniqueEntity(fields="username", message="Username already taken")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank()
     */
    private $username;

    /**
     * @ORM\Column(type="string", length=255, unique=true)
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    private $email;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(max="4096")
     */
    private $plainPassword;

    /**
     * @ORM\Column(type="string", length=64)
     */
    private $password;


    /**
     * @ORM\Column(type="string", length=255)
     */
    private $phoneNumber;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $countryCode;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $verificationCode;

    /**
     * @ORM\Column(type="boolean")
     */
    private $verified;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getUsername(): ?string
    {
        return $this->username;
    }

    public function setUsername(string $username): self
    {
        $this->username = $username;

        return $this;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;

        return $this;
    }

    public function getPhoneNumber(): ?string
    {
        return $this->phoneNumber;
    }

    public function setPhoneNumber(string $phoneNumber): self
    {
        $this->phoneNumber = $phoneNumber;

        return $this;
    }

    public function getCountryCode(): ?string
    {
        return $this->countryCode;
    }

    public function setCountryCode(string $countryCode): self
    {
        $this->countryCode = $countryCode;

        return $this;
    }

    public function getVerificationCode(): ?string
    {
        return $this->verificationCode;
    }

    public function setVerificationCode(?string $verificationCode): self
    {
        $this->verificationCode = $verificationCode;

        return $this;
    }

    public function getVerified(): ?bool
    {
        return $this->verified;
    }

    public function setVerified(bool $verified): self
    {
        $this->verified = $verified;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    /**
     * @param mixed $password
     */
    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }


    /**
     * Returns the password used to authenticate the user.
     *
     * @return string The password
     */
    public function getPassword()
    {
        return $this->password;
    }

    /**
     * @param mixed $password
     */
    public function setPassword($password)
    {
        $this->password = $password;
    }


    /**
     * String representation of object
     * @link http://php.net/manual/en/serializable.serialize.php
     * @return string the string representation of the object or null
     * @since 5.1.0
     */
    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
        ));

    }

    /**
     * Constructs the object
     * @link http://php.net/manual/en/serializable.unserialize.php
     * @param string $serialized <p>
     * The string representation of the object.
     * </p>
     * @return void
     * @since 5.1.0
     */
    public function unserialize($serialized)
    {
        list(
            $this->id,
            $this->username,
            $this->password,
            ) = unserialize($serialized);
    }

    /**
     * Returns the roles granted to the user.
     *
     *     public function getRoles()
     *     {
     *         return ['ROLE_USER'];
     *     }
     *
     * Alternatively, the roles might be stored on a ``roles`` property,
     * and populated in any number of different ways when the user object
     * is created.
     *
     * @return (Role|string)[] The user roles
     */
    public function getRoles()
    {
       return array('ROLE_USER');
    }


    /**
     * Returns the salt that was originally used to encode the password.
     *
     * This can return null if the password was not encoded using a salt.
     *
     * @return string|null The salt
     */
    public function getSalt()
    {
        return null;
    }

    /**
     * Removes sensitive data from the user.
     *
     * This is important if, at any given point, sensitive information like
     * the plain-text password is stored on this object.
     */
    public function eraseCredentials()
    {
    }
}

Here, we have created a couple of fields for the database.

Create a DefaultController

Next, we need to generate a new controller to handle page rendering for the default page of our application. Open the terminal and run the following command:

$ php bin/console make:controller DefaultController

Once this is done, you will have two new files created for you src/Controller/DefaultController.php and a corresponding view page for it in templates/default/index.html.twig. Open the DefaultController.php file and replace its content with the following:

// ./src/Controller/DefaultController

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    /**
     * @Route("/", name="default")
     */
    public function index()
    {
        return $this->render('default/index.html.twig');
    }
}

The controller above will render the content within the default/index.html.twig file. Open template/default/index.html.twig and paste the following in it:

{# ./template/default/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %} PHP Phone Verification with Twilio {% endblock %}

{% block body %}
    <div class="example-wrapper">
        <div class="jumbotron text-center">

            <h2> Verify phone numbers with Symfony4 and Twilio</h2>
            <div>
                {% if app.user %}

                    <a href="{{ path('home') }}" class="btn btn-default">Home</a>

                {% else %}
                    <div class="button-links">
                        <a href="{{ path('login') }}" class="btn btn-success">Login</a>
                        <a href="{{ path('user_registration') }}" class="btn btn-primary">Register</a>
                    </div>

                {% endif %}
            </div>
        </div>
    </div>

{% endblock %}

Generate the Security Controller

In this section, generate a new controller that will handle the login process for a user:

$ php bin/console make:controller SecurityController

Open the newly created controller and replace its content with the following code:

// src/Controller/SecurityController.php

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;


class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="login")
     */
    public function login(Request $request, AuthenticationUtils $authUtils)
    {
        $error = $authUtils->getLastAuthenticationError();

        $lastUsername = $authUtils->getLastUsername();

        return $this->render('security/index.html.twig', [
            'last_name' => $lastUsername,
            'error' => $error,
        ]);
    }

}

Create the login page

Open the templates/security/index.html.twig file and configure it as shown here:

{# ./templates/security/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}Login {% endblock %}

{% block body %}

    <div class="col-sm-12 wrapper">

        <div class="auth-form" style="max-width: 600px;margin: 0 auto;">

            <h2 class="text-center">Login</h2>
            <hr>

            {% if error %}
                <div class="alert alert-danger alert-dismissible">
                    <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
                    <strong>Error!</strong> {{ error.messageKey|trans(error.messageData, 'security') }}
                </div>

            {% endif %}

            {% for message in app.flashes('success') %}
                <div class="flash-notice alert alert-success alert-dismissible">
                    {{ message }}
                </div>
            {% endfor %}

            <form action="{{ path('login') }}" method="post">

                <div class="row">
                    <div class="form-group col-md-12">
                        <label for="username">Username:</label>
                        <input type="text" id="username" class="form-control" name="_username" value="" autocomplete="off"/>
                    </div>
                </div>

                <div class="row">
                    <div class="form-group col-md-12">
                        <label for="password">Password:</label>
                        <input type="password" id="password" class="form-control" name="_password" />
                    </div>
                </div>

                <div class="row">
                    <div class="form-group col-md-12">
                        <button type="submit" class="btn btn-success form-control">Login</button>
                    </div>
                </div>

                    <div class="text-center form-group">
                        Don't have an account? <span><a href="{{ path('user_registration') }}">Register</a></span>
                    </div>
            </form>
        </div>

    </div>

{% endblock %}

Here, we have created the necessary input fields required to authenticate a user.

Generate the RegistrationController

Generate a controller to handle user registration with:

$ php bin/console make:controller RegistrationController

Open src/Controller/RegistrationController.php and paste the following:

// ./src/Controller/RegistrationController.php

<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class RegistrationController extends AbstractController
{
      /**
     * @Route("/register/page", name="user_registration", methods={"GET"})
     */
    public function registerAction(Request $request)
    {
        return $this->render('registration/index.html.twig');
    }
}

Create a Registration Page

Paste the content below for the registration page. You can find the file in the templates/registration folder:

{# templates/registration/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %} Register {% endblock %}

{% block body %}

    <div class="col-sm-12 wrapper">
        <div class="auth-form" style="max-width: 600px;margin: 0 auto;">
            <h2 class="text-center">Register</h2>
            <hr>
            <form action="{{ url('register') }}" method="post">
                <div class="col-md-12">
                    <div class="row">
                        <div class="form-group col-md-12">
                            <label for="username"> Username </label>
                            <input type="text" name="username" class="form-control" placeholder="Enter username" id="username">
                        </div>
                        <div class="form-group col-md-12">
                            <label for="email"> Email </label>
                            <input type="text" name="email" class="form-control" placeholder="Enter email" id="email">
                        </div>
                    </div>


                    <div class="row">
                        <div class="form-group col-md-2">
                            <label for="country_code">  Code </label>
                            <input type="text" name="country_code" placeholder="+234" class="form-control" id="country_code">
                        </div>

                        <div class="form-group col-md-10">
                            <label for="phone_number"> Phone Number </label>
                            <input type="text" name="phone_number" class="form-control" id="phone_number">
                        </div>
                    </div>

                    <div class="row">
                        <div class="form-group col-md-12">
                            <label for="password">  Password </label>
                            <input type="password" name="password" class="form-control" id="password">
                        </div>
                    </div>
                </div>
                <div class="form-group col-md-12">
                    <button class="form-control btn btn-success"> Register </button>
                </div>
            </form>

            <div class="text-center">
                Have an account <span><a href="{{ path('login') }}">Login</a></span>
            </div>
        </div>
    </div>

{% endblock %}

Here we have created input fields for username, email, country code, phone number, and password.

Add a Stylesheet

Create a new folder named css within the public folder, and create another file named style.css within the newly created folder. Paste the following code into the new file:

// ./public/css/style.css

body {
    background: #e9ecef;
}

nav {
    background: #ffffff;
    height: 70px;
}

.button-links {
    margin-top: 40px;
}

.navbar ul li {
    display: inline-block;
    padding: 10px;
}

.navbar-nav {
    display: inline-block;
}

.welcome_page {
    margin-top: 50px;
}

Update the Base Template

Update the base template with:

// ./templates/base.html.twig

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{% block title %} PHP Phone Verification with Twilio {% endblock %}</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
    {% block stylesheets %}
        <link rel="stylesheet" href="{{ asset('css/style.css') }}">
    {% endblock %}
</head>
<body>

<div id="app">
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <div class="navbar-header">
                <a class="navbar-brand" href="{{ path('default') }}"> Phone Verify </a>
            </div>
            <ul class="nav navbar-nav navbar-right">
                {% if app.user %}
                    <li><a class="nav-link" href="{{ path('home') }}">Dashboard</a></li>
                    <li><a class="nav-link" href="{{ path('logout') }}">Logout</a></li>

                {% else %}

                    <li><a class="nav-link" href="{{ path('login') }}">Login</a></li>
                    <li><a class="nav-link" href="{{ path('user_registration') }}">Register</a></li>
                {% endif %}
            </ul>
        </div>
    </nav>


    {% block body %}{% endblock %}
</div>
</body>
</html>

If you run the application at the moment with php bin/console server:run, you will see the page below once you navigate to http://localhost:8000:

At this point, our application is ready for users to start registering and verifying their phone number before they can access the secured areas of our application.

Update the DotEnv file

In the root directory of the project, we will update the .env file with the database credentials and also include our Twilio credentials as well. Open the file and configure it as shown below:

#DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name

#Twilio Credentials
TWILIO_AUTHY_API_KEY=YOUR_PRODUCTION_API_KEY

Note: Do not forget to replace the following with the appropriate credentials:

  • db_user: Replace with your database username
  • db_password: Replace with your database password
  • db_name: Replace with your database name
  • TWILIO_AUTHY_API_KEY: Replace with the production API key as obtained from your Authy application dashboard.

Next, run the following command php bin/console doctrine:database:create, to create a database with the value of your database name. At the moment, the database still has no tables in there. Run the following command that will instruct Doctrine to create the tables based on the User entity that we have created earlier.

$ php bin/console doctrine:schema:update --force

Capture the User’s Phone Number and Send Verification Code

Back to the RegistrationController. Update the content with:

// ./src/Controller/RegistrationController.php

<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\UserType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use GuzzleHttp\Client;
class RegistrationController extends AbstractController
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;
    /**
     * @var \Doctrine\Common\Persistence\ObjectRepository
     */
    private $userRepository;
 
    public function __construct(EntityManagerInterface $entityManager )
    {
        $this->entityManager = $entityManager;
        $this->userRepository = $entityManager->getRepository('App:User');
    }

    /**
     * @Route("/register/page", name="user_registration", methods={"GET"})
     */
    public function registerAction(Request $request)
    {
        return $this->render('registration/index.html.twig');
    }

    /**
     * @param Request $request
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     * @Route("/register", name="register", methods={"POST"})
     */
    public function registerUsers(Request $request)
    {
        if ( $request->request->get('country_code') ) {

           $authy_api = new \Authy\AuthyApi( getenv('TWILIO_AUTHY_API_KEY') );
           $user      = $authy_api->registerUser( $request->request->get('email'), $request->request->get('phone_number'), $request->request->get('country_code') );

           if ( $user->ok() ) {

               $sms = $authy_api->requestSms( $user->id(), [ "force" => "true" ] );

               if ( $sms->ok() ) {

                   $this->addFlash(
                       'success',
                       $sms->message()
                   );
               }    
              
               $user_params = [
                   'username' => $request->request->get('username'),
                   'email' => $request->request->get('email'),
                   'country_code' => $request->request->get('country_code'),
                   'phone_number' => $request->request->get('phone_number'),
                   'password' => $request->request->get('password'),
                   'authy_id' => $user->id(),
               ];

               $this->get('session')->set('user', $user_params);
           }
       }

       return $this->redirectToRoute('verify_page');

    }

    function updateDatabase($object)
    {
        $this->entityManager->persist($object);
        $this->entityManager->flush();
    }
}

What we have added to the RegistrationController() is a method named registerUsers(). This method uses the HttpFoundation Component to grab the users inputs for verification purposes.

Lastly, we set the users details into session. The intention is to get these details from the session once the user’s phone number is verified and save it to the database. This will help us ensure that we populate the database only with the details of verified users.

Create HomeController and Validate Verification Code

Now, we will create a controller that will handle the verification of the user’s phone number and eventually save the details into the database. To achieve this, run the following command:

$ php bin/console make:controller HomeController

Navigate to the new file created by the command above in /src/Controller/HomeController.php and replace its content with:

// src/Controller/HomeController.php

<?php

namespace App\Controller;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class HomeController extends AbstractController
{

   

    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager )
    {
        $this->entityManager = $entityManager;
    }

    /**
     * @Route("/home", name="home")
     */
    public function index()
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => 'HomeController',
        ]);
    }

    /**
     * @Route("/verify/page", name="verify_page")
     */
    public function verifyCodePage()
    {
        return $this->render('home/verify.html.twig');
    }


    /**
     * @Route("/verify/code", name="verify_code")
     * @param Request $request
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function verifyCode(Request $request, UserPasswordEncoderInterface $encoder)
    {
        try {
            // Get data from session
                       $data = $this->get('session')->get('user');

           $authy_api    = new \Authy\AuthyApi( getenv('TWILIO_AUTHY_API_KEY') );
           $verification = $authy_api->verifyToken( $data['authy_id'], $request->query->get('verify_code') );

           $this->saveUser( $encoder,$data );

           return $this->redirectToRoute('home');


        } catch (\Exception $exception) {
            $this->addFlash(
                'error',
                'Verification code is incorrect'
            );
            return $this->redirectToRoute('verify_page');
        }
    }

    public function saveUser(UserPasswordEncoderInterface $encoder, $data)
    {
        $user = new User();
        $user->setUsername($data['username'])
            ->setEmail($data['email'])
            ->setCountryCode($data['country_code'])
            ->setPhoneNumber($data['phone_number'])
            ->setVerified(true)
            ->setPassword($encoder->encodePassword($user, $data['password']))
            ;

        $this->addFlash(
          'success',
          'You phone number has been verified. Log in here'
        );

        // save user
        $this->entityManager->persist($user);
        $this->entityManager->flush();
    }
}

We created a method to verify the code entered by users and saved it into the database.

Homepage

Also, update the index.html.twig file generated for the HomeController() with the following content:

{# ./templates/home/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}PHP Phone Verification with Twilio{% endblock %}

{% block body %}

    <div class="container">
        <div class="row">
            <div class="welcome_page">
                <p>Welcome!</p>
                <p>Your phone number has been verified</p>
            </div>
        </div>
    </div>

{% endblock %}

This page will only be displayed to users once their phone number has been verified:

Create the Verify Page

Here, we will create a page where users can enter the 6-digit code they received and get the appropriate response depending on the verification status of the code. To begin, navigate to the template/home folder and create a new file named verify.html.twig and paste in it:

{# ./template/home/verify.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}PHP Phone Verification with Twilio{% endblock %}

{% block body %}

    <div class="container">
        <div class="row">
            <div>
                <h2>Verify Phone Number</h2>
            </div>
        </div>

        <div class="row">
            <div class="col-md-6">
                {% for message in app.flashes('success') %}
                    <div class="flash-notice alert alert-success">
                        {{ message }}
                    </div>
                {% endfor %}
                {% for message in app.flashes('error') %}
                    <div class="flash-notice alert alert-danger">
                        {{ message }}
                    </div>
                {% endfor %}

                <div>
                    <p> Enter the 6 digit code here</p>
                </div>

                <form action="{{ url('verify_code') }}" method="get">
                    <div class="form-group">
                        <input type="text" class="form-control" name="verify_code" placeholder="Enter code here">
                    </div>

                    <div class="form-group">
                        <button type="submit" class="btn btn-success"> Verify Now</button>
                    </div>
                </form>
            </div>
        </div>
    </div>

{% endblock %}

Verify phone number example

If the verification code entered by a user is wrong, then a message will be displayed as shown below:

Verify phone number example - error

Otherwise, the user will be redirected to the login page and receive a message indicating that the phone number has been verified, and he or she can now login to view the protected page:

Successful login after verification

Update Security.yaml file

Next, update the contents of ./config/packages/security.yaml with:

security:
    encoders:
        App\Entity\User:
            algorithm: bcrypt
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        our_db_provider:
            entity:
                 class: App\Entity\User
                 property: username
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
#            pattern: ^/
            anonymous: ~
#            http_basic: ~
            provider: our_db_provider
#            anonymous: true

            # activate different ways to authenticate

            # http_basic: true
            # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate

            form_login:
                  login_path: login
                  check_path: login
                  default_target_path: /home
#        secured_area:
#            anonymous: ~
            logout:
                path: /logout
                target: /

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
         - { path: ^/home, roles: ROLE_USER }

 Finally, add the logout route to ./config/routes.yaml file:

logout:
    path: /logout

Testing the application

We are now ready to test the application. Don’t forget to restart the server if it isn’t running with php bin/console server:run. Next, navigate to http://localhost:8000 and try the application by registering a user:

Symfony registration screen

Conclusion

In this tutorial, we have learned how to set up a new Symfony project and restructure our application to capture a user’s phone number, ensuring that the phone number is verified before the details of such user can be saved into our database. Thanks to Twilio’s Authy API, we can now have users with verified phone numbers in our application.

Please note that the registration flow implemented in this tutorial can also be implemented for an existing project or a new one as shown here.

I hope you found this tutorial helpful. Feel free to explore the source code here on GitHub and don’t hesitate to visit the official documentation of Twilio to view more details about Authy API and other resources.

 

Olususi Oluyemi is a tech enthusiast, programming freak and a web development junkie who loves to embrace new technology.
Twitter: https://twitter.com/yemiwebby
GitHub: https://github.com/yemiwebby
Website: https://yemiwebby.com.ng/