How To Implement OTP Authentication in Symfony With WhatsApp

February 27, 2024
Written by
Popoola Temitope
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Implement OTP Authentication in Symfony With WhatsApp

As the demand for seamless user experiences continues to grow, implementing secure and efficient user verification mechanisms, such as sending One-Time Passwords (OTP) via SMS, voice call, email, or WhatsApp, becomes increasingly important.

In this tutorial, you will learn how to implement and send an OTP to users via WhatsApp using Twilio Programmable Messaging API for WhatsApp. The application will include registration, login, and verification pages. During the authentication process, users are directed to the verification page where they can verify their account using an OTP sent to their registered WhatsApp number.

Prerequisites

To complete this tutorial, you will need the following:

Getting started

To create a new Symfony application and change into the new project directory, navigate to the folder where you want to scaffold the project and run the commands below:

symfony new my-project --version="6.3.*" --webapp
cd my-project

Set up the database

Let's connect the application to the database. To do this, open the .env file in the project’s root directory. Then, comment out the default DATABASE_URL setting for PostgreSQL and uncomment the DATABASE_URL for MySQL. Update the DATABASE_URL with the actual MySQL database name, username, and password as follows, replacing the placeholders with the appropriate values for your database.

DATABASE_URL="mysql://</database_user>:<database_password>@127.0.0.1:3306/<database_name>"

Now, run the command below to connect and create the database.

php bin/console doctrine:database:create

Create a Users entity

Let’s create the Users entity, which represents the users table schema and its properties, such as username, phone, email, password, etc. To do this, run the command below:

php bin/console make:entity

The above command will prompt you to enter the entity names and their properties. Set them as shown in the screenshot below.

Creating Users entity

Now, you need to run the migration commands below to migrate the database with the newly created entity.

php bin/console make:migration
php bin/console doctrine:migrations:migrate

Answer "yes" when prompted while running the second command.

Create the Form Type

Firstly, Let's create a Form Type that will be used to define the input fields for the registration form. To do this, follow these steps:

  • Navigate to the src folder.
  • Create a Form subfolder within the src folder.
  • Inside the Form subfolder, create a file named RegistrationType.php.

Now, open the RegistrationType.php file and add the following code:

<?php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('fullname')
            ->add('username')
            ->add('whatsapp_no')
            ->add('password',PasswordType::class);
    }
}

Next, let's create the login Form Type with username and password as the input fields. Inside the src/Form folder, create a file named LoginType.php and add the following code to it:

<?php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;

class LoginType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('username')
            ->add('password',PasswordType::class);
    }
}

Install the Twilio PHP SDK

Before you can use the Twilio WhatsApp API, you must install the Twilio's helper library for PHP into your project using the command below:

composer require twilio/sdk

Set the necessary environment variables

Log in to your Twilio Console Dashboard to obtain your Twilio Account SID and Auth Token, as shown in the image below:

Twilio Access Token

After obtaining them, open the .env file and set them as follows:

TWILIO_ACCOUNT_SID=<your_account_sid>
TWILIO_AUTH_TOKEN=<your_auth_token>

Set up your Twilio WhatsApp number

Twilio provides a WhatsApp Sandbox that allows developers to test the twilio WhatsApp API during development. From your Twilio Console dashboard menu, navigate to Explore Products > Messaging > Try it out > Send a WhatsApp message option, as shown in the screenshot below:

Twilio WhatsApp dashboard

Next, you will be required to send a join message from your phone to the phone number shown in the WhatsApp Sandbox. Send the join message, as shown in the image below.

Twilio WhatsApp message

Next, add the dedicated Twilio WhatsApp number (without the "whatsapp:" prefix) to the .env file as follows:

TWILIO_WHATSAPP_NUMBER=<dedicated_twilio_whatsapp_number>

Create the registration Controller

A Symfony controller is used to define the application's logic and can also define an application's routes. Run the command below to create a controller named Registration.

php bin/console make:controller Registration

The command will create two files: src/Controller/RegistrationController.php and templates/registration/index.html.twig.

Implement a service to send WhatsApp messages

Now, let's create a service class for sending OTP messages to users via WhatsApp. To create it, navigate to the src folder and create a folder called Service. Then, within the src/Service directory, create a file named TwilioService.php and add the following code to it.

<?php

namespace App\Service;

use Twilio\Rest\Client;

class TwilioService
{
    public function sendWhatsappOTP($recipientPhoneNumber, $WhatsappOTPCode)
    {
        $accountSid = $_ENV['TWILIO_ACCOUNT_SID'];
        $authToken = $_ENV['TWILIO_AUTH_TOKEN'];
        $twilioWhatsappNumber = $_ENV['TWILIO_WHATSAPP_NUMBER'];
        $client = new Client($accountSid, $authToken);
        $message = $client->messages->create(
            "whatsapp:{$recipientPhoneNumber}",
            [
                'from' => "whatsapp:{$twilioWhatsappNumber}",
                'body' => "Your OTP code is: {$WhatsappOTPCode}.",
            ]
        );
        return $message->sid;
    }
}

