Verify Phone Numbers with Bref PHP and Twilio Verify

June 18, 2020
Written by
Michael Okoko
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Verify Phone Numbers with Bref PHP and Twilio Verify.png

Bref is a composer package that helps you deploy your PHP applications as AWS Lambda functions. It leverages the Serverless Framework to provision and deploy such applications, and provides support for common frameworks like Laravel, Symfony, and Slim PHP.

In a previous article, we explored how we can use the Bref PHP framework to forward errors from our Twilio application to a Slack channel. In this article, we will revisit Bref by building a service that verifies users’ phone numbers using the Twilio Verify API.

Prerequisites

To complete this tutorial, you will need:

Create a Twilio Verify Service

To get started, head over to your Twilio Verify Console and create a new Verify Service. Take note of the generated Service SID. Twilio lets you reuse the same service across different channels such as Email and SMS, so feel free to use an existing one if already available.

Create Project and Install Dependencies

Create a new folder for the project and install the required dependencies with the commands below:

$ mkdir bref-twilio-verify && cd bref-twilio-verify
$ composer require bref/bref twilio/sdk vlucas/phpdotenv slim/slim slim/psr7

The composer command above will install the Bref serverless framework and the Twilio helper library phpdotenv to help us use environment variables declared in a .env file. Additionally, it installs the core of the Slim PHP framework and some of its dependencies. That way, you can use minimal features of the framework (such as routing) without bringing in the extras that come with using the full framework.

Initialize a bref project in the current directory with ./vendor/bin/bref init and select the HTTP application runtime to set up an HTTP endpoint. This enables your code to process HTTP requests using the AWS API Gateway. Initializing bref generates two additional files in the working directory:

  • serverless.yml which contains information about the application and the configuration details used to provision and deploy it to the specified cloud provider (AWS).
  • index.php the application entry point. By default, it’s the Bref splash screen.

Next, create a .env file in the project root to hold your Twilio credentials with:

$ touch .env

Add the Twilio Verify SID generated earlier, as well your Twilio credentials to the .env file as shown below:

TWILIO_VERIFY_SID=VAXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_AUTH_TOKEN=40XXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXX

Setup Application Routes with Slim

Slim is a minimal framework for developing PHP applications, whose lightness makes it more fitting for this use case. Specifically, we will use it to power the application routes which comprise three endpoints:

  • GET /: An index endpoint for performing a health check.
  • POST /send: Receives a user’s phone number (in E.164 format) as a request parameter and sends the verification token to the phone number. It also responds with a “Token sent” message if successful.
  • POST /verify: Receives a user’s phone number (also in E.164 format) and the received token which is then used to verify the number. Its response depends on if the token is valid or not.

To implement the endpoints described above, first, create an app directory in the project root folder and move the generated index.php file there. Next, set up the Slim framework by replacing the content of the index.php file (using your favorite IDE) with the code block below:

<?php
use App\JsonParserMiddleware;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Twilio\Rest\Client;

require_once __DIR__."/../vendor/autoload.php";

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__."/../");
$dotenv->load();

$app = AppFactory::create();
$app->addMiddleware(new JsonParserMiddleware());
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorHandler = $errorMiddleware->getDefaultErrorHandler();
$errorHandler->forceContentType("application/json");

The code above loads the .env file we created earlier and adds a JsonParserMiddleware to parse incoming JSON requests. Also, it forces the default Slim error handler to send back application errors as a JSON response.

Complete your implementation by appending the code below to your index.php file:

$app->get('/', function (Request $request, Response $response) use ($app) {
        $payload = json_encode(["message" => "I am alive!"]);
        $response->getBody()->write($payload);
        return $response
           ->withHeader("Content-type", "application/json")
           ->withStatus(200);
});

/*
 * POST /send
 * @param string phone E.164 formatted phone number of the recipient
 */
$app->post("/send", function (Request $request, Response $response) {
        $data = $request->getParsedBody();
        $payload = [];
        $statusCode = 200;
        if ($data['phone'] == null) {
            $payload['error'] = "Phone is required";
            return response($response, $payload, 422);
        }
        $verifier = getVerifyService();
        try {
            $attempt = $verifier->verifications->create($data['phone'], 'sms');
            $payload['message'] = "Token sent";
            $payload['sid'] = $attempt->sid;
        } catch (\Twilio\Exceptions\TwilioException $e) {
            $payload['error'] = $e->getMessage();
            $statusCode = 400;
        }
        return response($response, $payload, $statusCode);
});

/*
 * POST /verify
 * @param string phone E.164 formatted phone number of the recipient
 * @param string token The received token as entered by the recipient
 */
$app->post("/verify", function (Request $request, Response $response) {
        $data = $request->getParsedBody();
        $payload = [];
        $statusCode = 200;
        if ($data['phone'] == null || $data['token'] == null) {
            $payload['error'] = "Phone and token fields are required";
            return response($response, $payload, 422);
        }
        $verifier = getVerifyService();
        try {
            $attempt = $verifier->verificationChecks->create(
                $data['token'],
                ['to' => $data['phone']]
            );
            if ($attempt->valid) {
                $payload['message'] = "Verified!";
            } else {
                $payload['message'] = $attempt->status;
                $payload['data'] = $attempt;
            }
        } catch (\Twilio\Exceptions\TwilioException $e) {
            $statusCode = 500;
            $payload['error'] = $e->getMessage();
        }
        return response($response, $payload, $statusCode);
});

function response(Response $response, array $payload, $statusCode = 200) {
        $response->getBody()->write(json_encode($payload));
        return $response
            ->withHeader("Content-type", "application/json")
            ->withStatus($statusCode);
}

