Secure CakePHP with One-Time Password Verification Using WhatsApp

August 05, 2025
Written by
Isijola Jeremiah
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Secure CakePHP with One-Time Password Verification Using WhatsApp

As the demand for seamless user experiences increases, it is essential to implement secure and efficient user verification mechanisms. One such method involves sending One-Time Passwords (OTPs) with WhatsApp.

In this tutorial, you will learn how to secure CakePHP by sending an OTP to users with WhatsApp during the login process, using the Twilio WhatsApp Business API. The application will include both login and verification pages. During the authentication process, users will be directed to the verification page, where they can verify their account using the OTP sent to their WhatsApp number.

Prerequisites

To follow this tutorial, you will need the following:

  • PHP 8.3 or above
  • Composer installed globally
  • Basic experience with CakePHP
  • ATwilio account (free or paid). Create a new account if you are new to Twilio.
  • A mobile phone with an active SIM and WhatsApp installed
  • Your preferred text editor or IDE and web browser

Create a CakePHP project

To create a new CakePHP project, use Composer and run the command below in your terminal.

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

When you see the prompt " Set Folder Permissions? (Default to Y) [Y, n]?", respond with " Y" to proceed with the project installation.

Configure the application

To configure the Application, follow the steps below:

  • Set "driver" to "Cake\Database\Driver\Sqlite" in config/app_local.php under "Datasources".
  • Copy .env.example to .env and config/.env.example to config/.env.
  • Add export DATABASE_URL="sqlite://127.0.0.1/resources/database/database.sqlite" to config/.env.
  • Create the SQLite file with touch resources/database/database.sqlite and set permissions to 777.
  • Clear the cache with bin/cake cache clear_all and run migrations with bin/cake migrations migrate.

After that, run the command below to navigate to the project's working directory and start the application development server.

cd whatsapp-otp-app bin/cake server

Once the application server has started, open http://localhost:8765 in your browser to view the default welcome page of the application. This will allow you to confirm that the base application is functioning properly, as shown in the screenshot below.

CakePHP version 5.1.6 Chiffon setup screen showing environment and filesystem configuration status.

Now, open the project in your preferred code editor.

Install Twilio's official PHP Helper Library

To simplify integrating with Twilio's Verify API in the application, install the Twilio PHP Helper Library using the command below in a new terminal session.

composer require twilio/sdk

Set the necessary environment variables

The .env file is used to securely store sensitive data. Let's create environment variables to store Twilio credentials, such as the Twilio Auth Token and Twilio SID, in our application. To do this, run the command below to create a .env file:

cp config/.env.example config/.env

Next, navigate to the config folder, open the .env file, and add the following environment variables:

export TWILIO_ACCOUNT_SID=<your_account_sid>
export TWILIO_AUTH_TOKEN=<your_auth_token>

To allow the application to automatically load environment variables, open the bootstrap.php file and uncomment the relevant lines.

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

Retrieve your Twilio credentials

To find your Twilio Account SID and Auth Token, log in to your Twilio Console Dashboard. Under the Account Info section, you will see your Twilio credentials as shown in the screenshot below.

Twilio account information settings displaying Account SID, Auth Token, My Twilio phone number, and API Keys.

Copy your Twilio Account SID and Auth Token, then replace it with the corresponding environment variable inside the .env file.

Set up your Twilio WhatsApp number

Twilio offers a WhatsApp Sandbox that enables developers to test the Twilio WhatsApp API during the development process. To access it, go back to your Twilio Console dashboard and navigate to Explore Products > Messaging > Try it out > Send a WhatsApp message. This option can be found in the menu, as illustrated in the screenshot below:

Twilio dashboard shows instructions for connecting to WhatsApp Sandbox, including a QR code and a test phone number.

Next, you need to send a join message from WhatsApp to the phone number displayed in the WhatsApp Sandbox, as shown in the image below:

WhatsApp chat showing Twilio OTP service confirmation for sending/receiving messages.

With that done, add the code below to the end of .env.

TWILIO_WHATSAPP_NUMBER=<dedicated_twilio_whatsapp_number>

Then, replace <dedicated_twilio_whatsapp_number> with your Twilio WhatsApp number (without the "whatsapp:" prefix).

Create the controller

Next, let's create the application's controller. Run the command below to generate a controller file named OtpVerificationController.php in the src/Controller directory.

bin/cake bake controller otpVerification --no-actions

Next, open the OtpVerificationController.php file and replace the existing code with the application logic provided below.

<?php

declare(strict_types=1);

namespace App\Controller;

use Cake\Http\Exception\BadRequestException;
use Twilio\Rest\Client;
use Cake\Log\Log;

class OtpVerificationController extends AppController
{
    private string $twilioAccountSid;
    private string $twilioAuthToken;
    private string $twilioWhatsAppNumber;

    public function initialize(): void
    {
        parent::initialize();
        $this->twilioAccountSid = env('TWILIO_ACCOUNT_SID') ?: '';
        $this->twilioAuthToken = env('TWILIO_AUTH_TOKEN') ?: '';
        $this->twilioWhatsAppNumber = env('TWILIO_WHATSAPP_NUMBER') ?: '';
    }

    public function login()
    {
        if ($this->request->is('post')) {
            $phoneNumber = $this->request->getData('phone_number');
            if (empty($phoneNumber)) {
                $this->Flash->error(__('Phone number is required.'));
                return;
            }
            $otpCode = random_int(100000, 999999);
            $this->request->getSession()->write("otp_$phoneNumber", $otpCode);
            if ($this->sendOtp($phoneNumber, $otpCode)) {
                $this->Flash->success(__('OTP sent to your WhatsApp.'));
                return $this->redirect(['action' => 'verify', '?' => ['phone' => $phoneNumber]]);
            } else {
                $this->Flash->error(__('Failed to send OTP. Try again.'));
            }
        }
    }

    public function verify()
    {
        $phoneNumber = $this->request->getQuery('phone');
        if (!$phoneNumber) {
            throw new BadRequestException('Phone number is missing.');
        }

        if ($this->request->is('post')) {
            $otpCode = $this->request->getData('otp_code');
            if (empty($otpCode)) {
                $this->Flash->error(__('OTP code is required.'));
            } else {
                if ($this->checkOtp($phoneNumber, $otpCode)) {
                    $this->Flash->success(__('Login successful.'));
                    return $this->redirect(['controller' => 'OtpVerification', 'action' => 'login']);
                } else {
                    $this->Flash->error(__('Invalid OTP. Try again.'));
                }
            }
        }

        $this->set(compact('phoneNumber'));

    }

    private function sendOtp(string $phoneNumber, int $otpCode): bool
    {
        $twilio = new Client($this->twilioAccountSid, $this->twilioAuthToken);
        try {
            $message = "Your OTP code is: $otpCode. Do not share this code with anyone.";
            $twilio->messages->create(
                "whatsapp:$phoneNumber",
                [
                    'from' => "whatsapp:$this->twilioWhatsAppNumber",
                    'body' => $message
                ]
            );
            return true;
        } catch (\Exception $e) {
            Log::error('Twilio Error (sendOtp): ' . $e->getMessage());
            return false;
        }
    }

    private function checkOtp(string $phoneNumber, string $otpCode): bool
    {
        $storedOtp = $this->request->getSession()->read("otp_$phoneNumber");
        if ($storedOtp && $storedOtp == $otpCode) {
            $this->request->getSession()->delete("otp_$phoneNumber"); // Clear OTP after successful verification
            return true;
        }
        return false;
    }
}

In the code above:

  • The login() method validates the phone number input, generates a six-digit OTP, stores it in the session, and sends it via Twilio WhatsApp using sendOtp(). If successful, the user is redirected to the verification page; otherwise, an error message is displayed.
  • The verify() method retrieves the phone number from the request, uses checkOtp() to check the OTP entered by the user against the one stored in the session, and clears it upon successful verification. If valid, the user is redirected to the login page; otherwise, an error message is displayed.
  • The sendOtp() method integrates with Twilio's API to send the OTP via WhatsApp. If the message is sent successfully, it returns true; otherwise, it logs the error and returns false.
  • The checkOtp() method reads the stored OTP from the session and compares it with the user’s input. If they match, the OTP is cleared from the session, and the function returns true; otherwise, it returns false.

Create the UI template

For each method in the OtpVerificationController you need to create a corresponding template that handles the page's content. To do that, navigate to the templates folder and create a new folder named OtpVerification. Inside the OtpVerification folder, create the following files:

  • login.php
  • verify.php

Next, add the following code to the login.php file:

<?= $this->Form->create() ?>
    <fieldset>
        <legend>Login with OTP</legend>
        <?= $this->Form->control('phone_number', ['label' => 'Enter Phone Number', 'required' => true]) ?>
    </fieldset>
    <?= $this->Form->button(__('Send OTP')) ?>
<?= $this->Form->end() ?>

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

<?= $this->Form->create() ?>
    <fieldset>
        <legend>Verify OTP</legend>
        <p>Enter the OTP sent to <?= h($phoneNumber) ?></p>
        <?= $this->Form->control('otp_code', ['label' => 'Enter OTP', 'required' => true]) ?>
    </fieldset>
    <?= $this->Form->button(__('Verify OTP')) ?>
<?= $this->Form->end() ?>

Add the route configuration

Next, let's add routes for login and verify to the application. To do this, navigate to the config folder and open the routes.php file. Inside the file, locate $routes->scope() and insert the following code before $builder->fallbacks().

$builder->connect('/login', ['controller' => 'OtpVerification', 'action' => 'login']);
$builder->connect('/verify', ['controller' => 'OtpVerification', 'action' => 'verify']);

Navigate to the src/Application.php file and comment out the following code to disable CSRF:

->add(new CsrfProtectionMiddleware([
    'httponly' => true,
]))

Test the application

Ensure the application is still running. Open http://localhost:8765/login in your browser to access the login page, input your mobile phone number, and submit the form.

Login screen for CakePHP with a field to enter a phone number and a Send OTP button.

You will be redirected to the verify page where you will input the OTP sent to your WhatsApp account.

OTP verification interface for CakePHP displaying fields to enter and verify OTP sent via WhatsApp.

Below is an image of the OTP received:

Messaging app showing Twilio OTP code and sandbox message, with secure service notice from Meta.

If OTP is successfully Verified, you will then be redirected to the login page.

Screenshot of a CakePHP page showing a successful login message and a prompt to login with OTP by entering the phone number.

That's how to secure CakePHP apps using WhatsApp-based One-Time Password Verification with Twilio

In this tutorial, you learned how to build a WhatsApp-based verification system in CakePHP using the Twilio WhatsApp Business API. You set up phone number verification for OTP requests via WhatsApp, enhancing security by replacing traditional SMS-based verification with WhatsApp OTP authentication.

Isijola Jeremiah is a developer who specialises in enhancing user experience on both the backend and frontend. Contact him on LinkedIn.