Build a Simple Phone Verification System with Twilio, PHP, MySQL, and jQuery

April 17, 2012
Written by

j0O0q477iroVtcxzYfoJ8d3BV2BSoV7IvzTuunzGHPC8R3ezwO39230xhEmE6E6VOawOXtUpS7objUHdcRfQRCpB_BI7iK48v-lIrcPI5umB55Cv_XbVgCm6GPenBFRAZEPCulQ_

Verifying the phone numbers of your users is a fast, effective way to tamp down on fraud and spam accounts. This post will help you build such a system using Twilio Voice, PHP 7, MySQL 5.x and jQuery.

Below we’ll walk through the code but if you have an existing MAMP environment or equivalent you can download the complete example here and get started. To run this example on your own server, update the database settings in database.php and your Twilio account settings in call.php. This example also makes use of our Twilio PHP Helper library installed via Composer.

Basic steps

  1. User visits verification web page and enters phone number.
  2. Random verification code is generated and user is called and prompted to enter code.
  3. If code is entered incorrectly, re-prompt to enter code.
  4. If code is entered correctly, update database.
  5. Update web page with status message.

Step Zero: MySQL Database setup

If you already have MySQL setup, skip this step. We’ll be managing our MySQL database with MAMP. Once you’ve installed and setup the GUI, you should be able to reach your server on localhost:8888 or whatever port MAMP has directed you to.

It looks something like this:


Keep the Host, User and Password values handy for later. You can then access the phpMyAdmin interface by clicking it from there where we will take the next steps.


On this page you will want to create a new database, I’ve called it verify and set the Collation to utf8_general_ci. We will then setup the database schema with one table called numbers.

We’ll be storing the phone number along with the generated verification code in a MySQL database so that we can update the verified user info an retrieve it later. For this example there is a simple table definition which you can store as dbschema.sql and can be added directly to the phpMyAdmin interface as well. Copy and paste the code below in the SQL tab as the following clip shows.

—
— Table structure for table `numbers`
—