In the code above:

  • The sendWhatsappOTP() function receives two arguments, $recipientWhatsappNumber and $otpCode. The $recipientWhatsappNumber variable represents the recipient's WhatsApp number, where the OTP will be sent, and The $otpCode variable holds the actual OTP generated for the user's phone number verification.
  • The Client Twilio method is then used to send the WhatsApp OTP to the user's WhatsApp number.

Create the registration route

To create the registration route, navigate to the src/Controller directory, open RegistrationController.php, and replace its code with the following.

<?php

namespace App\Controller;

use App\Entity\Users;
use App\Service\TwilioService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Form\RegistrationType;
use App\Form\LoginType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

class RegistrationController extends AbstractController
{
    private $entityManager;
    private $twilioService;

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

    #[Route('/registration', name: 'registration')]
    public function index(Request $request,SessionInterface $session): Response
    {
        $form = $this->createForm(RegistrationType::class);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $formData = $form->getData();
            $existingUser = $this->entityManager->getRepository(Users::class)->findOneBy(['username' => $formData['username']]);
            if ($existingUser) {
                $errorMessage = 'Username already exists.';
                $this->addFlash('error', $errorMessage);
                return $this->redirectToRoute('registration');
            } else {
                $otp = sprintf('%06d', mt_rand(0, 999999));
                $this->twilioService->sendWhatsappOTP($formData['whatsapp_no'], $otp);
                $session->set('username', $formData['username']);
                $session->set('otp', $otp);
                $user = new Users();
                $user->setFullname($formData['fullname']);
                $user->setUsername($formData['username']);
                $user->setWhatsappNo($formData['whatsapp_no']);
                $user->setPassword($formData['password']);
                $user->setVerifyStatus('Pending');
                $this->entityManager->persist($user);
                $this->entityManager->flush();
                return $this->redirectToRoute('verify');
            }
        }

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

In the code above:

  • All the necessary dependencies are imported
  • The route for the user's registration page is defined
  • In the registration route, once a user is successfully registered, an OTP is sent to the user's WhatsApp number
  • Then, the user is redirected to the verify page where they can enter the OTP

Next, let’s create the registration form template. Navigate to the templates/registration directory and create a new file named register.html.twig. Inside the created file, add the following code to it:

{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block body %}
    <div class="container">
        <h2 class="center">Registration</h2>
        {{ form_start(form) }}
        <div class="form-group">
            {{ form_row(form.fullname, {'attr': {'placeholder': 'Full Name'}}) }}
        </div>
        <div class="form-group">
            {{ form_row(form.username, {'attr': {'placeholder': 'Username'}}) }}
        </div>
        <div class="form-group">
            {{ form_row(form.whatsapp_no, {'attr': {'placeholder': 'WhatsApp Number'}}) }}
        </div>
        <div class="form-group">
            {{ form_row(form.password, {'attr': {'placeholder': 'Password'}}) }}
        </div>
        <button type="submit">Register</button>
        {{ form_end(form) }}
       <div class="center"><span>Already have an account, <a href="login">login here</a></span></div>
    </div>
{% endblock %}

Create the verify route

To create the verify route, where the user can enter their OTP to verify their account, add the following function to RegistrationController.php.

    #[Route('/verify', name: 'verify')]
    public function verify(Request $request, SessionInterface $session): Response
    {
        $msg = "";
        if ($session->get('otp') !== null && $session->get('username') !== null) {
            $otpFromForm = $request->request->get('otp');
            $sessionOtp = $session->get('otp');
            if (empty($otpFromForm)) {
                $msg = "";
            } else {
                if ($otpFromForm == $sessionOtp) {
                    $sessionUsername = $session->get('username');
                    $userRepository = $this->entityManager->getRepository(Users::class);
                    $user = $userRepository->findOneBy(['username' => $sessionUsername]);
                    if ($user) {
                        $user->setVerifyStatus('Verified');
                        $this->entityManager->persist($user);
                        $this->entityManager->flush();
                        $msg = 'Account verified successfully.';
                        //  return $this->redirectToRoute('dashboard');
                    } else {
                        return $this->redirectToRoute('login');
                    }
                } else {
                    $msg = 'Verification code is incorrect.';
                }
            }
        } else {
            return $this->redirectToRoute('login');
        }    
        return $this->render('registration/verify.html.twig', ['message' => $msg]);
    }

In the code above, the OTP code entered by the user is verified to check if it is correct or not. If the OTP code is correct, the user's verify_status is then changed from Pending to Verified in the database.

To create the verify form template, in the templates/registration directory, create a verify.html.twig file, and add the following code:

{% extends 'base.html.twig' %}
{% block body %}
    <div class="container">
        <form method="post">
            <h2 class="center">Verify your account</h2>
                <div class="form-group">
                <div class="center"><small>Check your Whatsapp message for your OTP</small></div><br/><br/>
                    <div class="center">{{message}}</div><br/><br/>
                    <div class="form-group">
                       <input type="text" name="otp" required placeholder="Enter your OTP">
                    </div>
                    <button type="submit">Verify</button>
                </div>
            </form>
    </div>
{% endblock %}

Create the login route

To create the login route, add the following function to RegistrationController.php.

    #[Route('/login', name: 'login')]
    public function login(Request $request,EntityManagerInterface $entityManager, SessionInterface $session): Response
    {
        $msg= "";
        $form = $this->createForm(LoginType::class);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $formData = $form->getData();
            $repository = $entityManager->getRepository(Users::class);
            $login = $repository->findOneBy([
                'username' => $formData['username'],
                'password' => $formData['password'],
            ]);
            if ($login !== null) {
                if($login->getVerifyStatus()=="Pending"){
                $WhatsappNo = $login->getWhatsappNo();
                    $otp = sprintf('%06d', mt_rand(0, 999999));
                    $this->twilioService->sendWhatsappOTP($WhatsappNo, $otp);
                    $session->set('username', $formData['username']);
                    $session->set('otp', $otp);
                    return $this->redirectToRoute('verify');
                } else {
                    $msg= "Your account has been verified, and you will be redirected to your dashboard page.";
                  //  return $this->redirectToRoute('dashboard');
                }
            }  else {
                $msg= "Incorrect Username/Password";
            }
        }
        return $this->render('registration/login.html.twig', [
            'form' => $form->createView(),'message' => $msg,
        ]);
    }

In the code above,

  • The user's username and password are checked to see if they are correct.
  • The user's account verification status is checked to determine if the user has verified their account or not.
  • If the user's account verification status is 'Pending', they will be redirected to the account verification page. Otherwise, a message will be displayed to show that the account is verified.

Next, let’s create the login form template. To do that, in the templates/registration directory, create a login.html.twig file, and add the following code:

{% extends 'base.html.twig' %}
{% block title %}Login{% endblock %}
{% block body %}
<div class="container">
        <h2 class="center">Login</h2>
        <div class="center">{{message}}<br/><br/></div>
        {{ form_start(form) }}
            <div class="form-group">
                {{ form_row(form.username, {'attr': {'placeholder': 'Username'}}) }}
            </div>
            <div class="form-group">
                {{ form_row(form.password, {'attr': {'placeholder': 'Password'}}) }}
            </div>
            <button type="submit">Login</button>
        {{ form_end(form) }}
            <div class="center"><span>New user, <a href="registration">Register here</a></span></div>
 </div>
{% endblock %}

Wrapping up

Now, let’s style the application. To do so, in the templates directory, open base.html.twig file and replace its code with the following.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
        {% block stylesheets %}
        {% endblock %}

        {% block javascripts %}
        {% endblock %}
        <style>
            body {
                font-family: Arial, sans-serif;
                background-color: #f4f4f4;
                margin: 0;
                padding: 0;
                display: flex;
                justify-content: center;
                align-items: center;
                height: 100vh;
            }
            .center{
                text-align:center;
            }
            .container {
                background-color: #fff;
                padding: 20px;
                border-radius: 8px;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
                width: 300px;
            }
            .form-group {
                margin-bottom: 15px;
            }
            label {
                display: block;
                font-weight: bold;
                margin-bottom: 5px;
            }
            input {
                width: 100%;
                padding: 8px;
                box-sizing: border-box;
                border: 1px solid #ccc;
                border-radius: 4px;
            }
            button {
                background-color: #4caf50;
                color: #fff;
                padding: 10px;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                width: 100%;
            }
            button:hover {
                background-color: #45a049;
            }
        </style>
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>

Testing the application

To test the application, let’s first start the application server by running the code below:

symfony server:start

By default, the application will run on port 8000. To open the user's registration page, navigate to http://localhost:8000/registration in your browser.

On the registration page, create a new account as shown in the image below:

Registration Page
The testing WhatsApp number above must be the same number used during the Twilio WhatsApp API sandbox setup.

The application login page can be accessed at http://localhost:8000/login. During the login, the user will be redirected to the verify page if their account is not verified. You can login to your account as shown in the image below.

Login Page

When redirected to the verify page, enter the OTP sent to your WhatsApp number, as shown in the image below:

Account Verification Page

Once the entered OTP is correct, a success message will be displayed, as shown in the image below.

Account Verification Page

Now, you can then redirect the user to your preferred page such as dashboard or profile page.

That's how to send verification OTP through WhatsApp in Symfony app using Twilio

In this tutorial, you learned to create a secure user account verification system by sending a verification OTP to the user's WhatsApp number using Twilio WhatsApp API in a Symfony application.

To use Twilio WhatsApp API in production, you will need to upgrade your Twilio account to enjoy the full features of the Twilio WhatsApp API.

Popoola Temitope is a mobile developer and a technical writer who loves writing about frontend technologies. He can be reached on LinkedIn .