Verify User Registrations with Symfony
Published on April 19, 2021

Users registering with false information can be a pest, which is especially the case when registering with phone numbers that you expect to be contactable. Vonage's Verify API provides a solution to this by enabling you to confirm that the phone number is correct and owned by the user. The API takes a phone number, sends a pin code to that phone number and expects it to be relayed back through the correct source.

In this tutorial, you'll extend an existing basic user authentication system, built-in Symfony 5, by implementing multi-factor authentication with the Vonage Verify API (Formerly Nexmo Verify API).

You can find the finished code under the end-tutorial branch in this GitHub repository.

Prerequisites

DT API Account

To complete this tutorial, you will need a DT API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the DT API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Getting Started

Cloning the Repository

Clone the existing repository by copying this command into your Terminal, and then change directory into the project:

git clone git@github.com:nexmo-community/verify-user-registrations-with-symfony.git cd verify-user-registrations-with-symfony

Database Credentials

Within the symfony/ directory create a .env.local file, to store your local environment variables you don't wish to be committed to your repository. For example, your code needs to know the method and credentials to connect to your database. Copy the following line into your .env.local file:

DATABASE_URL=postgresql://user:password@postgres:5432/test?serverVersion=11&charset=utf8

The above example contains several pieces of information needed to connect to the database.

  • postgresql is the protocol used to connect.

  • user and password is the set of credentials used to authenticate to the database.

  • postgres is the address for the domain.

  • 5432 is the port to connect to the database.

  • test is the database name.

Installing Third-Party Libraries

There several third-party libraries already defined in this project need to be installed, both via Composer and yarn packages.

Run the following three commands:

# Install libraries such as Symfony framework bundle and Doctrine Orm bundles (for manipulating the database). composer install # Install Symfony Webpack encore for integrating bootstrap and front end technologies into the Symfony application. yarn install # Compile front end files ready for development use. yarn run dev

Running Docker

For this tutorial and to ensure that the server requirements are the same for everyone, a Docker config has been set up to use containers with predefined configurations.

Within the docker/ directory run:

docker-compose up -d

Once the docker-compose command has finished, you should be able to see the following confirmation that the three containers are running:

Image showing Terminal output of Docker containers successfully runningImage showing Terminal output of Docker containers successfully running

Running Database Migrations

In your terminal, connect to the PHP Docker container by running the following command:

docker-compose exec php bash

To create the database tables and execute all files found in symfony/src/migrations/, run the following command:

php bin/console doctrine:migrations:migrate

This command creates a user database table with the relevant columns.

Test Run the Registration

Go to: http://localhost:8081/register in your browser, and you will see a registration page similar to what you see in the image below:

Initial template render of registration page with form.Initial template render of registration page with form.

Enter a test telephone number and password. On submission of the form, you should now see the profile page!

Note: Using your phone number here will create you a new user, so be ready to delete that registration from the user database table.

If you're at this point, you're all set up, and ready for this tutorial.

Installing Nexmo PHP SDK

Note: Composer commands are required to run from within the PHP docker container. From the Running the Database Migrations tutorial step, you remotely accessed the terminal for the PHP docker container. If you leave the container's terminal session, you can get back to it by running docker-compose exec php bash from the docker/ directory.

The tutorial uses Vonage Verify API. The easiest way to use this in PHP is to install our PHP SDK.

To install this run:

composer require nexmo/client

In your Vonage Developer Dashboard, you'll find "Your API credentials", make a note of these.

Within the directory symfony/, add the following two lines to your .env.local file (replacing the api_key and api_secret with your key and secret):

VONAGE_API_KEY= VONAGE_API_SECRET=

Create a new directory called Util inside symfony/src/, and within that directory create a new file called VonageUtil.php.

This Utility class will handle any code that uses the Nexmo PHP SDK. The example below will not do anything other than creating a NexmoClient object with the authentication credentials you've saved in .env.local. Copy the example below into your newly created VonageUtil.php:

<!--?php

// symfony/src/Util/VonageUtil.php

namespace App\Util;

use App\Entity\User;
use Nexmo\Client as NexmoClient;
use Nexmo\Client\Credentials\Basic;
use Nexmo\Verify\Verification;