CREATE TABLE IF NOT EXISTS `numbers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `phone_number` varchar(50) DEFAULT NULL,
  `verification_code` int(11) DEFAULT NULL,
  `verified` tinyint(1) NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

You will want to be sure to save all of the files we create for this post in the MAMP folder htdocs. The full path should look like Applications/MAMP/htdocs.

If you’ve chosen not to download the completed project, create a file called database.php and place the following code in it remembering to update it with your database info if it does not match the default values.

<?php

    function setupDatabase()
    {
        // put your database information mentioned above here
        $username = 'root';
        $password = 'root';
        $host = 'localhost';
        $dbname = 'verify';

        try {
            $pdo = new PDO("mysql:host=$host;dbname=$dbname",$username,$password);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        } catch(PDOException $e) {
            return 'ERROR: ' . $e->getMessage();
        }

        return $pdo;
    }

    // attempts to delete existing entries and
    // save verification code in DB with phone number
    function updateDatabase($phoneNumber, $code)
    {
        $pdo = setupDatabase();
        if (!is_a($pdo, 'PDO')) {
            echo 'PDO is false';
          return $pdo;
        }

        // Assuming US country code for example
        $params = [ 'phoneNumber' => '1' . $phoneNumber ];

        try {
            $stmt = $pdo->prepare("DELETE FROM numbers WHERE phone_number=:phoneNumber");
            $stmt->execute($params);

            $params['code'] = $code;
            $stmt = $pdo->prepare("INSERT INTO numbers (phone_number, verification_code) VALUES(:phoneNumber, :code)");
            $stmt->execute($params);

        } catch(PDOException $e) {
            return 'ERROR: ' . $e->getMessage();
        }

        return $code;
    }

    function matchVerificationCode($phoneNumber, $code)
    {
        $pdo = setupDatabase();
        if (!is_a($pdo, PDO::class)) {
            echo 'ERROR: PDO is false';
            return 'ERROR: PDO is false '.$pdo;
        }

        $params = [ 'phoneNumber' => $phoneNumber ];

        try {
            $stmt = $pdo->prepare("SELECT * FROM numbers WHERE phone_number=:phoneNumber");
            $stmt->execute($params);

            $result = $stmt->fetch();
            $response = 'unverified';
            if ($result['verification_code'] == $code) {
                $stmt = $pdo->prepare("UPDATE numbers SET verified = 1 WHERE phone_number=:phoneNumber");
                $stmt->execute($params);
                $response = 'verified';
            }

            return $response;

        } catch(PDOException $e) {
            return 'ERROR: ' . $e->getMessage();
        }
    }

    function statusIs($phoneNumber)
    {
        $pdo = setupDatabase();
        if (!is_a($pdo, 'PDO')) {
            echo 'PDO is false';
            return $pdo;
        }

        $params = [ 'phoneNumber' => $phoneNumber ];

        try {
            $stmt = $pdo->prepare("SELECT * FROM numbers WHERE phone_number=:phoneNumber");
            $stmt->execute($params);

            $result = $stmt->fetch(PDO::FETCH_ASSOC);
            if ($result['verified'] == 1) {
                return 'verified';
            }

            return 'unverified';

        } catch(PDOException $e) {
            return 'ERROR: ' . $e->getMessage();
        }
    }

 

Step One: The verification web page

We’ll start with the HTML the user will see. Create a file called index.php and add the following code to the :

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Phone Verification by Twilio</title>
        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
        <script type="text/javascript" src="main.js"></script>
    </head>
    <body>
        <div id="errors"></div>
        <form id="enter_number">
            <p>Enter your phone number:</p>
            <p><input type="text" name="phone_number" id="phone_number" /></p>
            <p><input type="submit" name="submit" value="Verify" /></p>
        </form>

        <div id="verify_code" style="display: none;">
            <p>Calling you now.</p>
            <p>When prompted, enter the verification code:</p>
            <h1 id="verification_code"></h1>
            <p><strong id="status">Waiting...</strong></p>
        </div>
    </body>
</html>

When the user visits the page they’ll be presented with the form asking them for their phone number. Please note that while we don’t verify the phone number in this example, in a production environment you would want to make sure the phone number is E.164 formatted. Once entered, we’ll hide the form and show them their verification code. Here’s the jQuery needed to make that transition as referenced in the html above in main.js:

$(document).ready(function(){
    $("#enter_number").submit(function(e) {
        e.preventDefault();
        initiateCall();
    });
});

function initiateCall() {
    $.post("call.php", { phone_number : $("#phone_number").val() }, null, "json")
        .fail(
            function(data) {
                showErrors(data.errors);
            })
        .done(
            function(data) {
                showCodeForm(data.verification_code);
            })
    ;
    checkStatus();
}

function showErrors(errors) {
    $("#errors").text(code);
}

function showCodeForm(code) {
    $("#verification_code").text(code);
    $("#verify_code").fadeIn();
    $("#enter_number").fadeOut();
}

function checkStatus() {
    $.post("status.php", { phone_number : $("#phone_number").val() },
        function(data) { updateStatus(data.status); }, "json");
}

function updateStatus(current) {
    if (current === "unverified") {
        $("#status").append(".");
        setTimeout(checkStatus, 3000);
    }
    else {
        success();
    }
}

function success() {
    $("#status").text("Verified!");
}

Our initiateCall() function kicks off an AJAX request to generate the random code and initiate the phone call, which we will address later. Once we’ve got the code, we’ll update the UI to show it to the user and start polling our database for status updates. The code to poll the server looks like this in status.php:

<?php
    require("database.php");

    // require POST request
    if ($_SERVER['REQUEST_METHOD'] != "POST") die;

    // assuming US country code for example
    $json["status"] = statusIs('1' . $_POST["phone_number"]);

    header('Content-type: application/json');
    echo(json_encode($json));
?>

Once we’ve got confirmation from the server that the number has been verified, we update the UI to let the user know.

Step Two: Generate and display random code

Step two in our walkthrough overlaps with step one since it is initiated when the user submits the form with their phone number. We’ll pick up in our initiateCall() function which makes the AJAX POST request to call.php. Inside call.php we generate the random 6-digit verification code and start the phone call with the Twilio REST API using the PHP Helper Library. If you haven’t already, now would be a good time to run the following command in your project directory:

composer require twilio/sdk

You will also want to replace the $accountSid , $authToken , $outgoingNumber , and $endPoint  variables with your own information.

Once we have the verification code we’ll send it back as JSON for the browser to parse and display.

<?php
    require __DIR__ . '/vendor/autoload.php';
    require("database.php");

    use TwilioRestClient;

    function returnError($error)
    {
        $json = array();
        $json["error"] = $error;

        header('Content-type: application/json');
        http_response_code(500);
        echo(json_encode($json));
    }

    function makeCall($submittedNumber, $code)
    {
        // put your project information here
        $accountSid = "YOUR_ACCOUNT_SID";
        $authToken = "YOUR_AUTH_TOKEN";
        $outgoingNumber = 'YOUR_TWILIO_NUMBER';
        $endPoint = "YOUR_URL/twiml.php";

        // Instantiate a new Twilio Rest Client
        $client = new Client($accountSid, $authToken);

        try {
            // initiate phone call via Twilio REST API
            $client->account->calls->create(
                $submittedNumber,        // The phone number you wish to dial
                $outgoingNumber,         // Verified Outgoing Caller ID or Twilio number
                [ "url" => $endPoint ]   // The URL of twiml.php on your server
            );
        } catch (Exception $e) {
            returnError($e->getMessage());
        }

        // return verification code as JSON
        $json = array();
        $json["verification_code"] = $code;

        header('Content-type: application/json');
        echo(json_encode($json));
    }

    // require POST request
    if ($_SERVER['REQUEST_METHOD'] != "POST") die;

    // save a verification code in DB with phone number
    // attempts to delete existing entries first
    $submittedNumber = $_POST["phone_number"];
    $code = rand(100000, 999999);
    $updateError = updateDatabase($submittedNumber, $code);

    if (strpos($updateError, 'ERROR:') !== false) {
        returnError($updateError);
    } else {
        makeCall($submittedNumber, $code);
    }

 

Steps Three and Four: Collect and verify code via phone call

When initiating the call we direct Twilio to use the twiml.php file which generates the necessary <Gather>  TwiML to prompt the caller to enter their code. The first time this file is requested we’ll ask the caller to enter their code. Once they’ve entered 6 digits, Twilio will make another post to the same URL with the Digits POST parameter now included. With this information we look up the caller’s phone number in the database and check for a match. If the code entered is incorrect, we’ll re-prompt them to enter it again. Once they’ve entered the correct code we update the database and thank them for calling.

<?php
    require __DIR__ . '/vendor/autoload.php';
    require("database.php");

    use TwilioTwiml;

    $response = new Twiml;


    if (empty($_POST["Digits"])) {
        $gather = $response->gather([ 'input' => 'dtmf', 'timeout' => 10, 'numDigits' => 6 ]);
        $gather->say("Please enter your verification code.");
    } else {
        // grab caller phone number and caller entered code
        $submittedNumber = ltrim($_POST["Called"], ' ');
        $submittedCode = $_POST["Digits"];

        // verify code and phone number against db record
        $match = matchVerificationCode($submittedNumber, $submittedCode);
        if ($match == 'verified') {
            $response->say("Thank you! Your phone number has been verified.");
        } else {
            $gather->say("Verification code incorrect, please try again.");
        }
    }
    header('Content-Type: text/xml');
    echo $response
?>

 

Step Five: Update the web page when the number is verified

Back in Step One we created the jQuery needed to begin polling the server for status updates. Our checkStatus() function makes a POST to status.php which looks up the phone number in the database and sends back some JSON containing the verification status.

Read more about Phone Verification

Documentation: Authy for Two Factor Authentication
Blog post: SMS Phone Verification in Rails 4 with AJAX and Twilio
Blog post: How to Build a Phone-Based Two-Factor Authentication
Blog post: Phone-Based Two-factor Authentication Is A Better Way to Stay Secure
Blog post: The Key to Phone Verification is a Good User Experience

That’s all folks!

We did it. Simple phone verification mission accomplished. Many thanks to Jonathan Gottfried for his inspiration from this post in its original incarnation. And shoutout to my colleague Margaret Staples for lending her PHP expertise.

If you have any questions or ideas for your next project, please find us on twitter or email mspeir@twilio.com.