Integrating Two-Factor Authentication in Laravel with Vonage
Published on May 11, 2023

Introduction

Two-factor authentication (2FA) is an important part of any application that requires a user to log in. It adds an extra layer of security which can help prevent unauthorised access and protect your users' data.

In this article, we're going to take a look at how we can add 2FA to our Laravel application using the Vonage Verify API. We'll also discuss the benefits of 2FA and how it can help give your users more confidence in your app.

What is 2FA?

2FA is a method of authentication that requires a user to provide two pieces of information (or factors) to verify their identity. These factors typically fall into one of the following categories:

  • Something you know - Things like a password or personal identification number (PIN).

  • Something you have - Things such as a token generated by a physical device (such as a PIN pad or phone) or a YubiKey.

  • Something you are - This could be biometric information such as a fingerprint or facial recognition.

  • Somewhere you are - This could be a location such as a specific IP address or GPS coordinates. You may have seen this in action when you've tried logging into online accounts from a different location than usual.

  • Something you do - This could be something such as a gesture or action that only you know (such as mouse movements on the page).

The most common forms of 2FA that you'll usually come across when signing in to a web application are "something you know" and "something you have". For example, you may be required to enter your email and password (something you know) and then enter a code that is sent to, or generated by, your phone (something you have).

For the remainder of this article, we're going to focus on the "something you know" and "something you have" categories. We'll be using the Vonage Verify API to implement the "something you have" part of the 2FA process.

Benefits of Using 2FA

Now that we have a better understanding of what 2FA is, let's take a look at the benefits it provides for both the user and the application's owner.

Reduce the Chance of Account Takeover

By providing the functionality for a user to enable 2FA for their account, it reduces the chance of their account being accessed by a malicious actor.

Lists containing passwords that were leaked in data breaches can be found online with relative ease. These lists can be used by malicious users to try and access accounts on other services that may be using the same password. For example, let's imagine a user that has an account on "Website A" and "Website B" and that both accounts use the same password. If "Website A" has a data breach and the user's password is leaked, a hacker may attempt to use the same password to access the user's account on "Website B". Without 2FA, the hacker would be able to access the user's account on "Website B" and potentially cause damage. This could be anything from changing the user's password, deleting the account completely, or even making purchases on the user's behalf.

A similar scenario could also happen if a user was using a weak password or one that was easy to guess.

Therefore, by enabling 2FA, the user would be able to prevent this from happening. If the malicious user was required to enter a code that was sent to their phone, they would also need to have access to the user's phone to access their account.

As a bonus to the application owner, this can also reduce the number of support requests that they receive from users who have had their accounts compromised.

Improve Trust and Confidence

As a result of implementing 2FA in your applications, it can show your users that you take their security seriously. This can help to build a level of trust and confidence in your app which may lead to more sales and conversions.

Reduce Fraudulent User Activity and Signups

If you use 2FA as part of a verification or sign-up process, it can help to reduce the amount of fraudulent activity that you receive.

For example, you may want to ask your users to provide a phone number when they register. You could then send a verification code to the user via SMS and ask them to enter it into your application to prove they have access to the given phone number. As a result of this, it could help to reduce the number of fake accounts that are created.

However, it's important to remember that this doesn't completely eliminate the risk of fake accounts being created. It just puts a roadblock in place to make it harder for malicious users to create fake accounts. If a bot was set up to create accounts, a malicious actor could purchase a bank of phone numbers and then use them to create accounts.

What is the Vonage Verify API?

Vonage is a communications platform that provides a range of APIs that you can use to help build your app. They offer APIs for sending and receiving SMS messages, sending WhatsApp messages, making phone calls, and more.

One of the APIs that they provide is the "Verify API". This API allows you to send a verification code to a user via SMS messages, phone calls, WhatsApp messages, and emails. It also provides the functionality to check that the code a user enters is valid.

Using the Verify API is a great way to add 2FA to your Laravel application because it removes the need for you to build some of the functionality yourself. For example, they provide the ability to build automated workflows (which we'll cover in more depth later) and handle things like fraud detection, automatic code expiration, and the actual sending of SMS messages, WhatsApp messages, emails, and phone calls.

For more information on the Verify API, you can check out the full documentation here: https://developer.vonage.com/en/verify/overview

Vonage Verify Flows

The Vonage Verify API provides several channels that you can use to send the 2FA verification codes to your users. At the time of writing this article, the following channels are available:

  • SMS

  • Voice Call

  • Email

  • WhatsApp

  • WhatsApp (Codeless)

The "WhatsApp (Codeless)" feature sends a "Yes" or "No" button to the user via WhatsApp. This allows the user to authenticate themselves without needing to remember or enter a code.

The Vonage Verify API allows you to build verification workflows. These allow you to build a set of channels that Vonage should attempt to send the verification code to. For example, you could build a workflow that sends the code via SMS and then if the user doesn't use the code within a given timeframe (such as 5 minutes), it could be sent via a phone call instead. This particular feature can be useful if the user doesn't have any network on their phone (and therefore can't receive the SMS message), but has internet access so they can receive emails and WhatsApp messages as a fallback.

Here are some examples of workflows you may want to use (assuming there is a 5-minute gap between each sending to the next channel):

  1. SMS -> SMS -> SMS

  2. SMS -> Voice Call -> SMS

  3. SMS -> WhatsApp

  4. Email

  5. Voice Call -> Voice Call -> Voice Call

Adding 2FA to your Laravel Application

Now that we have an understanding of what the Verify API is, let's take a look at how to use it to add 2FA to our Laravel applications.

Our flow will be as follows:

  1. Send a verification code to the user via SMS

  2. Allow the user to enter the code into the application.

  3. If the user has not entered the code within five minutes, send the code via a phone call instead.

We'll be making the following assumptions:

  • We will be interacting with a phone_number field that exists on the users table. The field will be a required field and contain the phone numbers of the users. So that we can focus solely on the Verify API flow, we won't be covering how to add this field to the database.

  • We will assume that 2FA is permanently enabled for every user in the application.

Creating Your Vonage Account

Before we get started with writing any code, we'll first need to get our API keys from Vonage. You can do this by signing up for a Vonage account if you don't already have one.

<sign-up>

You'll need to keep hold of your API key and API secret so that we can add them to our Laravel application.

Installing the Vonage SDK

Now that we have our API keys, we can install Vonage's Laravel package that we'll be using to interact with the Verify API. We can do this via Composer by running the following command in our project root:

composer require vonage/vonage-laravel

The package should now be installed. After doing this, we can add our API key and secret to our .env file:

VONAGE_KEY={vonage-key-goes-here} VONAGE_SECRET={vonage-secret-goes-here}

Creating the Service Class

Let's start by creating a new class that we will use to interact with the Verify API. We'll call this class TwoFactorAuthService and we'll store it in the app/Services directory.

We'll add two methods to this class:

  • sendVerification - This will be used to send the verification code to the user.

  • verify - This will be used to check that the code the user entered is correct.

The class may look something like this:

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use Exception;
use Vonage\Client;
use Vonage\Verify2\Request\SMSRequest;
use Vonage\Verify2\VerifyObjects\VerificationWorkflow;

final class TwoFactorAuthService
{
    public function sendVerification(User $user): string
    {
        $smsRequest = (new SMSRequest(
            to: $user->phone_number,
            brand: 'Vonage Verify',
        ));

        $voiceWorkflow = new VerificationWorkflow(
            channel: VerificationWorkflow::WORKFLOW_VOICE,
            to: $user->phone_number,
        );

        $smsRequest->addWorkflow($voiceWorkflow);

        $result = app(Client::class)
            ->verify2()
            ->startVerification($smsRequest);

        return $result['request_id'];
    }

    public function verify(string $code, string $requestId): bool
    {
        try {
            return app(Client::class)
                ->verify2()
                ->check($requestId, $code);
        } catch (Exception) {
            return false;
        }
    }
}

Let's break down what's happening in this class in a bit more depth.

The sendVerification method accepts a User model instance as an argument. This is the user that is attempting to authenticate using 2FA. In this method, we are creating a workflow that will send the verification code to the user via SMS and then a phone call (if the user doesn't use the code within a given timeframe). We are then using the Vonage SDK to send the verification code to the user. The startVerification method returns an array that contains a request_id field. This field will be used to identify the user's flow and check that the code the user entered is correct. We'll be using this ID inside a controller (which we'll cover further down), so we'll return the request_id from this method.

The verify method accepts a code parameter which is the code that the user entered when attempting to authenticate using 2FA. It also accepts a requestId parameter. This will typically be the request ID that was returned from the sendVerification method. If the code the user entered is correct, the check method will return true. If the code is incorrect, it will throw an exception, so we'll catch it and return false to deny the user access.

Creating the Middleware

Now that we have our class for building our workflow and interacting with the Verify API, we'll want to create two middleware classes:

  • VerifyTwoFactorAuth - This middleware will be used to protect our application's routes. If the user has not yet entered their 2FA code, they will be redirected to the 2FA verification page.

  • PreventRequestsIfTwoFactorAuthVerified - This middleware will prevent the user from being able to access the 2FA verification page if they've already entered their 2FA code.

Let's start by creating the VerifyTwoFactorAuth middleware. We'll do this by running the following command in our project root:

php artisan make:middleware VerifyTwoFactorAuth

You should now have a new VerifyTwoFactorAuth class in the app/Http/Middleware directory. Let's update this class and then we'll take a look at what's happening:

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

final class VerifyTwoFactorAuth
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($this->shouldRedirectToTwoFactorAuthPage($request)) {
            return redirect()->route('auth.2fa.show');
        }

        return $next($request);
    }

    private function shouldRedirectToTwoFactorAuthPage(Request $request): bool
    {
        return !$request->session()->has('two_factor_auth_verified')
            && $request->route()->getName() !== 'auth.2fa.show'
            && $request->route()->getName() !== 'auth.2fa.verify';
    }
}

In the handle method above, we're checking whether the user has been verified using 2FA. We do this by checking whether the two_factor_auth_verified key exists in the user's session. If it does, we'll allow the user to continue as expected. If not, we'll redirect the user to the 2FA verification page, unless they're already on that page or submitting the 2FA verification form.

We can now use this middleware to protect our application's routes. For example, our routes/web.php routes file may look something like so:

use App\Http\Controllers\ProfileController;
use App\Http\Middleware\VerifyTwoFactorAuth;
use Illuminate\Support\Facades\Route;

Route::middleware(['auth', VerifyTwoFactorAuth::class])->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

The three routes in the above example will now require the user to be authenticated and have verified their 2FA code before they can access them.

We'll also need to create a PreventRequestsIfTwoFactorAuthVerified middleware. This will be used to prevent the user from navigating to the 2FA verification screen if they're already verified. We'll create this middleware by running the following command:

php artisan make:middleware PreventRequestsIfTwoFactorAuthVerified

You should now have a new PreventRequestsIfTwoFactorAuthVerified class in the app/Http/Middleware directory. Let's update this class and then we'll take a look at what's happening:

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

final class PreventRequestsIfTwoFactorAuthVerified
{
    public function handle(Request $request, Closure $next): Response
    {
        // If we have already verified the user's 2FA code, redirect them to the dashboard.
        // We don't want to keep allowing them to access the 2FA verification page.
        if ($request->session()->get('two_factor_auth_verified')) {
            return redirect()->route('dashboard');
        }

        return $next($request);
    }
}

As we can see in the above class, we're checking whether the user has already verified their 2FA by checking for the existence of the two_factor_auth_verified key in the user's session. If that key exists, this means the user has already verified their 2FA code, so we'll redirect them to the dashboard so they can't access the 2FA verification page. If not, we'll allow the user to continue as expected.

That's it! We now have our middleware prepped and ready to use. We'll cover how to use this particular middleware in the next section when we create the 2FA verification controller and routes.

Creating the Controller and Routes

Now that we have our service class and middleware ready, it's time to glue it all together with a controller and some routes.

We'll start by creating two new routes for our 2FA verification process:

  • GET /auth/2fa - This route will be used to display the 2FA verification form.

  • POST /auth/2fa - This route will be used when the user submits the form to verify their 2FA code.

We'll do this by adding the following to our routes/web.php file:

use App\Http\Controllers\Auth\TwoFactorAuthController;
use App\Http\Middleware\PreventRequestsIfTwoFactorAuthVerified;
use App\Http\Middleware\VerifyTwoFactorAuth;
use Illuminate\Support\Facades\Route;

Route::middleware(['auth', VerifyTwoFactorAuth::class])->group(function () {
    
    // ...

    Route::controller(TwoFactorAuthController::class)->middleware(PreventRequestsIfTwoFactorAuthVerified::class)->group(function () {
        Route::get('/auth/2fa', 'show')->name('auth.2fa.show');
        Route::post('/auth/2fa', 'verify')->name('auth.2fa.verify');
    });
});

You may have noticed that we're using the PreventRequestsIfTwoFactorAuthVerified middleware (that we created earlier) on the TwoFactorAuthController group.

We can now create the TwoFactorAuthController (that we are referencing in these two routes) by running the following command:

php artisan make:controller Auth/TwoFactorAuthController

You should now have a new TwoFactorAuthController class in the app/Http/Controllers/Auth directory. Let's update this class and then we'll take a look at what's happening:

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Services\TwoFactorAuthService;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

final class TwoFactorAuthController extends Controller
{
    /**
     * Show the two-factor authentication page.
     */
    public function show(Request $request, TwoFactorAuthService $twoFactorAuthService): View
    {
        // If the user hasn't already started the verification process, start it.
        // We check this here to prevent the user re-triggering the 2FA process
        // if the page is refreshed.
        if (! $request->session()->has('two_factor_auth_request_id')) {
            $requestId = $twoFactorAuthService->sendVerification($request->user());

            $request->session()->put('two_factor_auth_request_id', $requestId);
        }

        return view('auth.two-factor-auth');
    }

    /**
     * Verify the two-factor authentication code entered by the user.
     */
    public function verify(Request $request, TwoFactorAuthService $twoFactorAuthService): RedirectResponse
    {
        $validated = $request->validate([
            'two_factor_auth_code' => ['required', 'numeric'],
        ]);

        $isValidCode = $twoFactorAuthService->verify(
            code: $validated['two_factor_auth_code'],
            requestId: $request->session()->get('two_factor_auth_request_id')
        );

        if (! $isValidCode) {
            return back()->withErrors([
                'two_factor_auth_code' => 'The code you entered was invalid.',
            ]);
        }

        $request->session()->put('two_factor_auth_verified', true);

        return redirect()->route('dashboard');
    }
}

In the show method, we're checking whether the user has already started the 2FA verification process. We do this by checking for the existence of a two_factor_auth_request_id field in the user's session. This field will hold the request ID that is returned from the sendVerification method in the TwoFactorAuthService. If the field doesn't exist, we'll start the verification process (sending the code via SMS and phone call) by calling the sendVerification method and storing the request ID in the session. If the user has already started the verification process, we'll continue as normal to the 2FA verification form. This will handle if the user refreshes the page or navigates away from the page and then back to it.

In the verify method, we start by performing some basic validation to ensure that a two_factor_auth_code field has been passed in the request. We then use the code entered by the user and the two_factor_auth_request_id field in the user's session to verify the code using the verify method in the TwoFactorAuthService. This will send a request to the Verify API to ensure the code is valid. If the code is valid, we'll store a two_factor_auth_verified field in the user's session to indicate that the user has verified their 2FA code. This will prevent the user from being redirected to the 2FA verification form for each subsequent request. If the code is invalid, we'll redirect the user back to the 2FA verification form with an error message.

Creating the Form

In the show method of the TwoFactorAuthController, we are returning an auth.two-factor-auth view to the user. This view will contain the form that the user will use to enter their 2FA code.

For the purpose of this guide, we'll create a simple form that doesn't contain any styling. The Blade view may look something like so:

<form method="POST" action="{{ route('auth.2fa.verify') }}">
    @csrf

    <!-- Two-factor Auth Code -->
    <div>
        <label for="two_factor_auth_code">Two Factor Auth Code')</label>
        <input id="two_factor_auth_code" type="text" name="two_factor_auth_code" required="" autofocus="">

        @if ($errors->get('two_factor_auth_code'))
            <ul>
                @foreach ((array) $errors->get('two_factor_auth_code') as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        @endif
    </div>
    
    <button type="submit">Verify</button>
</form>

As we can see in the Blade view above, we have added a single two_factor_auth_code text input field, label, and submit button. We've also added a condition and loop that will output any error messages we may want to display to the user (such as when they enter an invalid code).

That's it! You should now have a fully functioning 2FA implementation in your Laravel application!

Taking It Further

Now that we have an understanding of how to implement 2FA in our Laravel applications, let's take a look at how we could take this feature further to provide more value.

Adding a Toggle to Enable/Disable 2FA

In this guide, we've assumed that all users will have 2FA enabled and will be required to enter a code every time they log in. However, you may want to give your users the option to enable and disable 2FA on their accounts so they can choose whether they want to use it. Although, if you do provide this functionality, you may want to consider adding a notice or warning to your user interface to make your users aware of the security implications of disabling 2FA.

As well as this, you may also want to provide the ability for users to choose the channels the 2FA codes are sent on. For example, they may want to receive the codes via SMS and voice call. Or, they may only want them to be received via email and WhatsApp.

Providing flexibility like this will allow you users to build up an authentication workflow that works well for them, and that finds a balance between security and convenience.

Adding Friendly Forwarding

In our examples above, we've hardcoded the user to always be redirected to the dashboard after they have successfully verified their 2FA code. However, you may want to provide a "friendly forwarding" feature that redirects the user to the page they were originally trying to access.

For example, if the user was originally trying to navigate directly to "https://my-app.com/orders", you could implement friendly forwarding so they are redirected to that page after they have successfully verified their 2FA code.

Re-verifying 2FA Periodically

In this guide, we've only implemented 2FA for the login process. However, depending on the application you're building, you may want to periodically re-verify the user's 2FA code. For example, you may want the user to enter a new code every hour. This can help to protect the user's account if their device is compromised or if they accidentally stay logged in on a public computer (such as in a library or cafe).

Furthermore, you may also want to ask the user for their password or 2FA code before they can perform some actions. For instance, you may want to do this before allowing a user to delete their account or change their email address. This will help to prevent malicious users from performing potentially destructive actions if they get access to an account.

Provide a Backup Option

If you add a 2FA feature to your application, you may also want to provide a backup option for users to get access to their accounts in an emergency. As an example, a user may lose their phone or have it stolen. As a result of this, they may not be able to gain access to their account anymore.

For this reason, a common backup option is to allow users to download a list of recovery codes that are generated when they enable 2FA for their account. These recovery codes are typically single-use and can be used to gain access to the user's account without having to enter a 2FA code. If you do choose to generate these codes, you must remember to make them cryptographically secure so they can't be easily guessed or brute-forced.

Conclusion

Hopefully, this guide should have given you an insight into how to use the Vonage Verify API to implement 2FA in your Laravel applications. It should have also highlighted some of the benefits of using 2FA and how it can help you and your users.

Ashley AllenGuest Author

Ashley is a freelance Laravel web developer who loves contributing to open-source projects, building exciting systems, and helping others learn about web development.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.