How to Implement Account Verification and Login by Phone in Laravel

April 12, 2019
Written by
Victor Abbah
Contributor
Opinions expressed by Twilio contributors are their own

Implement Account Verification and Login by Phone in Laravel.png

At times, you might want to create an app that uses a phone number/password pair as a means of authentication, as opposed to the normal email/password pair. In some other cases, you are not necessarily using phone numbers as a means of authentication, but having a phone number is critical to your app. In such situations, it is very important you verify that the phone numbers your users provide are valid and functional. One way to do this is to give them a call and tell them a code that they will have to provide to your app. If you use Gmail, then you are probably familiar with the voice call verification it uses. In this article, I will be showing you how to achieve that using Laravel and Twilio’s excellent service. Let’s get to it.

Technical Requirements

For this tutorial we’ll assume the following:

  • You use Laravel valet (You can use any development environment though).
  • You have MySQL installed on your machine.
  • You have a global installation of Composer.
  • You have Ngrok set up (You won’t use this if you are using Valet).
  • You have a Twilio account.
  • You have a verified number (If you are in trial mode).
  • You have a voice-enabled Twilio phone number

Create and Setup a New Laravel App

We are going to create a fresh installation of Laravel in our valet "parked" directory. We do this by running the following command:

$ laravel new twilio

This is assuming we have already installed the Laravel installer. If you don't have that, you can check the Laravel official documentation for direction on that. Alternatively, you can install a fresh Laravel app using Composer. Run the following command in your terminal window:

$ composer create-project --prefer-dist laravel/laravel twilio

Next up, we need to create a .env file in which we can store all the configurations our app needs. Run the following command in your terminal:

$ cp .env.example .env

This command creates the .env using the .env.example as a template. Open the .env file and add the database credentials for database name, username and password.

Create a database and set the credentials of that database in the just created .env file.

If you completed all of these steps correctly, you should see your new app in the browser by running valet link from your terminal. In my case, I named the folder "twilio" and specified a .devs extension, so my URL is twilio.devs.

Set Up the Authentication Routes and View

After setting up the database credentials, we can now set up authentication using the artisan command. Laravel provides quick scaffolding of all the routes and views we need for authentication using this simple command. With this one command, we can install a layout view, registration and login views, as well as routes for all authentication endpoints!

$ php artisan make:auth

After running this command, run the migration.

$ php artisan migrate

And when you check your database, you should see the tables created by the make:auth artisan command. If you refresh the app, you will also notice some authentication links automatically added to the interface now.

 

Set Up Twilio

The next thing we are going to do is to add our Twilio credentials to the .env file. Visit your Twilio dashboard and copy your Account SID and Auth Token.

TWILIO_SID=<twilio_sid>
TWILIO_AUTH_TOKEN=<twilio_auth_token>

Install Twilio’s SDK Using Composer

Next up, we install The Twilio PHP Helper Library. This will help us talk to Twilio's API from our code. Run the following command in your terminal:

$ composer require twilio/sdk

Tying Everything Together

This is the point where we start doing the actual work of making sure our app calls the user upon registration, and to make sure the user can also login with his phone number. We would not want our users to be unable to log in after registration.

Modifying our User Model

Now that we have everything set up, we need to start tweaking our app to accept a phone number upon registration instead of email. First, we will change the users table migration file generated for us, when we ran the php artisan make:auth command like so:

        public function up()
        {
            Schema::create('users', function (Blueprint $table) {
                $table->increments('id');
                $table->string('name');
                $table->string('phone')->unique();
                $table->string('verification_code')->unique()->nullable();
                $table->timestamp('phone_verified_at')->nullable();
                $table->string('password');
                $table->rememberToken();
                $table->timestamps();
            });
        }

The above code changes the email field to phone. It also adds a verification_code field to the user table that stores the code that will be told to the users when they are called. a phone_verified_at field is also added to the user's table to store when the user's phone number is verified.

We also need to make some changes to the app/User.php model. The following code ensures that the phone, name, and password fields are mass assignable. You can read more about mass assignment in the Laravel documentation:

        protected $fillable = [
            'name', 'phone', 'password',
        ];

        protected $casts = [
            'phone_verified_at' => 'datetime',
        ];

Still, in app/User.php file, let's add a couple of methods to help us in the verification process. Add the following methods:

        public function hasVerifiedPhone()
        {
            return ! is_null($this->phone_verified_at);
        }

        public function markPhoneAsVerified()
        {
            return $this->forceFill([
                'phone_verified_at' => $this->freshTimestamp(),
            ])->save();
        }

The hasVerifiedPhone method just checks to see if the phone number of the user is verified. The markPhoneAsVerified verifies the phone number of the user by setting the phone_verified_at field of the user to the current timestamp.

Modifying our Authentication Controllers

Like I mentioned earlier, we would not want a situation where a user registers but is unable to log in simply because our app expects an email for authentication. Let's make sure that does not happen. Open app/Http/Controllers/Auth/LoginController.php and add the following method:

        public function username()
        {
            return 'phone';
        }

This method returns the login username to be used by the controller. It is basically saying "This is the field in the database you should look up as the username when authenticating a user".

Next, we edit app/Http/Controllers/Auth/RegisterController.php and change the validator and  create methods like so:

        protected function validator(array $data)
        {
            return Validator::make($data, [
                'name' => ['required', 'string', 'max:255'],
                'phone' => ['required', 'string', 'unique:users'],
                'password' => ['required', 'string', 'min:6', 'confirmed'],
            ]);
        }

        protected function create(array $data)
        {
            return User::create([
                'name' => $data['name'],
                'phone' => $data['phone'],
                'password' => Hash::make($data['password']),
            ]);
        }

The above methods just make sure we validate and store the user input properly during registration. Nothing fancy here.

Modifying our Authentication Views

Now we need to start working on what our users will actually interact with: The authentication views. We need to edit the registration and login views to expect phone numbers instead of email. Open the resources/views/auth/register.blade.php and edit the portion asking for email to the following:

        <div class="form-group row">
            <label for="phone" class="col-md-4 col-form-label text-md-right">Phone</label>

            <div class="col-md-6">
                <input id="phone" type="text" class="form-control{{ $errors->has('phone') ? ' is-invalid' : '' }}" name="phone" value="{{ old('phone') }}" required>

                @if ($errors->has('phone'))
                    <span class="invalid-feedback" role="alert">
                        <strong>{{ $errors->first('phone') }}</strong>
                    </span>
                @endif
            </div>
        </div>

Also, open the resources/views/auth/login.blade.php and edit the portion asking for email to the following:

        <div class="form-group row">
            <label for="phone" class="col-md-4 col-form-label text-md-right">Phone</label>

            <div class="col-md-6">
                <input id="phone" type="text" class="form-control{{ $errors->has('phone') ? ' is-invalid' : '' }}" name="phone" value="{{ old('phone') }}" required autofocus>

                @if ($errors->has('phone'))
                    <span class="invalid-feedback" role="alert">
                        <strong>{{ $errors->first('phone') }}</strong>
                    </span>
                @endif
            </div>
        </div>

We have successfully tweaked our app to accept phone numbers during authentication. Well done!

Middleware, Controller, and Route for Verification

At this point, we need to protect our routes from users with unverified accounts. We do so by creating a middleware that we apply to the routes we need to protect. We also need to create the necessary controller and routes for verification. Let's get to it.

Create a Middleware

Laravel makes it very easy to create a middleware. Just switch to your terminal and run the artisan command:

$ php artisan make:middleware EnsurePhoneIsVerified

Open the middleware and update the following code to the handle method:

        if (! $request->user()->hasVerifiedPhone()) {
            return redirect()->route('phoneverification.notice');
        }

        return $next($request);

Now we need to register the middleware so that  the application knows about it. Open the app/Http/Kernel.php and add this to the  $routeMiddleware array.

        'verifiedphone' => \App\Http\Middleware\EnsurePhoneIsVerified::class,

Lets apply the middleware to the home route. Open up routes/web.php and edit it like so:

Route::get('/home', 'HomeController@index')->name('home')->middleware('verifiedphone');

Now any user that visits the home route without verifying their phone number will be redirected to the route we specified in the middleware.

Add Necessary Routes

Open routes/web.php and add the following code it:

        Route::get('phone/verify', 'PhoneVerificationController@show')->name('phoneverification.notice');
        Route::post('phone/verify', 'PhoneVerificationController@verify')->name('phoneverification.verify');

As you can see from the above routes, we are routing traffic to a PhoneVerificationController that we don’t have yet. Let's quickly create that using artisan. Run the following command:

$ php artisan make:controller PhoneVerificationController

Open that controller and add the following code to it:

<?php
        public function show(Request $request)
    {
        return $request->user()->hasVerifiedPhone()
                        ? redirect()->route('home')
                        : view('verifyphone');
    }

    public function verify(Request $request)
    {
        if ($request->user()->verification_code !== $request->code) {
            throw ValidationException::withMessages([
                'code' => ['The code your provided is wrong. Please try again or request another call.'],
            ]);
        }

        if ($request->user()->hasVerifiedPhone()) {
            return redirect()->route('home');
        }

        $request->user()->markPhoneAsVerified();

        return redirect()->route('home')->with('status', 'Your phone was successfully verified!');
    }

In the show method, we are saying "If the user has a verified phone, simply redirect the user to the home route" else, return a view notifying them to provide a verification code. In the verify method, we are verifying a user. Notice how we use the hasVerifiedPhone method we created earlier in app/User.php. Let’s now create a view where users can provide their verification code.

Create a file at resources/views/verifyphone.blade.php and add the following code to it:

        @extends('layouts.app')
        @section('content')
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-md-8">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif
                    <div class="card">
                        <div class="card-header">Verify your phone</div>
                        <div class="card-body">
                            <p>Thanks for registering with our platform. We will call you to verify your phone number in a jiffy. Provide the code below.</p>

                            <div class="d-flex justify-content-center">
                                <div class="col-8">
                                    <form method="post" action="{{ route('phoneverification.verify') }}">
                                        @csrf
                                        <div class="form-group">
                                            <label for="code">Verification Code</label>
                                            <input id="code" class="form-control{{ $errors->has('code') ? ' is-invalid' : '' }}" name="code" type="text" placeholder="Verification Code" required autofocus>
                                            @if ($errors->has('code'))
                                                <span class="invalid-feedback" role="alert">
                                                    <strong>{{ $errors->first('code') }}</strong>
                                                </span>
                                            @endif
                                        </div>
                                        <div class="form-group">
                                            <button class="btn btn-primary">Verify Phone</button>
                                        </div>
                                    </form>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        @endsection

Calling the User

We have everything all set up. We now need to hook into the registration flow and call the user. Laravel provides a nice hook for us to do that. In app/Http/Controllers/Auth/RegisterController.php, after successful registration, Laravel calls a method registered. This means we can override this method to call the user after successful registration. Open app/Http/Controllers/Auth/RegisterController.php and add the method:

        protected function registered(Request $request, User $user)
        {
            $user->callToVerify();
            return redirect($this->redirectPath());
        }

In this method we call the callToVerify method on the user that we are yet to define. After that, we redirect the user to the home route. Let’s quickly define that method. Open app/User.php and add the following code:

<?php
        use Twilio\Rest\Client;
