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/verify-v2/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
- 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):
- SMS -> SMS -> SMS
- SMS -> Voice Call -> SMS
- SMS -> WhatsApp
- 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:
- Send a verification code to the user via SMS
- Allow the user to enter the code into the application.
- 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 theusers
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.
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:
1composer 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:
1VONAGE_KEY={vonage-key-goes-here}2VONAGE_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:
app/Services/TwoFactorAuthService.php
1declare(strict_types=1);23namespace App\Services;45use App\Models\User;6use Exception;7use Vonage\Client;8use Vonage\Verify2\Request\SMSRequest;9use Vonage\Verify2\VerifyObjects\VerificationWorkflow;1011final class TwoFactorAuthService12{13 public function sendVerification(User $user): string14 {15 $smsRequest = (new SMSRequest(16 to: $user->phone_number,17 brand: 'Vonage Verify',18 ));1920 $voiceWorkflow = new VerificationWorkflow(21 channel: VerificationWorkflow::WORKFLOW_VOICE,22 to: $user->phone_number,23 );2425 $smsRequest->addWorkflow($voiceWorkflow);2627 $result = app(Client::class)28 ->verify2()29 ->startVerification($smsRequest);3031 return $result['request_id'];32 }3334 public function verify(string $code, string $requestId): bool35 {36 try {37 return app(Client::class)38 ->verify2()39 ->check($requestId, $code);40 } catch (Exception) {41 return false;42 }43 }44}
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:
1php 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:
app/Http/Middleware/VerifyTwoFactorAuth.php
1declare(strict_types=1);23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89final class VerifyTwoFactorAuth10{11 public function handle(Request $request, Closure $next): Response12 {13 if ($this->shouldRedirectToTwoFactorAuthPage($request)) {14 return redirect()->route('auth.2fa.show');15 }1617 return $next($request);18 }1920 private function shouldRedirectToTwoFactorAuthPage(Request $request): bool21 {22 return !$request->session()->has('two_factor_auth_verified')23 && $request->route()->getName() !== 'auth.2fa.show'24 && $request->route()->getName() !== 'auth.2fa.verify';25 }26}
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:
routes/web.php
1use App\Http\Controllers\ProfileController;2use App\Http\Middleware\VerifyTwoFactorAuth;3use Illuminate\Support\Facades\Route;45Route::middleware(['auth', VerifyTwoFactorAuth::class])->group(function () {6 Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');7 Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');8 Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');9});
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:
1php 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:
app/Http/Middleware/PreventRequestsIfTwoFactorAuthVerified.php
1declare(strict_types=1);23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Symfony\Component\HttpFoundation\Response;89final class PreventRequestsIfTwoFactorAuthVerified10{11 public function handle(Request $request, Closure $next): Response12 {13 // If we have already verified the user's 2FA code, redirect them to the dashboard.14 // We don't want to keep allowing them to access the 2FA verification page.15 if ($request->session()->get('two_factor_auth_verified')) {16 return redirect()->route('dashboard');17 }1819 return $next($request);20 }21}
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:
routes/web.php
1use App\Http\Controllers\Auth\TwoFactorAuthController;2use App\Http\Middleware\PreventRequestsIfTwoFactorAuthVerified;3use App\Http\Middleware\VerifyTwoFactorAuth;4use Illuminate\Support\Facades\Route;56Route::middleware(['auth', VerifyTwoFactorAuth::class])->group(function () {78 // ...910 Route::controller(TwoFactorAuthController::class)->middleware(PreventRequestsIfTwoFactorAuthVerified::class)->group(function () {11 Route::get('/auth/2fa', 'show')->name('auth.2fa.show');12 Route::post('/auth/2fa', 'verify')->name('auth.2fa.verify');13 });14});
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:
1php 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:
app/Http/Controllers/Auth/TwoFactorAuthController.php
1declare(strict_types=1);23namespace App\Http\Controllers\Auth;45use App\Http\Controllers\Controller;6use App\Services\TwoFactorAuthService;7use Illuminate\Contracts\View\View;8use Illuminate\Http\RedirectResponse;9use Illuminate\Http\Request;1011final class TwoFactorAuthController extends Controller12{13 /**14 * Show the two-factor authentication page.15 */16 public function show(Request $request, TwoFactorAuthService $twoFactorAuthService): View17 {18 // If the user hasn't already started the verification process, start it.19 // We check this here to prevent the user re-triggering the 2FA process20 // if the page is refreshed.21 if (! $request->session()->has('two_factor_auth_request_id')) {22 $requestId = $twoFactorAuthService->sendVerification($request->user());2324 $request->session()->put('two_factor_auth_request_id', $requestId);25 }2627 return view('auth.two-factor-auth');28 }2930 /**31 * Verify the two-factor authentication code entered by the user.32 */33 public function verify(Request $request, TwoFactorAuthService $twoFactorAuthService): RedirectResponse34 {35 $validated = $request->validate([36 'two_factor_auth_code' => ['required', 'numeric'],37 ]);3839 $isValidCode = $twoFactorAuthService->verify(40 code: $validated['two_factor_auth_code'],41 requestId: $request->session()->get('two_factor_auth_request_id')42 );4344 if (! $isValidCode) {45 return back()->withErrors([46 'two_factor_auth_code' => 'The code you entered was invalid.',47 ]);48 }4950 $request->session()->put('two_factor_auth_verified', true);5152 return redirect()->route('dashboard');53 }54}
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:
1<form method="POST" action="{{ route('auth.2fa.verify') }}"> 2 @csrf 3 4 <!-- Two-factor Auth Code --> 5 <div> 6 <label for="two_factor_auth_code">Two Factor Auth Code</label> 7 <input id="two_factor_auth_code" type="text" name="two_factor_auth_code" required autofocus /> 8 9 @if ($errors->get('two_factor_auth_code'))10 <ul>11 @foreach ((array) $errors->get('two_factor_auth_code') as $error)12 <li>{{ $error }}</li>13 @endforeach14 </ul>15 @endif16 </div>17 18 <button type="submit">Verify</button>19</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.