Two-Factor Authentication (2FA) adds an extra layer of security to your Laravel application by requiring users to verify their identity using a time-based one-time password (TOTP). Google Authenticator is a popular choice for implementing 2FA.
In this guide, we'll walk through setting up Google Authenticator 2FA in Laravel using the pragmarx/google2fa-laravel
package.
First, create a new Laravel project:
composer create-project laravel/laravel 2fa
cd 2fa
We will add a custom column first.
Edit the users migration file (database/migrations/...create_users_table.php
):
$table->text('google2fa_secret')->nullable();
Execute the migration to update the database:
php artisan migrate:fresh
Set up basic authentication scaffolding:
composer require laravel/ui
php artisan ui bootstrap --auth
Install and build frontend dependencies:
npm install && npm run build
Install the Google2FA Laravel package:
composer require pragmarx/google2fa-laravel
We'll need bacon/bacon-qr-code
for generating QR codes:
composer require bacon/bacon-qr-code
Publish the Google2FA configuration file:
php artisan vendor:publish --provider="PragmaRX\Google2FALaravel\ServiceProvider"
Update config/google2fa.php
with the following:
<?php
return [
'enabled' => env('OTP_ENABLED', true),
'lifetime' => env('OTP_LIFETIME', 0),
'keep_alive' => env('OTP_KEEP_ALIVE', true),
'auth' => 'auth',
'guard' => '',
'session_var' => 'google2fa',
'otp_input' => 'one_time_password',
'window' => 1,
'forbid_old_passwords' => false,
'otp_secret_column' => 'google2fa_secret',
'view' => 'auth.2fa_verify',
'session_variable' => 'google2fa_passed',
'error_messages' => [
'wrong_otp' => "The 'One Time Password' typed was wrong.",
'cannot_be_empty' => 'One Time Password cannot be empty.',
'unknown' => 'An unknown error has occurred. Please try again.',
],
'throw_exceptions' => env('OTP_THROW_EXCEPTION', true),
'qrcode_image_backend' => \PragmaRX\Google2FALaravel\Support\Constants::QRCODE_IMAGE_BACKEND_SVG,
];
Update routes/web.php
:
<?php
use App\Http\Controllers\TwoFactorController;
use Illuminate\Support\Facades\Route;
Auth::routes();
Route::get('/',function(){
return view('welcome');
});
Route::middleware(['auth'])->group(function () {
Route::get('/2fa/setup', [TwoFactorController::class, 'setup'])->name('2fa.setup');
Route::get('/2fa/disable', [TwoFactorController::class, 'showDisableForm'])->name('2fa.disable.form');
Route::post('/2fa/enable', [TwoFactorController::class, 'enable'])->name('2fa.enable');
Route::post('/2fa/disable', [TwoFactorController::class, 'disable'])->name('2fa.disable');
});
Route::middleware(['auth', '2fa'])->group(function () {
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::post('/2fa', function () {
return redirect()->intended('/home');
})->name('2fa');
});
Create app/Http/Controllers/TwoFactorController.php
:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
class TwoFactorController extends Controller
{
public function setup()
{
$google2fa = app('pragmarx.google2fa');
$user = Auth::user();
if ($user->google2fa_secret) {
return redirect()->route('home')->with('info', '2FA already enabled.');
}
$secret = $google2fa->generateSecretKey();
$qrUrl = $google2fa->getQRCodeUrl(
config('app.name'),
$user->email,
$secret
);
$writer = new Writer(
new ImageRenderer(
new RendererStyle(200),
new SvgImageBackEnd()
)
);
$qrCodeSvg = base64_encode($writer->writeString($qrUrl));
session(['2fa_secret' => $secret]);
return view('auth.2fa_setup', compact('qrCodeSvg', 'secret'));
}
public function enable(Request $request)
{
$request->validate([
'otp' => 'required|digits:6',
]);
$user = Auth::user();
$secret = session('2fa_secret');
$google2fa = app('pragmarx.google2fa');
if ($google2fa->verifyKey($secret, $request->otp)) {
$user->google2fa_secret = $secret;
$user->save();
return redirect()->route('home')->with('success', '2FA enabled!');
}
return back()->with('error', 'Invalid OTP, please try again.');
}
public function showDisableForm()
{
$user = Auth::user();
if (!$user->google2fa_secret) {
return redirect()->route('home')->with('info', '2FA is not enabled.');
}
return view('auth.2fa_disable');
}
public function disable(Request $request)
{
$request->validate([
'otp' => 'required|digits:6',
]);
$user = Auth::user();
if (!$user->google2fa_secret) {
return redirect()->route('home')->with('info', '2FA is not enabled.');
}
$google2fa = app('pragmarx.google2fa');
$otpValid = $google2fa->verifyKey($user->google2fa_secret, $request->input('otp'));
if (!$otpValid) {
return back()->with('error', 'Invalid OTP. Please try again.');
}
$user->google2fa_secret = null;
$user->save();
session()->forget('google2fa_passed');
return redirect()->route('home')->with('success', '2FA has been disabled.');
}
}
Create resources/views/auth/2fa_setup.blade.php
:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2>Enable Two-Factor Authentication</h2>
<p>Set up your two factor authentication by scanning the barcode below. Alternatively, you can use the code
<strong>{{ $secret }}</strong></p>
<p>Ensure you submit the current one because it refreshes every 30 seconds.</p>
<img src="data:image/svg+xml;base64,{{ $qrCodeSvg }}" alt="QR Code">
<form method="POST" action="{{ route('2fa.enable') }}" class="mt-4">
@csrf
<div class="form-group">
<label for="otp">Enter OTP from app:</label>
<input type="number" name="otp" id="otp" class="form-control" required>
</div>
@error('otp')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
@if (session('error') && !$errors->has('otp'))
<span class="invalid-feedback" role="alert" style="display: block;">
<strong>{{ session('error') }}</strong>
</span>
@endif
<button class="btn btn-primary mt-2" type="submit">Enable 2FA</button>
</form>
</div>
</div>
</div>
@endsection
Create resources/views/auth/2fa_verify.blade.php
:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row py-5 justify-content-center">
<div class="col-md-6">
<div class="card p-4 bg-white">
<h2>Two-Factor Authentication</h2>
<form method="POST" action="{{ route('2fa') }}">
@csrf
<div class="form-group">
<p>Please enter the <strong>OTP</strong> generated on your Authenticator App. <br> Ensure you submit the current one because it refreshes every 30 seconds.</p>
<label for="one_time_password">Enter the 6-digit OTP from your app:</label>
<input type="number" name="one_time_password" class="form-control" required>
</div>
@if (count($errors) > 0)
@foreach ($errors->all() as $error)
<p class="text-danger">{{ $error }}</p>
@endforeach
@endif
<button class="btn btn-primary mt-3" type="submit">Verify</button>
</form>
</div>
</div>
</div>
</div>
@endsection
Create resources/views/auth/2fa_disable.blade.php
:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row py-5 justify-content-center">
<div class="col-md-6">
<div class="card p-4 bg-white">
<h2>Disable Two-Factor Authentication</h2>
<form method="POST" action="{{ route('2fa.disable') }}">
@csrf
<div class="form-group">
<p>Please enter the <strong>OTP</strong> generated on your Authenticator App. <br> Ensure you
submit the current one because it refreshes every 30 seconds.</p>
<label for="one_time_password">Enter OTP to disable 2FA:</label>
<input type="number" name="otp" class="form-control" required>
</div>
@if (count($errors) > 0)
@foreach ($errors->all() as $error)
<p class="text-danger">{{ $error }}</p>
@endforeach
@endif
<button class="btn btn-danger mt-3" type="submit">Disable 2FA</button>
</form>
</div>
</div>
</div>
</div>
@endsection
Update bootstrap/app.php
to register the 2FA middleware:
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'2fa' => \PragmaRX\Google2FALaravel\Middleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
Update your navigation menu (e.g., resources/views/layouts/app.blade.php
) to include 2FA links:
@auth
@if (Auth::user()->google2fa_secret)
<li class="nav-item">
<a class="nav-link" href="{{ route('2fa.disable.form') }}">Disable 2FA</a>
</li>
@else
<li class="nav-item">
<a class="nav-link" href="{{ route('2fa.setup') }}">Enable 2FA </a>
</li>
@endif
@endauth
Run the development server:
php artisan serve
Register a user and navigate to /2fa/setup
.
Scan the QR code using Google Authenticator or a similar app.
Enter the OTP to enable 2FA.
Now, whenever the user logs in, they'll be prompted for a 2FA code.
You've successfully implemented Google Authenticator 2FA in Laravel! This enhances your application's security by requiring users to verify their identity with a time-based OTP.