.
.

        public function callToVerify()
        {
            $code = random_int(100000, 999999);
            
            $this->forceFill([
                'verification_code' => $code
            ])->save();

            $client = new Client(env('TWILIO_SID'), env('TWILIO_AUTH_TOKEN'));

            $client->calls->create(
                $this->phone,
                "+15306658566", // REPLACE WITH YOUR TWILIO NUMBER
                ["url" => "http://your-ngrok-url>/build-twiml/{$code}"]
            );
        }

This is where we actually call the user and disclose the verification code. We first generate a random 6 digit code and store it to the user. After that, we create a client to help us talk to Twilio's API using the credentials we stored in .env earlier.

At this point, it is paramount to understand how Twilio's Programmable Voice API works. The create method accepts 3 parameters:

  • The phone number to be called. For us, this will be the user's phone number. If you are in trial mode, this has to be a verified phone number.
  • The second parameter is the phone number making the call. This has to be a voice-enabled Twilio phone number created in your console. If you don't currently own a Twilio phone number with Voice functionality, you'll need to purchase or create one for testing.
  • The 3rd parameter is the most interesting part of Twilio's programmable voice API to me. The 3rd parameter is a URL to an XML file that must return a valid TwiML (Twilio Markup Language) or a URL to your application that Twilio makes an HTTP request to, asking for instructions on how to handle the call. This has to be a live URL because Twilio is going to send an HTTP request back to it. This is where Ngrok comes to play. It will help us get a live URL to our application.

Let's define the route and a method on PhoneVerificationController.php. Open routes/web.php and edit it like this:

Route::post('build-twiml/{code}', 'PhoneVerificationController@buildTwiMl')->name('phoneverification.build');

Open PhoneVerificationController.php and edit it like so:

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Twilio\TwiML\VoiceResponse;
.
.

        public function buildTwiML($code)
        {
            $response = new VoiceResponse();
            $response->say("Hi, thanks for Joining. This is your verification code. {$code}. I repeat, {$code}.");
            echo $response;
        }

One important thing we need to do next is to disable CSRF checks on the route Twilio will be making an HTTP request to. This is very important. If we don't disable it, Laravel will not allow access and Twilio will not be able to know how to handle the call. To do that, let's edit app/Http/Middleware/VerifyCsrfToken.php like so:

        protected $except = [
            '/build-twiml/*'
        ];

At this point when we register a user, we will receive a call from Twilio. Everything works fine but I noticed something we need to fix. Twilio programmable voice tries to read the code out as a word instead of spelling out each letter. For example, if the code is qwerty, Twilio tries to pronounce it as a word, as expected. But that is not what we need. We need it to say each letter separately. That means we need to return it like q.w.e.r.t.y, this way, the code will not be read as a word. Edit PhoneVerificationController.php once again like so:

        public function buildTwiMl($code)
        {
            $code = $this->formatCode($code);
            $response = new VoiceResponse();
            $response->say("Hi, thanks for Joining. This is your verification code. {$code}. I repeat, {$code}.");
            echo $response;
        }

        public function formatCode($code)
        {
            $collection = collect(str_split($code));
            return $collection->reduce(
                function ($carry, $item) {
                    return "{$carry}. {$item}";
                }
            );
        }

Run the Migration

Because we updated the user's migration table, we need to update the columns to include the changes we made. Run the following command to refresh the tables:

$ php artisan migrate:refresh

And that is it!

Testing

To test,

  • Run valet share and copy the ngrok URL.
  • In the app/User.php at the callToVerify() method, replace "your-ngrok-url" with the URL you copied above.
  • Visit your website and register with a phone number.
  • Input the code you heard during the call in the view you are directed to after registration.

NOTE: When registering, Phone numbers should be formatted with a '+' and country code e.g., +16175551212 (E.164 format).

Conclusion

Believe it or not, This is all it takes to achieve phone verification using Twilio's Programmable voice, Twilio TwiML™ and Laravel. Awesome!

Written By:

Name: Victor Abbah Nkoms
Twitter: twitter.com/veekthoven
Email: veekthoven@gmail.com
Github: github.com/veekthoven