function getVerifyService() {
        $token = getenv("TWILIO_AUTH_TOKEN");
        $sid = getenv("TWILIO_ACCOUNT_SID");
        $verifySid = getenv("TWILIO_VERIFY_SID");
        $twilio = new Client($sid, $token);
        return $twilio->verify->v2->services($verifySid);
}

$app->run();

Now it is time to set up the JsonParserMiddleware imported earlier. Create a new JsonParserMiddleware.php file in the same app folder with the following content:

<?php
namespace App;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class JsonParserMiddleware implements MiddlewareInterface
{
        public function process(Request $request, RequestHandler $handler): Response
        {
            $contentType = $request->getHeaderLine('Content-Type');

            if (strstr($contentType, 'application/json')) {
                $contents = json_decode(file_get_contents('php://input'), true);
                if (json_last_error() === JSON_ERROR_NONE) {
                    $request = $request->withParsedBody($contents);
                }
                return $handler->handle($request);
            } else {
                $response = new \Slim\Psr7\Response();
                $payload = ['error' => "Bad request"];
                $response->getBody()->write(json_encode($payload));
                return $response
                    ->withHeader("Content-type", "application/json")
                    ->withStatus(400);
            }
        }
}

In the code above, we defined a JsonParserMiddleware class that sends back any request whose Content-Type isn’t of type application/json with a “Bad request” response. Besides validating the header, it also parses the request body (which is available as “php://input”) and ensures it is cast as valid JSON.

Next, update your composer.json file so it is aware of the new app namespace by adding the following autoload block after the require object:

        "autoload": {
            "psr-4": {
                "App\\": "app/"
            }
        }

Now, your composer.json file should look like the one below:

{
        "require": {
            "bref/bref": "^0.5.24",
            "slim/slim": "^4.5",
            "slim/psr7": "^1.1",
            "ext-json": "*",
            "twilio/sdk": "^6.5",
            "vlucas/phpdotenv": "^4.1"
        },
        "autoload": {
            "psr-4": {
                "App\\": "app/"
            }
        }
}

NOTE: You may need to run composer update after this update in order to access the index / endpoint.

Test Endpoints With cURL

You can test your application before deploying to AWS using cURL or Postman by spinning up the built-in PHP server in the project directory. Start the server with the command below:

$ php -S localhost:8000 -t app/

The following cURL command sends a verification token to the phone number specified:

$ curl -H "Content-Type: application/json" -X POST -d '{"phone": "+23480000000"}' http://localhost:8000/send

Make sure you run the previous command in a new terminal and update the phone number with your own.

You can verify a given token by using the command below:

$ curl -H "Content-Type: application/json" -X POST -d '{"phone": "+23480000000", "token": "123456"}' http://localhost:8000/verify

Deploying to Lambda

Create IAM Policy and User

To make your AWS resources available to the serverless framework, grab the AWS Key, Secret, and region of an IAM user. Ensure the user has full access to the following resources:

  • IAM
  • AWS S3
  • API Gateway
  • Lambda
  • CloudFormation
  • CloudWatch Logs

You can do that by creating a new policy using the JSON policy document below and attaching it to your new IAM user.

{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "iam:*",
                    "s3:*",
                    "apigateway:*",
                    "lambda:*",
                    "cloudformation:*",
                    "logs:*"
                ],
                "Resource": "*"
            }
        ]
}

Navigate one folder up in your terminal and set up the AWS credentials locally with the command below:

$ serverless config credentials --provider aws --key  AWS_KEY  --secret AWS_SECRET --profile bref-twilio-verify

Deploy with Serverless

Next, replace the content of the serverless manifest file (i.e serverless.yml) with the one below:

service: twilio-verify

provider:
        name: aws
        region: us-west-2
        runtime: provided
        profile: bref-twilio-verify

plugins:
        - ./vendor/bref/bref

functions:
        index:
            handler: app/index.php
            timeout: 28 # in seconds (API Gateway has a timeout of 29 seconds)
            layers:
                - ${bref:layer.php-73-fpm}
            events:
                - http:
                      path: /
                      method: GET
        sendToken:
            handler: app/index.php
            layers:
                - ${bref:layer.php-73-fpm}
            events:
                - http:
                      path: /send
                      method: POST
        verifyToken:
            handler: app/index.php
            layers:
                - ${bref:layer.php-73-fpm}
            events:
                - http:
                    path: /verify
                    method: POST

# Exclude files from deployment
package:
        exclude:
            - 'node_modules/**'
            - 'tests/**'

NOTE: Remember to change the value of the region flag to match your IAM user’s region if it is different.

The configuration now uses the AWS credentials we created earlier. It also creates a unique instance of the service for each endpoint, that way, we can take advantage of Lambda’s monitoring to gain insights such as:

  • The number of times a route is invoked.
  • How long it takes for a route to respond.

We can then deploy the bref application from the project root directory with:

$ serverless deploy

The above command outputs your application URL (similar to https://XXXXXXX.execute-api.us-west-2.amazonaws.com/dev) in the endpoints section when it’s done. You can go ahead and test the endpoints again, but using the generated URL this time.

Conclusion

You have successfully deployed a Bref microservice that verifies users’ phone numbers via the Twilio Verify API. You can find the complete project on Github. You can further extend it by restricting access to the endpoints we implemented using mechanisms like AWS Lambda authorizers.

Michael Okoko is a software engineer and CS undergrad at Obafemi Awolowo University, Nigeria. He loves open source and is mostly interested in Linux, Golang, PHP, and fantasy novels! You can reach him via: