How to Verify User Account With Phone Call OTP in CakePHP Using Twilio

March 11, 2024
Written by
Popoola Temitope
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Verify User Accounts With Phone Call OTPs in CakePHP Using Twilio

In this tutorial, you will learn how to verify a user's phone number during user registration via phone call OTP in a CakePHP application using Twilio Verify API.

The application includes the registration page, a verification page where the received OTP can be entered to verify the user account, and a profile page that is accessible only to verified users.

Prerequisites

You will need the following to complete this tutorial:

Create a new CakePHP project

CakePHP is a free, open-source web development framework for building robust and scalable PHP applications following the Model-View-Controller (MVC) architectural pattern.

To create a new CakePHP project, in your terminal navigate to the directory you want to scaffold the project and run the command below.

composer create-project --prefer-dist cakephp/app:~5.0 phone_call_otp

After the project is installed successfully, you will be prompted with the following question:

"Set Folder Permissions? (Default to Y) [Y, n]?"

Answer with " Y".

Next, run the command below to navigate to the project folder in your terminal.

cd phone_call_otp

Set up the database connection

To connect the application to the MySQL database, open the project in your code editor. Then, navigate to the config folder and open the app_local.php file.

The database configuration is located inside the default subsection of the Datasource section. Here, you will need to replace the values for the host, username, password, and database settings with your corresponding database values, like this:

CakePHP database connection

Next, log in to your MySQL database server and create a new database named reg_users.

Create a database table

Now, let’s create a database table named member using CakePHP migration features to define the table's properties. To do so, first, run the command below to generate the database migration file.

bin/cake bake migration member

Next, navigate to the config/Migrations folder, open the migration file ending with _Member.php, and add the following code to the change() function.

$table = $this->table('members');
$table->addColumn('fullname', 'string', [
    'limit' => 255,
    'null' => true,
]);
$table->addColumn('username', 'string', [
    'limit' => 50,
    'null' => true,
]);
$table->addColumn('phone_no', 'string', [
    'limit' => 15,
    'null' => true,
]);
$table->addColumn('password', 'string', [
    'limit' => 255,
    'null' => true,
]);
$table->addColumn('verify_status', 'string', [
    'limit' => 255,
    'null' => true,
    'default' => 'Pending',
]);
$table->create();

Now, run the command below to complete the database migration.

bin/cake migrations migrate

Create the application model and entity

Next, you need to generate the application model and entity files by running the command below.

bin/cake bake model members

The command above will generate the model file named MembersTable.php inside the /src/Model/Table folder, and the entity file named Member.php inside the /src/Model/Entity folder.

Install the Twilio PHP Helper Library

To use Twilio's Verify API in the application, you need to install Twilio's PHP Helper Library using the command below.

composer require twilio/sdk

Create a Verification Service

Next, you need to create a Twilio Verification Service. This is a set of common configurations used to create and check verifications, such as one time passwords. From the Twilio Console, navigate to Explore Products > User Authentication & Identity > Verify.

There, click Create new and fill out the form that appears. Enter a "Friendly name" for the service, check "Authorize the use of friendly name." checkbox, and under Verification channels, enable SMS. Then, click Continue, and click Continue in the next prompt.

Now, you'll be on the Service settings page for your new service. Copy the Service SID and store it somewhere handy, as you'll need it in a little bit.

Retrieve your Twilio access credentials

Log in to the Twilio Console dashboard. There, you can access your Twilio Account SID and Auth Token under the Account Info section, as shown in the screenshot below.

Twilio token information

Store the Twilio access credentialsas environment variables

CakePHP does not work natively with an .env file. Therefore, you need to configure the application to use the .env file, as it securely stores sensitive data.

To utilize the .env file, you need to create a copy of the config/.env.example file and rename it to .env. To do so, run the command below.

cp config/.env.example config/.env

Next, navigate to the config folder, open the bootstrap.php file, and uncomment the following code.

if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
    $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']);
    $dotenv->parse()
        ->putenv()
        ->toEnv()
        ->toServer();
}

Now, let’s add the environment variables to the .env file. To do that, open the .env file and add the following environment variables.

TWILIO_ACCOUNT_SID="<twilio_account_sid>"
TWILIO_AUTH_TOKEN="<twilio_auth_token>"
TWILIO_VERIFICATION_SERVICE="<twilio_verification_serv ice_id>"

In the code above, replace the first two placeholders with your Twilio Account SID and Auth Token which you retrieved from the Account Info panel of the Twilio Console. Then, replace <twilio_verification_service_id> with the Verify Service SID that you stored safely earlier in the tutorial

Create a voice call service

Let’s create a TwilioService class that will connect to Twilio's Verify API to send an OTP via a phone call. In the src folder, create a subfolder named Services. Inside it, create TwilioService.php and add the following code to the file.

<?php
declare(strict_types=1);

namespace App\Service;

use Twilio\Rest\Client;
use Twilio\Rest\Verify\V2\Service\VerificationInstance;

