Multi-Factor Authentication with PHP and Twilio

There are various approaches used to confirm people are in fact who they say they: reference some aspect of the user herself (e.g. biometrics), ask for something the user knows (e.g. a password), or ask for something the user physically has (e.g. an RFID card). Traditional websites implement single-factor authentication, requiring just a user’s password. But multi-factor authentication on the other hand requires verification from at least two of the different approaches and is considerably more secure.

Using biometrics to authenticate someone to your website is probably still a long ways off with many technological hurdles and civil liberties concerns to overcome. A more practical approach to multi-factor authentication is to implement options from the other two categories, such as requiring a password and a confirmation token sent to the user via phone either by SMS or voice call (the user knows the password, and has a phone). This is what you’ll learn how to implement in this article.

Telephony integration can be frustrating if you’re doing it from the ground up, and most of the time it’s not practical to build up your own infrastructure (though possible with commodity hardware and programs like Asterisk). Luckily, Twilio offers the infrastructure and an API which developers can use to write interactive telephony applications without much hassle. I’ll be making use of their services in this article.

You can make and receive phone calls and send and receive text messages with Twilio using TwiML (Twilio Markup Language) and their REST API. You can work directly with the API, or use one of the available helper libraries. The library I’ll use here is twilio-php, the library released and officially supported by Twilio.

So, are you ready to learn how you can implement multi-factor authentication? Read on!

Using Twilio from PHP

Connecting to the Twilio service is as easy as including the twilio-php library and creating a new instance of the Services_Twilio class. The object’s constructor accepts your account’s SID and auth token which are listed on your Twilio account’s dashboard page after you sign up for their service.

With an available Services_Twilio instance at your disposal, you can access their API though the account property. account exposes an instance of Services_Twilio_Rest_Account which represents your Twilio account. I’m only using one account here, but it is possible to have multiple sub-accounts. This can be useful for segmenting your interactions depending on your needs. You can learn more about sub-accounts by reading the Twilio documentation.

<?php
require "Services/Twilio.php";

define("TWILIO_SID", "…");
define("TWILIO_AUTH_TOKEN", "…");

$twilio = new Services_Twilio(TWILIO_SID, TWILIO_AUTH_TOKEN);
$acct = $twilio->account;

The account instance exposes several other properties, among which are calls and sms_messages. These are instances of objects like Services_Twilio_Rest_Calls and Services_Twilio_Rest_SmsMessages which encapsulate the REST resources used to issue your calls and messages respectively. Yet, you rarely work with these objects beyond their create() methods, and the documentation refers to the properties that expose them as “instance resources.” I’ll do the same to help avoid any confusion.

Sending an SMS Message

Sending an SMS message is done through the create() method of an SMS message instance resource ($acct->sms_messages). The method takes three arguments: the Twilio number of your account (akin to the “from address” of an email), the recipient’s number (the “to address”), and the text of your message (which can be up to 160 characters).

<?php
$code = "1234"; // some random auth code

$msg = $acct->sms_messages->create(
    "+19585550199", // "from" number
    "+19588675309", // "to" number
    "Your access code is " . $code
);

Behinds the scenes, the twilio-php library issues a POST request of some TwiML on your behalf to the Twilio API. An instance of Services_Twilio_Rest_SmsMessage is returned by the call, which encapsulates information about the message. You can see a full list of what information is made available in the documentation, but probably the more important values are exposed by the status and price properties. status reveals the status of the SMS message (either queued, sending, sent, or failed), and price reveals the amount billed to your account for the message.

Sending a Voice Call

Initiating a voice call is done through the create() method of a Calls instance resource ($acct->calls). Like sending SMS messages, you need to provide your account number, the recipient’s number, and the message. In this case, however, the message is a URL to a TwiML document that describes the nature of the call.

<?php
// Note spaces between each letter in auth code. This forces
// the speech synthesizer to enunciate each digit.
$code = "1 2 3 4";

$msg = $acct->calls->create(
    "+19585550199", // "from" number
    "+19588675309", // "to" number
    "http://example.com/voiceCall.php?code=" . urlencode($code)
);

Again the library issues a POST request on your behalf and a voice call is made. When the callee answers her telephone, Twilio’s processes retrieve and execute the commands provided in the callback URL’s XML. In the above sample that initiates a voice call, I’ve passed the confirmation code as a GET parameter in the URL to the callback script. When the URL is retrieved by Twilio, PHP will use the parameter when rendering the response.