class VonageUtil
{
    /** @var NexmoClient */
    protected $client;

    public function __construct()
    {
        $this--->client = new NexmoClient(
            new Basic(
                $_ENV['VONAGE_API_KEY'],
                $_ENV['VONAGE_API_SECRET']
            )
        );     
    }
}

Creating a Verification Page

Verify New Columns

New properties are needed inside the User entity to process verification of a new user correctly.

Within your Docker Terminal, type

php bin/console make:entity

You're going to create three new properties. So following the steps in the above command, enter the values as listed below:

Where it asks for an Entity type User

- New property name: countryCode
- Type: string
- Length: 2
- Is Nullable: false
- New property name: verificationRequestId
- Type: string
- Length: 255
- Is Nullable: true
- New property name: verified
- Type: boolean
- Is Nullable: false

The countryCode is needed to determine which country the phone number belongs to for the Verify API to make the call successfully.

The verificationRequestId the ID the Verify API initially returns to the server, which when paired with the verification code verifies the user.

The verified property allows the system to determine whether a user has verified or not.

You'll need to run the following to generate a new migration file with these database changes.

php bin/console make:migration

The above command detects any changes made to the Entity files in your project. It then converts these changes into SQL queries which, when run as a migration, will persist the changes to your database.

The generated migration files are inside symfony/src/Migrations if you wish to see the upcoming database changes.

If you're happy with these changes, run the command below to persist them to the database.

php bin/console doctrine:migrations:migrate

Include Country Code in User Registration

Open your RegistrationFormType class in symfony/src/Form/RegistrationFormType.php. Add a new include for the ChoiceType class form type at the top of the file:

use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

In the same file, make the following changes:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
+      ->add(
+          'countryCode',
+          ChoiceType::class,
+          [
+              'label' => false,
+              'attr' => [
+                  'class' => 'form-control form-control-lg'
+              ],
+              'choices' => [
+                  "United Kingdom" => "GB",
+                  "United States" => "US"
+              ]
+          ]
+      )

The only two options to choose from, in this demo, are GB and the US. However, other countries are supported. You can find the ISO list of countries along with their accompanying country code here: ISO.org. Please make sure that the value in the array is the two-character ISO standard for your country of choice.

Within symfony/templates/registration/register.html.twig you'll find a form row for phoneNumber. Above this add the countryCode equivalent:

+ {{ form_row(registrationForm.countryCode) }}
  {{ form_row(registrationForm.phoneNumber) }}

If you have Docker running, you can check the registration page at http://localhost:8081/register and see a page similar to what's shown below:

Registration page with country code for use with the phone numberRegistration page with country code for use with the phone number

You're welcome to use the registration form, but please do not use your number or be ready to delete that registration from the user database table.

Verify Phone Number Is Valid

When calling a number to provide a verification code, the system requires a brand name. So in the symfony/ directory, open .env.local and add a new line. Replacing VerifyWithVonage with whatever company/brand you're representing for the verification:

VONAGE_BRAND_NAME=VerifyWithVonage

To verify a number is a valid phone number, you need to check to make sure the phone number is in the correct format and is valid for the region (country code) you've provided. For this, you're going to use Giggsey's PHP port of Google libphonenumber.

Run the command below to install this library.

composer require giggsey/libphonenumber-for-php

Open your VonageUtil file, found in symfony/src/Util. Within this class, you need to add a method to validate the phone number and country code. This method also checks whether the phone number is a match for that region. If it is, the method will return the phone number in an internationalised format. Copy the following into this class:

private function getInternationalizedNumber(User $user): ?string
{
    $phoneNumberUtil = \libphonenumber\PhoneNumberUtil::getInstance();

    $phoneNumberObject = $phoneNumberUtil->parse(
        $user->getPhoneNumber(),
        $user->getCountryCode()
    );

    if (!$phoneNumberUtil->isValidNumberForRegion(
        $phoneNumberObject,
        $user->getCountryCode())
    ) {
        return null;
    }

    return $phoneNumberUtil->format(
        $phoneNumberObject,
        \libphonenumber\PhoneNumberFormat::INTERNATIONAL
    );
}

The method uses the $user object to do the following:

  • parse the phone number and country code using the libphonenumber library,

  • checks that this is a valid number for the region provided (by country code).