class TwilioService
{
    private Client $client;

    public function __construct(string $accountSid, string $authToken)
    {
        $this->client = new Client($accountSid, $authToken);
    }

    public function sendVoiceOTP(string $recipientPhoneNumber): VerificationInstance
    {
        $verification = $this->client->verify
            ->v2
            ->services(env('TWILIO_VERIFICATION_SERVICE'))
            ->verifications
            ->create($recipientPhoneNumber, 'call');

        return $verification;
    }

    public function verifyVoiceOTP(string $recipientPhoneNumber, string $otp): bool
    {
        $verification_check = $this->client->verify
            ->v2
            ->services(env('TWILIO_VERIFICATION_SERVICE'))
            ->verificationChecks
            ->create([
                    'to' => $recipientPhoneNumber,
                    'code' => $otp,
                ]);

        return $verification_check->status === 'approved';
    }
}

The class above has two key methods: sendVoiceOTP() and verifyVoiceOTP(). The sendVoiceOTP() method is responsible for sending an OTP to the user's phone number ($recipientPhoneNumber) via a voice call, using the Twilio Verification Service you created earlier in the tutorial. 

The verifyVoiceOTP() method verifies a voice OTP sent to the user's phone number and returns true if the verification's status is set to approved.

Create the controller

Now, let’s create the controller for the application. Run the command below to create a controller named MemberController.

bin/cake bake controller member --no-actions

The command above will generate a controller file named MemberController.php inside the src/Controller folder.

Now, you need to add the application logic to the controller. To do that, open the MemberController.php file and replace the existing code with the following.

<?php
declare(strict_types=1);

namespace App\Controller;

use App\Model\Table\MembersTable;
use App\Service\TwilioService;
use Cake\Log\Log;

/**
 * Member Controller
 */
class MemberController extends AppController
{
    public function register()
    {
        if ($this->request->is('post')) {
            $username = $this->request->getData('username');
            $fullname = $this->request->getData('fullname');
            $phone_no = $this->request->getData('phone_no');
            $password = $this->request->getData('password');

            if (empty($username) || empty($fullname) || empty($phone_no) || empty($password)) {
                $this->Flash->error(__('Please fill in all the required fields.'));
            } else {
                $membersTable = new MembersTable();
                $user = $membersTable->findByUsername($username)->first();
                if ($user) {
                    $this->Flash->error(__('Username already exists.'));
                } else {
                    $memberData = $this->request->getData();
                    $member = $membersTable->newEmptyEntity();
                    $member = $membersTable->patchEntity($member, $memberData);
                    if ($membersTable->save($member)) {
                        $this->getRequest()->getSession()->write('username', $username);
                        $twilioService = new TwilioService(env('TWILIO_ACCOUNT_SID'), env('TWILIO_AUTH_TOKEN'));
                        $result = $twilioService->sendVoiceOTP($phone_no);
                        Log::write(LOG_DEBUG, sprintf('Verification status: %s', $result->status));
                        $this->Flash->success(__('Registration successful'));
                        $this->redirect(['action' => 'verify']);
                    } else {
                        $this->Flash->error(__('Unable to register. Please, try again.'));
                    }
                }
            }
        }
    }

    public function login()
    {
        if ($this->request->is('post')) {
            $username = $this->request->getData('username');
            $password = $this->request->getData('password');
            $membersTable = new MembersTable();
            $user = $membersTable->findByUsername($username)->first();
            if ($user) {
                if ($password === $user['password']) {
                    $this->getRequest()->getSession()->write('username', $username);
                    if ($user['verify_status'] === 'Pending') {
                        $phone_no = $user['verify_status'];
                        $twilioService = new TwilioService(env('TWILIO_ACCOUNT_SID'), env('TWILIO_AUTH_TOKEN'));
                        $twilioService->sendVoiceOTP($phone_no);
                        $this->Flash->success(__('login successful'));
                        $this->redirect(['action' => 'verify']);
                    } else {
                        $this->Flash->success(__('login successful'));
                        $this->redirect(['action' => 'profile']);
                    }
                } else {
                    $this->Flash->error(__('Incorrect password, try again'));
                }
            } else {
                $this->Flash->error(__('Username does not exit.'));
            }
        }
    }

    public function verify(): void
    {
        if ($this->getRequest()->getSession()->check('username')) {
            $sessionUsername = $this->getRequest()->getSession()->read('username');
            $membersTable = new MembersTable();
            $user = $membersTable->findByUsername($sessionUsername)->first();
            $message = 'You will receive your OTP via a phone call at ' . $user['phone_no'];
            if ($this->request->is('post')) {
                $otp = $this->request->getData('otp');
                $twilioService = new TwilioService(env('TWILIO_ACCOUNT_SID'), env('TWILIO_AUTH_TOKEN'));
                if ($twilioService->verifyVoiceOTP($user->phone_no, $otp)) {
                    $user->verify_status = 'Verified';
                    $membersTable->save($user);
                    $this->Flash->success(__('Account verified successfully'));
                    $this->redirect(['action' => 'profile']);
                } else {
                    $this->Flash->error(__('Invalid OTP.'));
                }
            }
            $this->set(compact('message'));
        } else {
            $this->redirect(['action' => 'login']);
        }
    }

