Securing a Laravel PHP Application with 2FA using Twilio Authy

December 05, 2019
Written by
Brian Iyoha
Contributor
Opinions expressed by Twilio contributors are their own

Securing a Laravel PHP Application with 2FA using Twilio Authy

Update January 2022

For new development, we encourage you to use the Verify API instead of the Authy API. The Verify API is an evolution of the Authy API with continued support for SMS, voice, and email one-time passcodes, an improved developer experience and new features including:

This blog post uses the Authy API. Any new features and development will be on the Verify API. Check out the FAQ for more information and Verify API Reference to get started.

In this tutorial, you will learn how to secure your Laravel application with Two-factor authentication using Twilio Authy.

Prerequisites

Completing this tutorial will require the following:

Getting Started

Create a new Laravel project using the Laravel Installer. If you don’t have it installed or prefer to use Composer, you can check out how to do so from the Laravel documentation. Run the following command in your terminal to generate a fresh Laravel project:

$ laravel new twilio-authy

Next, you will need to set up a database for the application. For this tutorial, we will make use of MySQL database. If you make use of a database administrator like phpMyAdmin for managing your databases then go ahead and create a database named twilio-authy and skip this section. If not, install MySQL from the official site for your platform of choice. After successful installation, fire up your terminal and run this command to login to MySQL:

$ mysql -u {your_user_name}

NOTE: Add the -p flag if you have a password for your MySQL instance.

Once you are logged in, run the following command to create a new database:

mysql> create database twilio-authy;
mysql> exit;

Next, update your .env file with your database credentials. Open up .env and make the following adjustments:

DB_DATABASE=twilio-authy
DB_USERNAME={your_user_name}
DB_PASSWORD={password if any}

Setting up Authy

Next, install the Twilio Authy SDK which will be used for sending out Two-factor authentication (2FA) one time passwords:

$ composer require authy/php

To make use of the Authy SDK you will need to create an Authy service. Head over to your Twilio console to create a new Authy service.

Twilio Authy Dashboard

Copy your PRODUCTION API KEY from the settings page of your service:

New Authy Application

Finally, update your .env file with the Authy secret:

AUTHY_SECRET={your_authy_api_key}

Building Authentication Logic

Out-of-the-box, Laravel allows us to easily scaffold a basic authentication system for both registering and logging in to your application.

We will be making use of the Laravel auth command to scaffold our basic authentication logic. Adjustments will be made later to add two-factor authentication(2fa) to our authentication process. Fire up a terminal in the project directory and run the following commands to scaffold a basic authentication system:

$ composer require laravel/ui --dev
$ php artisan ui vue --auth

The above command will create the login, registration, and home views, as well as routes for all authentication.

Updating the User Model

With the authentication system scaffolded, let’s begin making adjustments as needed. We need to update the Users migration to include new fields for storing phone_number and authy_id. Open up database/migrations/2014_10_12_000000_create_users_table.php and make the following changes to the up() method:

/**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->string('phone_number');
            $table->string('authy_id');
            $table->rememberToken();
            $table->timestamps();
        });
    }

Next, update the $fillable property of the User model. Open up app/User.php and make the following adjustment to the $fillable array:

/**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 'phone_number', 'authy_id',
    ];

Now run the following command to execute the migration:

$ php artisan migrate

This command will create a users table in your database with the fields in the up() method of the User’s migration file.

Adding 2FA to Authentication System

At this point, you should have the basic Laravel auth scaffolded. Now let’s make the needed adjustment to add Authy 2FA to our application. First, you will make an adjustment to the create() method which is called after successful validation of the data retrieved from the form. Open up the RegisterController (app/Http/Controllers/Auth/RegisterController.php) and update the create() method as follows:

/**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return \App\User
     */
    protected function create(array $data)
    {
        $authy_api = new AuthyApi(getenv("AUTHY_SECRET"));
        $authy_user = $authy_api->registerUser($data['email'], $data['phone_number'], $data['country_code']);
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
            'phone_number' => $data['phone_number'],
            'authy_id' => $authy_user->id(),
        ]);
    }

Let’s break down what is happening here. First you have to initialize the Authy SDK using your AUTHY_SECRET stored in your .env.To make use of Authy in your application a user must first be registered with the Authy service. This is done using the inbuilt registerUser() method of the Authy SDK:

$authy_user = $authy_api->registerUser($data['email'], $data['phone_number'], $data['country_code']);

The registerUser() method accepts three arguments for the user, email, phone number, and country code. Successful registration will return an id to be used for verifying the user’s identity.