There are only a handful of TwiML tags you need to construct the call flow, but you can use them to define flows that are quite complex (such as phone tree menus, etc.). A basic call flow for this type of scenario though could be generated by PHP and look something like this:

<?php
header("Content-Type: text/xml");
$code = isset($_GET["code"]) ? htmlspecialchars($_GET["code"]) : null;
$digit = isset($_POST["Digits"]) ? (int)$_POST["Digits"] : null;
$url = htmlspecialchars($_SERVER["PHP_SELF"]) . "?code=" . $code;

echo '<?xml version="1.0" encoding="UTF-8"?>';
?>
<Response>
<?php
if (is_null($code)) {
?>
 <Say>Sorry. An error occurred.</Say>
<?php
}
else {
?>
 <Gather action="<?php echo $url; ?>" numDigits="1">
<?php
    if (is_null($digit) || $digit == 1) {
?>
  <Say>Your access code is <?php echo $code; ?></Say>
<?php
    }
?>
  <Say>Press 1 to repeat the code.</Say>
 </Gather>
<?php
}
?>
 <Say>Good bye.</Say>
</Response>

The TwiML tags used here are Response (the root element), Say (provides text that will be spoken by Twilio), and Gather (collects input from the user).

While speaking the text as indicated by the child Say elements, Twilio will also be listening for user input because of Gather, pausing five seconds afterwards to provide a window for the user to enter her response. If Gather times out without input, it exits and executes the subsequent Say text and terminates the call. Otherwise, the input is POSTed back to the action URL for further handling.

The Twilio docs for Gather are very good at explaining the behavior and the element attributes you can use to modify the behavior, and even lists a couple samples. I recommend you give it a quick read.

There’s one last thing of note before moving on; I’ve added spaces between each digit in the auth code in the initiating script. This forces the speech synthesizer to enunciate each digit saying “one two three four” instead of “one thousand two hundred thirty four.” With speech synthesis, sometimes what we see in text isn’t always what we get. It’s okay to fudge or misspell voice dialog if it results in better clarity and understanding for your callees.

Implementing Multi-Factor Authentication

Now that you understand the basic workflow for SMS and voice interactions with Twilio, it’s time to see where it fits into the login process. What you’ll see here is pretty straight forward, and I’ve reduced the incidental code for the sake of clarity since the particulars of your own login process will inevitably vary.

The user should be presented with a login form which initiates the process. The form submission leads you to verifying her credentials and rejecting them if they’re bad, but valid credentials shouldn’t immediately authenticate the user to your application. Instead, consider the user “partially authenticated”, a state which allows her to view a second form requesting the code which has been sent to her phone. Only after she submits the correct code should the user be authorized.

<?php
session_start();

require "Services/Twilio.php";

define("TWILIO_SID", "…");
define("TWILIO_AUTH_TOKEN", "…");
define("TWILIO_FROM_NUMBER", "+19585550199");
define("TWILIO_VOICE_URL", "http://example.com/voiceCall.php");

define("DB_HOST", "localhost");
define("DB_DBNAME", "test");
define("DB_USER", "dbuser");
define("DB_PASSWORD", "dbpassword");

define("CRYPT_SALT", '$2a$07$R.gJb2U2N.FmZ4hPp1y2CN');

define("AUTH_CODE_LENGTH", 4);