    public function profile(): void
    {
        if ($this->getRequest()->getSession()->check('username')) {
            $sessionUsername = $this->getRequest()->getSession()->read('username');
            $membersTable = new MembersTable();
            $user = $membersTable->findByUsername($sessionUsername)->first();

            if ($sessionUsername === $user['username']) {
                if ($user['verify_status'] === 'Verified') {
                    $this->set(compact('user'));
                } else {
                    $this->Flash->error(__('You must Verify your account to have access to the profile page'));
                    $this->redirect(['action' => 'login']);
                }
                $this->set(compact('user'));
            } else {
                $this->Flash->error(__('You must login to have access to the profile page'));
                $this->redirect(['action' => 'login']);
            }
        } else {
            $this->Flash->error(__('You must login session have expired.'));
            $this->redirect(['action' => 'login']);
        }
    }
}

Here is a break down of the above code:

  • The register() method is used to validate the registration form and add the user’s record to the database. After the user is successfully registered, the sendVoiceOTP() function is used to generate and send an OTP to the user via a phone call. The usr is then redirected to the verify page.
  • The login() method is used to handle the login page logic. It checks whether the entered username and password are correct. If so, the user is redirected to the profile page; otherwise, they are directed to the verification page to verify their account.
  • The verify() method handles the account verification logic. It checks whether the entered OTP is correct. If so, the user's account status is updated to `verified` and the user is redirected to the profile page.
  • The profile() method handles the user profile page logic. It fetches the user's information from the database and passes it to the profile template.

Create the application templates

For all the methods in the MemberController, you need to create a corresponding template that handles the page's content. To do that, navigate to the template folder and create a new folder named Member. Inside the Member folder, create the following files:

  • Register.php
  • Login.php
  • Verify.php
  • Profile.php

Then, add the following code to the Register.php file:

<h1>Register</h1>
<?= $this->Form->create() ?>
@csrf
<?= $this->Form->control('full_name') ?>
<?= $this->Form->control('username') ?>
<?= $this->Form->control('phone_no') ?>
<?= $this->Form->control('password') ?>
<?= $this->Form->button('Register') ?>
<?= $this->Form->end() ?>
<p>have an account? <a href="login">Login here</a>.</p>

Add the following code to the Login.php file:

<h1>Login</h1>
<?= $this->Form->create() ?>
<?= $this->Form->control('username') ?>
<?= $this->Form->control('password') ?>
<?= $this->Form->button('Login') ?>
<?= $this->Form->end() ?>
<p>Don't have an account? <a href="register">Register here</a>.</p>

Add the following code to the Verify.php file:

<h3>Verify your account</h3>
<?= h($message) ?>
<?= $this->Form->create() ?>
<?= $this->Form->control('otp',['placeholder' => 'Enter your verification code']) ?>
<?= $this->Form->button('Verify my account') ?>
<?= $this->Form->end() ?>

Add the following code to the Profile.php file:

<h1>Welcome, <?= h($user['fullname']) ?></h1>
<p>Your username: <?= h($user['fullname']) ?></p>
<p>Your phone number: <?= h($user['phone_no']) ?></p>
<p>Account Status: <?= h($user['verify_status']) ?></p>

Configure the router

Now, let’s add routes for register, login, verify, and profile to the application routes. To do that, navigate to the config folder and open the routes.php file. Inside the file, locate $routes->scope() and add the following code before $builder->fallbacks().

$builder->connect('/login', ['controller' => 'Member', 'action' => 'login', '   Login Page']);
        $builder->connect('/register', ['controller' => 'Member', 'action' => 'register', 'Registration Page']);
        $builder->connect('/verify', ['controller' => 'Member', 'action' => 'verify', 'Verification Page']);
        $builder->connect('/profile', ['controller' => 'Member', 'action' => 'profile', 'Verification Page']);

Test the application

To test run the application, let’s start the application server by running the command below:

bin/cake server

The command will start the CakePHP application server. Open http://localhost:8765/member/register in your browser to access the registration page, as shown in the screenshot below, and register a new user.

User Registration Page

After the account registration is successful, you will be redirected to the verification page and receive your OTP code via phone call. Enter the verification OTP as shown in the screenshot below.

After the verification is complete, the user then has access to their profile page, as shown in the screenshot below:

User Profile Page

That's how to verify a user account with a voice call OTP in a CakePHP app using Twilio

In this tutorial, you learned how to verify a user account via phone call OTP in a CakePHP application using Twilio Verify. You can find the complete source code for this project on my GitHub. Happy coding!

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