NOTE:

  • The id returned after registering a user with an Authy service must be stored as part of your user data as this is the only way to identify this user within the Authy service.
  • You need to update the validation rules in the validator() method to include the phone_number and country_code fields.

After successful registration of the user with your Authy service, the user data along with the authy_id is stored in your Users table.

Be sure to import the AuthyApi class after the namespace in RegisterController.php.

use Authy\AuthyApi;

Sending 2FA OTP

To send out the 2FA OTP to a user, you will have to make changes to what happens after a user has been successfully authenticated. Since we are using the Laravel Auth scaffold we won’t be writing out the entire Login logic, instead we will make changes to the authenticated() method of the AuthenticatesUsers trait. To make the needed adjustments, open up app/Http/Controllers/Auth/LoginController.php and add the following method:

/**
 * The user has been authenticated.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  mixed  $user
 * @return mixed
 */
    protected function authenticated(Request $request, $user)
    {
        $authy_api = new AuthyApi(getenv("AUTHY_SECRET"));
        $authy_api->requestSms($user->authy_id);
        \session(['isVerified' => false]);
        return \redirect('verify');
    }

NOTE: The authenticated() method in the LoginController will override the default Trait’s method because it makes use of the AuthenticatesUsers trait.

Just like before, initialize the Authy SDK with your AUTHY_SECRET. Next, request that an SMS OTP should be sent to the user using the requestSms() method from the Authy SDK. The requestSms() method takes in an argument of the user’s authy_id (which we got earlier after successful registration of the user) and sends the user of the id an SMS with an OTP code. This code will later be verified before granting the user full access to the dashboard.

Next, we set a session variable (isVerified) to false which is used to indicate if the user has been verified using the OTP sent to them via SMS. After successfully sending out the OTP and resetting the isVerified flag, you can then redirect the user to a page where he/she will be asked to input the OTP sent to them.

NOTE: Apart from the requestSMS() method, Twilio Authy SDK also supports other channels for sending the OTP to a user.

Be sure to import the AuthyApi class after the namespace in LoginController.php.

use Authy\AuthyApi;

Verifying 2FA OTP

Next, let’s write out the logic for verifying a user’s OTP code. First generate a controller that will house the logic for verification. Open a terminal and run the following:

$ php artisan make:controller VerifyController

Now, open the just created file (app/Http/Controllers/VerifyController.php) and make the following adjustments:

<?php
namespace App\Http\Controllers;
use Authy\AuthyApi;
use Illuminate\Http\Request;
class VerifyController extends Controller
{
    public function index()
    {
        return view('auth.verify');
    }
    public function verify(Request $request)
    {
        try {
            $data = $request->validate([
                'verification_code' => ['required', 'numeric'],
            ]);
            $authy_api = new AuthyApi(getenv("AUTHY_SECRET"));
            $res = $authy_api->verifyToken(auth()->user()->authy_id, $data['verification_code']);
            if ($res->bodyvar("success")) {
                \session(['isVerified' => true]);
                return redirect()->route('home');
            }
            return back()->with(['error' => $res->errors()->message]);
        } catch (\Throwable $th) {
            return back()->with(['error' => $th->getMessage()]);
        }
    }
}

Taking a look at the verify() method you need to ensure that the data coming from the form is valid and has the verification_code property before proceeding to initialize the Twilio Authy SDK. To verify the OTP sent from the form data, make use of the verifyToken() method. This method takes in two arguments, the user authy_id and the OTP. Using the auth() helper function, you can retrieve the authy_id from the authenticated user’s model:

$authy_api->verifyToken(auth()->user()->authy_id, $data['verification_code']);

Next, check that the request was successful before setting the isVerified flag to true and redirecting the user to the home(dashboard) page:

if ($res->bodyvar("success")) {
                \session(['isVerified' => true]);
                return redirect()->route('home');
            }

If the request isn’t successful, the user is redirected back to the previous page with an error message.

Updating the Authenticate Middleware

So far, you have been able to add 2FA to your Laravel application, but you still need to ensure unverified users do not have access to protected pages. Fortunately, Laravel supports middleware and also has a default Authenticate middleware which is used to guard protected pages in our applications. You need to make changes to the default authenticate middleware to ensure a user OTP has been validated before granting access to protected pages. To do so, you will have to make adjustments to two middleware files; Authenticate.php (ensures a user is authenticated to access a protected route) and RedirectIfAuthenticated.php (redirects a user if authenticated and accessing a guest route). Open up app/Http/Middleware/Authenticate.php and make the following changes:

