
Are you looking for a way to add two factor authentication (2FA )in your php or laravel application. Well in this post we will describe how to implement two factor verification to add more security in laravel apps.
Two factor verification is a mechanism that provides a second layer of security in which the user is promoted with some sort of a second step, typically a verification screen after login the login process where he/she enters a verification code then proceed to the app.
In this app we will depend on Google Authenticator App to scan a Qrcode for the website user and generate an OTP so we can use that OTP to verify the user.
I will suppose that you have a laravel application up and running, we will install a package to provide two factor verification using Google Authenticator mobile app.
First install the package pragmarx/google2fa
composer require pragmarx/google2fa
Also install this package for the Qrcode:
composer require bacon/bacon-qr-code
The second step create a migration that alters the users table to add the below fields:
two_factor_secret
two_factor_enable
two_factor_verified
php artisan make:migration alter_users_table_add_2fa_secret
Open the migration file and update the up()
method and down()
method like so:
public function up(): void { Schema::table('users', function (Blueprint $table) { $table->string('two_factor_secret')->nullable(); $table->tinyInteger('two_factor_enable')->default(0); $table->tinyInteger('two_factor_verified')->default(0); }); }
public function down(): void { Schema::table('users', function (Blueprint $table) { $table->dropColumn(['two_factor_secret', 'two_factor_enable', 'two_factor_verified']); }); }
Next run php artisan migrate
to execute this migration and create the above columns.
The two_factor_enable
column as the name implies enable or disable two factor verification for the logged in user. two_factor_secret
stores the secret key generated so that we use this key to compare it with the entered code by the user. two_factor_verified
refers that the logged in user is 2Fa verified or not, we will use it in the middleware below.
The process is to make a screen that enables the user to toggle the 2FA to be enabled or disabled. Once the user enabled the 2FA, then on subsequent logins he will be redirected to another screen where he enters the OTP code.Â
Two Factor Controller
Create new controller that encapsulates the actions for the 2FA logic:
php artisan make:controller TwoFactorController
app/Http/Controllers/TwoFactorController.php
<?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\Http\Request; class TwoFactorController extends Controller { public function show2FaForm() { // Show view of 2FA QRcode and form } public function enable2FA(Request $request) { // Enable 2FA } public function disable2FA(Request $request) { // Disable 2FA } public function verify2FAForm() { // Show verify 2FA form } public function verify2FA(Request $request) { // Verify 2FA } }
The TwoFactorController
class contains the actions needed to preform two factory authentication. The class contains the above methods for 2FA:
showsFaForm()
: Show the view that renders the Qrcode and a form to toggle 2FA.enable2FA():
Enables 2FA verification.disable2FA()
: Disabled 2FA verification.verify2FaForm()
: Show the view to enters the verification code after login.verify2FA()
: This action verifies the user and triggered from the verify2FaForm.
Next let’s registers the routes for these actions in the web.php:
web.php
Route::middleware(['auth'])->group(function () { Route::get('two-factor', [\App\Http\Controllers\TwoFactorController::class, 'show2FaForm'])->name('two-factor.form'); Route::post('two-factor/enable', [\App\Http\Controllers\TwoFactorController::class, 'enable2FA'])->name('two-factor.enable'); Route::post('two-factor/disable', [\App\Http\Controllers\TwoFactorController::class, 'disable2FA'])->name('two-factor.disable'); Route::get('two-factor/verify', [\App\Http\Controllers\TwoFactorController::class, 'verify2FaForm'])->name('two-factor.verify-form'); Route::post('two-factor/verify', [\App\Http\Controllers\TwoFactorController::class, 'verify2FA'])->name('two-factor.verify'); });
The routes wrapped in a route group within the auth middleware. Now let’s add the logic for each controller action.
Open the first action show2FaForm()
and add this code:
public function show2FaForm() { $issuer = "MyWebsite"; $accountName = auth()->user()->name . '@MyWebsite'; $google2fa = new Google2FA(); $secretKey = $google2fa->generateSecretKey(); // secret key $qrCodeUrl = $google2fa->getQRCodeUrl( $issuer, $accountName, $secretKey ); $writer = new Writer( new ImageRenderer( new RendererStyle(200), new ImagickImageBackEnd() ) ); $qrcodeImage = base64_encode($writer->writeString($qrCodeUrl)); return view('two-factor', compact('secretKey', 'qrcodeImage')); }
This method renders the two-factor
blade view and sends the $secretKey
and $qrcodeImage
variables to the view. To learn how to retrieve the secret key and qrcode image head over to the package we installed github page for the usage instructions.Â
I got a new instance from Google2FA class, then i invoked the generateSecretKey()
and getQRCodeUrl()
methods. The getQRCodeUrl()
method accepts the issuer, user accountName and a secret key:
$secretKey = $google2fa->generateSecretKey(); // secret key $qrCodeUrl = $google2fa->getQRCodeUrl( $issuer, $accountName, $secretKey );
To retrieve the qrcode image, i used BaconQrCode package i installed above like so:
$writer = new Writer( new ImageRenderer( new RendererStyle(200), new ImagickImageBackEnd() ) ); $qrcodeImage = base64_encode($writer->writeString($qrCodeUrl));
Don’t forget to import the needed classes at the top of the controller:
use BaconQrCode\Renderer\Image\ImagickImageBackEnd; use BaconQrCode\Renderer\ImageRenderer; use BaconQrCode\Renderer\RendererStyle\RendererStyle; use BaconQrCode\Writer; use PragmaRX\Google2FA\Google2FA;
Next open resources/views/two-factor.blade.php
view and add this code:
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-10"> <div class="card"> <div class="card-header"> @if(auth()->user()->two_factor_enable) {{ __('Disable two factor verification') }} @else {{ __('Enable two factor verification') }} @endif </div> <div class="card-body"> <div class="row"> @if (session('error')) <div class="alert alert-danger" role="alert"> {{ session('error') }} </div> @endif <div class="col-md-6"> <img src="data:image/png;base64, {{ $qrcodeImage }}" alt="QR Code"> <p> @lang('Use the QR code or secret key on your Google Authenticator app to add your account. ') </p> <div class="form-group"> <label for="secretKey">{{ __('Secret Key') }}</label> <input type="text" class="form-control" id="secretKey" value="{{ $secretKey }}" readonly> </div> </div> <div class="col-md-6"> <form method="post" action="{{ !auth()->user()->two_factor_enable ? route('two-factor.enable') : route('two-factor.disable')}}"> @csrf <input type="hidden" name="key" value="{{$secretKey}}"> <div class="form-group"> <label for="twoFactorCode">{{ __('Google Authenticatior OTP') }}</label> <input type="text" class="form-control" name="google_otp" required /> </div> <button type="submit" class="btn btn-primary mt-3">@lang('Submit')</button> </form> </div> </div> </div> </div> </div> </div> </div> @endsection
This view renders a screen similar to this figure:
This view show the qrcode to enable/disables the 2FA. To use it you must install an authenticator app like Google Authenticator on your android device, then from Google Authenticator add your account either by scanning the qrcode or adding the secret key.Â
Once you scanned the qrcode from the Google Authenticator, your account will be added in the authenticator app along with the 6 digit OTP code. To enable or disable the 2Fa enters the OTP code into the form.
In the form action we output the route for enabling/Disabling the 2Fa, for this i am checking for the flag two_factor_enable
:
<form method="post" action="{{ !auth()->user()->two_factor_enable ? route('two-factor.enable') : route('two-factor.disable')}}"> </form>
Inside the form there are two inputs, the hidden field with name=”key” contains the secret key and the other input for the user provided Google OTP.
Now update the controller methods enable2FA()
and disable2FA()
as shown:
public function enable2FA(Request $request) { $this->validate($request, [ 'key' => 'required', 'google_otp' => 'required', ]); $google2fa = new Google2FA(); try { $valid = $google2fa->verifyKey($request->key, $request->google_otp); if($valid) { $user = User::find(auth()->user()->id); $user->two_factor_secret = $request->key; $user->two_factor_enable = 1; $user->save(); return redirect()->route('home')->with('status', 'Two Factor Authentication Enabled Successfully'); } else { return redirect()->back()->with('error', 'Invalid OTP'); } } catch (\Throwable $exception) { return redirect()->back()->with('error', 'Unknown error'); } } public function disable2FA(Request $request) { $this->validate($request, [ 'key' => 'required', 'google_otp' => 'required', ]); $google2fa = new Google2FA(); try { $valid = $google2fa->verifyKey($request->key, $request->google_otp); if($valid) { $user = User::find(auth()->user()->id); $user->two_factor_secret = null; $user->two_factor_enable = 0; $user->save(); return redirect()->route('home')->with('status', 'Two Factor Authentication Deactivated Successfully'); } else { return redirect()->back()->with('error', 'Invalid OTP'); } } catch (\Throwable $exception) { return redirect()->back()->with('error', 'Unknown error'); } }
Both actions have similar code, first we validate the request for key and google_otp. Then by using Google2FA::verifyKey()
method which accepts the secret key and the otp we check if this is valid.
If the verify key is valid we store the secret key in the user two_factor_secret
column and set two_factor_enable=1
and save the user.
In the disable2FA()
method the same is done, but if the verify key is valid in this case we reset the two_factor_secret
column to null and set two_factor_enable=0
Add the code for the other controller actions verify2FaForm()
and verify2FA()
:
public function verify2FaForm() { return view('two-factor-verify'); } public function verify2FA(Request $request) { $request->validate([ 'code' => 'required', ]); $code = $request->code; $user = User::find(auth()->user()->id); $google2fa = new Google2FA(); $secret = $user->two_factor_secret; $currOtp = $google2fa->getCurrentOtp($secret); $userCode = $code; if ($currOtp == $userCode) { $user->two_factor_verified = 1; $user->save(); return redirect('/home')->with('status', 'Verification successful'); } else { return back()->with('error', 'Wrong verification code'); } }
The verify2FaForm()
action return the blade view ‘two-factor-verify’ which contains the form to enter the otp by the user:
resources/views/two-factor-verify.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-10"> <div class="card"> <div class="card-header"> 2FA Verification </div> <div class="card-body"> <div class="row"> @if (session('error')) <div class="alert alert-danger" role="alert"> {{ session('error') }} </div> @endif <div class="col-md-4"> <form method="post" action="{{route('two-factor.verify')}}"> @csrf <div class="form-group"> <label for="code">{{ __('Verification Code') }}</label> <input type="text" class="form-control" name="code" id="code" autocomplete="off" required /> </div> <button type="submit" class="btn btn-primary mt-3">@lang('Submit')</button> </form> </div> </div> </div> </div> </div> </div> </div> @endsection
In this view the user enters the Google OTP and then submit the form to the route “two-factor.verify
“. This route represents the action verify2FA()
. In the verify2FA()
controller action the request is validated.Â
We get the current user two_factor_secret
which store the secret key. Using this secret key we invoked Google2FA::getCurrentOtp()
to retrieve the current active otp. Then we compare the current otp with the provided code from the form and update user two_factor_verified
flag to be 1Â
Check2Fa Middleware
In order to apply the 2FA functionality to the relevant pages, i added a middleware to check if the 2FA is enabled then we redirect the user to the verify 2FA page:
php artisan make:middleware Check2Fa
Next open the middleware class Check2Fa.php
and update the handle() method:
public function handle(Request $request, Closure $next): Response { if(auth()->check()) { if(auth()->user()->two_factor_enable && !auth()->user()->two_factor_verified) { return to_route('two-factor.verify-form'); } else { return $next($request); } } abort(403); }
The next step is apply the middleware to the pages that need verification, in our example i added a new route in web.php named home like so:
Route::middleware(['auth', \App\Http\Middleware\Check2Fa::class])->get('/home', function () { return view('home'); })->name('home');
To test the 2FA:
- Go to the page /two-factor to enable the 2FA
- Open your android Google Authenticator App and scan the Qrcode.
- Your account will be added in Google Authenticator App.
- Enter the OTP code from the Google Authenticator App in the form input and click submit.
- If the OTP is valid the 2FA is enabled and a message appears that the 2FA is activated.
- Now go to one of the pages that have the 2FA middleware applied, you will be redirected to the verify 2FA page.Â
- Enter the Google OTP from the Google Authenticator App and click submit now you become 2FA verified.
Showing The 2FA Form After Any Login
A remaining point you should implement is that if the flag two_factor_verified=1
then after each login he will be redirected normally to the application pages without showing the 2FA form.
To show the 2FA form after any login attempt add a code after the login logic to revert the two_factor_verified
flag. Depending on your login functionality this code should come after successful login.
app/Http/Controllers/Auth/LoginController.php
protected function authenticated(Request $request, $user) { $user->two_factor_verified = $user->two_factor_enable == 1 ? 0 : 1; $user->save(); }
Because i am using the laravel ui/auth package, the authenticated() method is where you add your custom code after successful authentication. In this case i am reverting the two_factor_verified
depending on two_factor_enable
Now the 2FA verify screen should appear after each login if 2FA enabled.