$db = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_DBNAME,
    DB_USER, DB_PASSWORD);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    // first stage of authentication
    if (empty($_SESSION["username"])) {
        $username = isset($_POST["username"])
            ? $_POST["username"] : "";
        $password = isset($_POST["password"])
            ? crypt($_POST["password"], CRYPT_SALT) : "";

        $query = sprintf("SELECT username, phone_number, code_pref FROM users WHERE username = %s AND password = %s",
           $db->quote($username),
           $db->quote($password));
        $result = $db->query($query);
        $row = $result->fetch(PDO::FETCH_ASSOC);
        $result->closeCursor();

        // invalid username/password provided
        if (!$row) {
            session_unset();
        }
        // valid username/password
        else {
            $_SESSION["isAuthenticated"] = false;
            $_SESSION["username"] = $row["username"];

            // generate and send auth code
            $code = "";
            for ($i = 0; $i < AUTH_CODE_LENGTH; $i++) {
                $code .= rand(0, 9);
            }

            $twilio = new Services_Twilio(TWILIO_SID, TWILIO_AUTH_TOKEN);
            $acct = $twilio->account;

            // send code via voice or SMS depending on
            // the user's preference
            if ($row["code_pref"] == "voice") {
                // add spaces to force enunciation
                $tmpCode = join(" ", string_split($code));
                $msg = $acct->calls->create(
                    TWILIO_FROM_NUMBER,
                    $_row["phone_number"],
                    TWILIO_VOICE_URL . "?code=" .
                        urlencode($tmpCode)
                );
            }
            else {
                $msg = $acct->sms_messages->create(
                    TWILIO_FROM_NUMBER,
                    $_row["phone_number"],
                    "Your access code is " . $code
                );
            }

            // "remember" code in session
            $_SESSION["code"] = $code;
        }
    }
    // second stage authentication
    else {
        $code = isset($_POST["code"]) ? $_POST["code"] : "";
        if ($code == $_SESSION["code"]) {
            $_SESSION["isAuthenticated"] = true;
            unset($_SESSION["code"]);
        }
    }
}

if (!empty($_SESSION["isAuthenticated"])) {
?>
<h1>W00t! You're Authenticated!</h1>
<?php
}
// present login forms
else {
    if (empty($_SESSION["username"])) {
?>
<h1>Login Step 1</h1>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
 <input type="text" name="username">
 <input type="password" name="password">
 <input type="submit" value="Login">
</form>
<?php
    }
    else {
?>
<h1>Login Step 2</h1>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
 <input type="text" name="code">
 <input type="submit" value="Confirm">
</form>
<?php
    }
}

The above code is only meant to be illustrative. In your real-world application, you might want to consider adding the following:

  • Add a link to resend the confirmation code to the user’s phone.
  • Add a cancel link on the code request form in case the user decides not to continue with the process. With respect to the code above, such a link would need to unset $_SESSION["username"] since the value, besides storing the username, also acts as a “partial authentication” flag with respect to $_SESSION["isAuthenticated"].
  • Add throttling or account locking if an incorrect code has been provided.
  • Depending on your level of paranoia or the compliance requirements you face, log the authentication events somewhere. (An application which requires multi-factor authentication is usually sensitive enough to warrant an audit trail!)

Additionally, you may want to put some thought into creating sufficiently complex authentication codes for your purposes. 4-digit numeric codes are pretty common, but you’re not limited to that. If you choose to use letters or a mix of alphanumeric values, I’d suggest avoiding values that can be easily confused (such as the number 0 vs the letter O, the number 1 vs the letter I, etc.).

Summary

The proliferation of affordable mobile devices and IP-Telephony has added additional channels for interacting with users, and in this article you learn one way to leverage these channels by implementing multi-factor authentication using the “cloud communications” service Twilio.

I hope you’ve found this article helpful in understanding both multi-factor authentication and Twilio. Feel free to leave a comment below sharing how you’ve used Twilio or implemented multi-factor authentication in your own applications. I’d love to hear about it!

Image via Fotolia

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://jeunito.me Jeunito

    I have always wondered how to do this and now I can. I hope this service isn’t geo locked and is available in the Philippines. Thanks Tim.

    • Eric

      Fyi twilio list Philippines in their pricing matrix.

  • L

    It’s not secure: htmlspecialchars($_SERVER["PHP_SELF"]).

    • http://zaemis.blogspot.com Timothy Boronczyk

      I’m not sure I understand your concern, L. Could you please elaborate? htmlspecialchars() is an accepted way to escape $_SERVER["PHP_SELF"] and is done for all instances throughout the article.

      • http://www.alabiansolutions.com Alabi

        1. action=echo htmlspecialchars($_SERVER["PHP_SELF"])
        2. action=echo $_SERVER["PHP_SELF"]
        3. action=”"
        4. action=”?index.php”
        5. action=”../folder” (assuming ../folder/index.php is the file need to process the form)
        method=”post” for all five.
        My question is which is most secure and please why? This article is about authentication but I think “‘L’s” comment brings the issue of secured code into it. Thank you.