If the number and region are valid, it formats the phone number into an internationalised one.

Sending a Verification Call on Registration

Create a method in VonageUtil that will make use of this private method. This new public method will ensure the user's input is valid and using the Verify API, start the verification process.

public function sendVerification(User $user)
{
    // Retrieves the internationalized number using the previous util method created.
    $internationalizedNumber = $this->getInternationalizedNumber($user);

    // If the number is not valid or valid for the country code provided, then return null
    if (!$internationalizedNumber) {
        return null;
    }

    // Initialize the verification process with Vonage
    $verification = new Verification(
        $internationalizedNumber,
        $_ENV['VONAGE_BRAND_NAME'],
        ['workflow_id' => 3]
    );

    return $this->client->verify()->start($verification);
}

To save the jumping between different files, you're going to add another method to this utility class. This new method will allow you to get the request_id which is returned within the Verification object when necessary.

public function getRequestId(Verification $verification): ?string
{
    $responseData = $verification->getResponseData();

    if (empty($responseData)) {
        return null;
    }

    return $responseData['request_id'];
}

These new methods you've created inside the VonageUtil class don't do anything right now. For them to be useful, you'll need to call the functionality from within the RegistrationController so open this controller found within symfony/src/Controller/.

First, you'll need to inject the VonageUtil into the RegistrationController as a service:

+use App\Util\VonageUtil;

class RegistrationController extends AbstractController
{
+    /** @var VonageUtil */
+    protected $vonageUtil;
+
+    public function __construct(VonageUtil $vonageUtil)
+    {
+        $this->vonageUtil = $vonageUtil;
+    }

Within your register method find the three $entityManager lines and add functionality to setVerified() as false as shown below:

+$user->setVerified(false);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();

Below $entityManager->flush() make the request to initiate the verification process. So, add the following two lines that first calls the VonageUtil method to send the verification request, the second line parses the response, and saves the requestId as a variable:

$verification = $this->vonageUtil->sendVerification($user);
$requestId = $this->vonageUtil->getRequestId($verification);

In the class find the following code:

return $guardHandler->authenticateUserAndHandleSuccess(
    $user,
    $request,
    $authenticator,
    'main' // firewall name in security.yaml
);

Replace this with functionality that first checks whether you've set the requestId, saves the requestId to the user, and then authenticates the user:

if ($requestId) {
    $user->setVerificationRequestId($requestId);
    $entityManager->flush();

    return $guardHandler->authenticateUserAndHandleSuccess(
        $user,
        $request,
        $authenticator,
        'main' // firewall name in security.yaml
    );
}

Verify Form

Within your Docker Terminal, run the command below, and then follow the instructions entering the values as listed:

php bin/console make:form
- Name: VerifyFormType
- Entity name: User

Creating a verify form in SymfonyCreating a verify form in Symfony

By submitting this, you should have a new class inside symfony/src/Form/ called VerifyFormType.php. Some changes are needed for this form to work as expected:

Replace the following lines:

->add('phoneNumber')
->add('roles')
->add('password')
->add('countryCode')
->add('verificationRequestId')
->add('verified')

with:

->add('verificationCode', TextType::class, [
    'mapped' => false,
    'attr' => [
        'class' => 'form-control form-control-lg'
    ],
    'constraints' => [
        new NotBlank([
            'message' => 'Please enter a verification code',
        ]),
        new Length([
            'min' => 4,
            'max' => 4,
            'minMessage' => 'The verification code is a 4 digit number.',
        ]),
    ],
])

You've just removed form fields that shouldn't be updated, added a new unmapped field (a field not mapped to the database table) called verificationCode. The verificationCode is sent to the API to verify the phone number.

At the top of the file, add three more includes. These are the fully qualified class names of classes used in the example code above.

use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

Verifying the Code

In your VonageUtil class, you need a new method to call the Vonage API to verify that the code provided by the user is valid. Put the example below into your VonageUtil class:

public function verify(string $requestId, string $verificationCode)
{
    $verification = new Verification($requestId);

    return $this->client->verify()->check($verification, $verificationCode);
}

Create a new template file inside symfony/templates/registration called verify.html.twig

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

{% block title %}Verify{% endblock %}

{% block body %}

Inside your RegistrationController a new method is required to display the above template and handle the form submission.

First, include the VerifyFormType and a Vonage class Verification at the top:

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
+use App\Form\VerifyFormType;
+use Nexmo\Verify\Verification;

Then, create the new method:

/**
 * @Route("/register/verify", name="app_register_verify")
 */
public function verify(Request $request): Response
{
    $user = $this->getUser();
    $form = $this->createForm(VerifyFormType::class, $user);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $verify = $this->vonageUtil->verify(
            $user->getVerificationRequestId(),
            $form->get('verificationCode')->getData()
        );

        if ($verify instanceof Verification) {
            $user->setVerificationRequestId(null);
            $user->setVerified(true);

            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->flush();

            return $this->redirectToRoute('profile');
        }
    }

    return $this->render('registration/verify.html.twig', [
        'verificationForm' => $form->createView(),
    ]);
}

At this point in the tutorial, the registration process is as follows:

  • on /register enter a phone number and password

  • a phone call is received quoting a four-digit number

  • redirected to /profile

There is currently no checking to ensure the user is verified.

Enforcing Verification

In this step, you're going to implement an event subscriber that checks for whether the user has verified before allowing them to access secured pages. If the user is not verified, they get redirected back to the verify form to input their verification code.

In your Docker Terminal, type the command to make a new event subscriber and follow the instructions in the screen with the following values:

php bin/console make:subscriber
- Class name: `VerifiedUserSubscriber`
- Event to subscribe to: `kernel.controller`

The image below shows an example of what is input to complete the command:

Verified user event subscriberVerified user event subscriber

Open VerifiedUserSubscriber which can be found in symfony/src/EventSubscriber/.

Add the checks and restrictions to the onKernelController method.

First you want to check whether the user is trying to access the profile URL or not. If they aren't then return and allow them to proceed to their destination page:

if (!preg_match('/^\/profile/i', $event->getRequest()->getPathInfo())) {
    return;
}

You now want to check whether the user has authenticated or not. To do this, inject the tokenStorage service into the event subscriber. While doing this, to save time, inject the router service for functionality after the user check.

At the top of the file along with the other class inclusions add the following:

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;

Next, inject the two services via the constructor of the class and set these services as properties within the class:

/** @var RouterInterface */
protected $router;

/** @var TokenStorageInterface */
private $tokenStorage;

/**
 * @param RouterInterface $router
 * @param TokenStorageInterface $tokenStorage
 */
public function __construct(
    RouterInterface $router,
    TokenStorageInterface $tokenStorage
) {
    $this->router = $router;
    $this->tokenStorage = $tokenStorage;
}

Back within the onKernelController function, below the check for whether the user is accessing the profile or not, add the following, which checks whether the user has authenticated and whether they're verified:

if (null === $user = $this->tokenStorage->getToken()->getUser()) {
    return;
}

// Check whether the user is verified, if they are, allow them to continue to their destination.
if ($user->getVerified()) {
    return;
}

Finally, if the user is at this point, they're a logged-in user, they're trying to access the profile section, and they're not verified. So redirect them to the verify route to make sure they have to verify their account before proceeding.

$route = $this->router->generate('app_register_verify');
$event->setController(function () use ($route) {
    return new RedirectResponse($route);
});

Test It!

The full method of testing this tutorial now is to register a new account on register with a valid phone number. You are taken to verify when you submit this form.

The phone number will receive a call quoting a four-digit code, which you'll need to enter into the form on the verify page. Now the profile is accessible.

You've now integrated a two-step registration process into your Symfony application using the Vonage Verify API. The example provided is just one of many ways to use the Verify API. Whether it be via multi-factor authentication during login or verifying, a user's phone number is valid to ensure they are contactable.

If this tutorial has piqued your interest in our Verify API, but PHP isn't the language of your choice, other tutorials in various languages or services can be found here on the Vonage blog, such as:

The finished code for this tutorial can be found on the GitHub repository in the end-tutorial branch.

Greg HolmesVonage Alumni

Former Developer Educator @Vonage. Coming from a PHP background, but not restricted to one language. An avid gamer and a Raspberry pi enthusiast. Often found bouldering at indoor climbing venues.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.