<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string[]  ...$guards
     * @return mixed
     *
     * @throws \Illuminate\Auth\AuthenticationException
     */
    public function handle($request, $next, ...$guards)
    {
        $this->authenticate($request, $guards);
        if (session("isVerified")) {
            return $next($request);
        }
        return \redirect('verify');
    }
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string
     */
    protected function redirectTo($request)
    {
        if (!$request->expectsJson()) {
            return route('login');
        }
    }
}

The important bit here is in the handle() method where you will check if the user is authenticated and isVerified is true before allowing the user to proceed with the request. Else, it redirects them to the verify page to get verified. Open up app/Http/Middleware/RedirectIfAuthenticated.php and make the following changes:

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->check()) {
            if (\session('isVerified')) {
                return redirect('/home');
            }
            return redirect('/verify');
        }
        return $next($request);
    }
}

Updating the views

At this point, you have added 2FA to your application authentication logic. Now, you will need to build out the views which users will use for interacting with your application. Fortunately, Laravel also scaffolds the basic views needed for registering and logging in to the application when the php artisan ui vue --auth command is used. Although the registration view has been scaffolded, you still need to make changes to it to include the fields for getting the user’s phone number and country code. Open up resources/views/auth/register.blade.php and replace its content with the code below to add the needed fields:

@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Register') }}</div>
                <div class="card-body">
                    <form method="POST" action="{{ route('register') }}">
                        @csrf
                        <div class="form-group row">
                            <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>
                            <div class="col-md-6">
                                <input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>
                                @error('name')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email">
                                @error('email')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="phone_number" class="col-md-4 col-form-label text-md-right">{{ __('Phone Number') }}</label>
                            <div class="col-md-6">
                                <input id="phone_number" type="tel" class="form-control @error('phone_number') is-invalid @enderror" name="phone_number" value="{{ old('phone_number') }}" required>
                                @error('phone_number')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="country_code" class="col-md-4 col-form-label text-md-right">{{ __('Country Code') }}</label>
                            <div class="col-md-6">
                                <input id="country_code" type="tel" class="form-control @error('country_code') is-invalid @enderror" name="country_code" value="{{ old('country_code') }}" required>
                                @error('country_code')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">
                                @error('password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
                            </div>
                        </div>
                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Register') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Next, create a new file resources/views/auth/verify.blade.php which will present the user with a form to input the OTP sent to them via SMS. Now open up resources/views/auth/verify.blade.php and add the following content:

@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Verify Your Phone Number') }}</div>
                <div class="card-body">
                    @if (session('error'))
                    <div class="alert alert-danger" role="alert">
                        {{session('error')}}
                    </div>
                    @endif
                    Please enter the OTP sent to your number: {{session('phone_number')}}
                    <form action="{{route('verify')}}" method="post">
                        @csrf
                        <div class="form-group row">
                            <label for="verification_code"
                                class="col-md-4 col-form-label text-md-right">{{ __('OTP: ') }}</label>
                            <div class="col-md-6">
                                <input id="verification_code" type="tel"
                                    class="form-control @error('verification_code') is-invalid @enderror"
                                    name="verification_code" value="{{ old('verification_code') }}" required>
                                @error('verification_code')
                                <span class="invalid-feedback" role="alert">
                                    <strong>{{ $message }}</strong>
                                </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Verify') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Updating The Application Routes

Awesome! Now that the views have been updated, proceed to add the appropriate routes for the application. Open up routes/web.php and make the following changes:

<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
 */
Route::get('/', function () {
    return view('welcome');
});
Route::get('/verify', 'VerifyController@index')->name('verify');
Route::post('/verify', 'VerifyController@verify')->name('verify');
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');

Testing Our Application

Now that you are done with building the application, let’s test it out. Open up your terminal and navigate to the project directory and run the following command:

$ php artisan serve

This will serve your Laravel application on a localhost port, normally 8000. Open up the localhost link printed out after running the command on your browser and you should be greeted with the default Laravel landing page with links to both the register and login page on the top right section of the header. You can proceed to register a user. If everything was coded correctly you will receive a OTP to verify your user’s session.

Conclusion

Awesome! Now that you have completed this tutorial, you have learned how to make use of Twilio's Authy Service for securing your Laravel application with Two-factor authentication. Subsequently, you learned how to modify the default Laravel authentication system and work with traits in a Laravel application. If you would like to take a look at the complete source code for this tutorial, you can find it on Github.

I’d love to answer any question(s) you might have concerning this tutorial. You can reach me via: