Why Implement MFA/2FA in Your Laravel Application?
MFA significantly reduces the risk of account compromises. According to Microsoft, it blocks over 99.9% of automated attacks. Common threats like phishing, credential stuffing, and password breaches become ineffective when a second factor is required. For Laravel developers, integrating MFA ensures compliance with security standards like GDPR or PCI DSS, especially for e-commerce, banking, or SaaS apps.
Google Authenticator is a popular choice because it's free, works offline, and supports TOTP—a standard that generates codes every 30 seconds. It's compatible with apps like Authy or Microsoft Authenticator too.
In this tutorial, we will walk through a complete implementation of Google Authenticator-based 2FA in Laravel 12, covering package installation, database migration, controller logic, middleware protection, Blade views, and recovery code generation. By the end, your users will have a secure, industry-standard second factor protecting their accounts.

Table Of Content
1 Prerequisites
- PHP ≥ 8.2
- Composer
- MySQL / PostgreSQL / SQLite (or any supported DB)
- Basic knowledge of Laravel authentication (e.g., using Breeze or Jetstream starters).
2 Introduction
We will use the PragmaRX Google2FA package along with BaconQrCode for generating scannable QR codes. The implementation covers a complete flow: setting up 2FA, verifying TOTP codes on login, protecting routes with custom middleware, and providing recovery codes as a fallback. This approach works with any TOTP-compatible authenticator app, including Google Authenticator, Authy, and Microsoft Authenticator.
3 Create a Fresh Laravel 12 Project
Let us start by creating a fresh Laravel 12 project and configuring the database connection. If you already have an existing Laravel 12 project, you can skip to Step 4 — Installing Required Packages.
3.1 Install Laravel Project
composer create-project laravel/laravel:^12.0 laravel-12-mfa-2fa-google-authenticator-setup
Navigate to your project directory:
cd laravel-12-mfa-2fa-google-authenticator-setup
3.2 Configure Your Database (.env)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_12_mfa
DB_USERNAME=root
DB_PASSWORD=
4 Installing Required Packages in Laravel 12
composer require pragmarx/google2fa
composer require bacon/bacon-qr-code
Publish the config file (optional):
php artisan vendor:publish --provider="PragmaRX\Google2FA\ServiceProvider"
This will create a config/google2fa.php file where you can customize settings like the key length, algorithm, and time window.
5 Create Migration & Model for Users Table
<?php
// database/migrations/2025_03_01_000000_create_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('google2fa_secret')->nullable();
$table->boolean('google2fa_enabled')->default(false);
$table->timestamp('google2fa_enabled_at')->nullable();
$table->json('recovery_codes')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['google2fa_secret', 'google2fa_enabled', 'google2fa_enabled_at', 'recovery_codes']);
});
}
};
Run the migration:
php artisan migrate
app/Models/User.php (Update Fillable/Hidden)
// Add to existing User model
protected $fillable = [
// ... existing
'google2fa_secret',
'google2fa_enabled',
'google2fa_enabled_at',
'recovery_codes',
];
protected $hidden = [
// ... existing
'google2fa_secret',
'recovery_codes',
];
Important: The google2fa_secret column is stored as text because it holds an encrypted value via Laravel's encrypt() helper. Never store raw TOTP secrets in plain text.
6 Create 2FA Controller
Generate the controller using Artisan:
php artisan make:controller TwoFactorController
Edit app/Http/Controllers/TwoFactorController.php. This controller handles the complete 2FA lifecycle: generating and displaying the QR code, enabling 2FA after code verification, verifying codes on login, managing recovery codes, and disabling 2FA.
<?php
namespace App\Http\Controllers;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use PragmaRX\Google2FA\Google2FA;
class TwoFactorController extends Controller
{
protected $google2fa;
public function __construct()
{
$this->middleware('auth');
$this->google2fa = app('pragmarx.google2fa');
}
public function setup()
{
$user = auth()->user();
if ($user->google2fa_enabled) {
return redirect()->route('dashboard');
}
$secret = $this->google2fa->generateSecretKey();
$user->google2fa_secret = encrypt($secret);
$user->save();
$qrCodeUrl = $this->google2fa->getQRCodeUrl(
config('app.name'),
$user->email,
$secret
);
$writer = new Writer(
new ImageRenderer(
new RendererStyle(400),
new SvgImageBackEnd()
)
);
$qrCodeSvg = $writer->writeString($qrCodeUrl);
return view('two-factor.setup', compact('qrCodeSvg', 'secret'));
}
public function enable(Request $request)
{
$request->validate(['one_time_password' => 'required']);
$user = auth()->user();
$secret = decrypt($user->google2fa_secret);
$valid = $this->google2fa->verifyKey($secret, $request->one_time_password);
if (!$valid) {
return back()->withErrors(['one_time_password' => 'Invalid code']);
}
$user->google2fa_enabled = true;
$user->google2fa_enabled_at = now();
$user->recovery_codes = $this->generateRecoveryCodes();
$user->save();
return redirect()->route('two-factor.recovery');
}
public function verifyForm()
{
if (session('2fa_verified')) {
return redirect()->route('dashboard');
}
return view('two-factor.verify');
}
public function verify(Request $request)
{
$request->validate(['one_time_password' => 'required']);
$user = auth()->user();
$secret = decrypt($user->google2fa_secret);
$valid = $this->google2fa->verifyKey($secret, $request->one_time_password, 8); // Tolerance for clock drift
if (!$valid) {
return back()->withErrors(['one_time_password' => 'Invalid code']);
}
session(['2fa_verified' => true]);
return redirect()->route('dashboard');
}
public function recovery()
{
$user = auth()->user();
$recoveryCodes = collect(json_decode($user->recovery_codes));
return view('two-factor.recovery', compact('recoveryCodes'));
}
public function regenerateRecovery()
{
$user = auth()->user();
$user->recovery_codes = $this->generateRecoveryCodes();
$user->save();
return redirect()->route('two-factor.recovery');
}
public function disable(Request $request)
{
// Validate password or code if needed
$user = auth()->user();
$user->google2fa_secret = null;
$user->google2fa_enabled = false;
$user->google2fa_enabled_at = null;
$user->recovery_codes = null;
$user->save();
session()->forget('2fa_verified');
return redirect()->route('dashboard');
}
protected function generateRecoveryCodes(): array
{
return Collection::times(8, function () {
return $this->google2fa->generateSecretKey(10);
})->toArray();
}
}
?>
7 Middleware for Verification
The middleware checks whether a logged-in user has 2FA enabled and whether they have already verified their TOTP code in the current session. If not, it redirects them to the verification page.
Create app/Http/Middleware/VerifyTwoFactor.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class VerifyTwoFactor
{
public function handle(Request $request, Closure $next)
{
$user = $request->user();
if ($user && $user->google2fa_enabled && !session('2fa_verified')) {
return redirect()->route('two-factor.verify');
}
return $next($request);
}
}
Register the middleware in app/Http/Kernel.php (for Laravel 12, you may also register it in bootstrap/app.php depending on your setup):
protected $routeMiddleware = [
// ...
'2fa' => \App\Http\Middleware\VerifyTwoFactor::class,
];
Note: In Laravel 12 with the slim application skeleton, middleware registration may use withMiddleware() in bootstrap/app.php. Refer to the Laravel 12 Routing and Middleware Guide for details.
8 Define Routes
Route::middleware('auth')->group(function () {
Route::get('/two-factor/setup', [TwoFactorController::class, 'setup'])->name('two-factor.setup');
Route::post('/two-factor/enable', [TwoFactorController::class, 'enable'])->name('two-factor.enable');
Route::get('/two-factor/verify', [TwoFactorController::class, 'verifyForm'])->name('two-factor.verify');
Route::post('/two-factor/verify', [TwoFactorController::class, 'verify']);
Route::get('/two-factor/recovery', [TwoFactorController::class, 'recovery'])->name('two-factor.recovery');
Route::post('/two-factor/regenerate', [TwoFactorController::class, 'regenerateRecovery'])->name('two-factor.regenerate');
Route::post('/two-factor/disable', [TwoFactorController::class, 'disable'])->name('two-factor.disable');
Route::get('/dashboard', function () { return view('dashboard'); })->middleware('2fa')->name('dashboard');
});
9 Views (resources/views/two-factor/)
setup.blade.php
@extends('layouts.app')
@section('content')
<h1>Setup 2FA</h1>
{!! $qrCodeSvg !!}
<p>Secret: {{ $secret }}</p>
<form method="POST" action="{{ route('two-factor.enable') }}">
@csrf
<input type="text" name="one_time_password" placeholder="Enter Code">
<button type="submit">Enable</button>
</form>
@endsection
verify.blade.php
@extends('layouts.app')
@section('content')
<h1>Verify 2FA</h1>
<form method="POST" action="{{ route('two-factor.verify') }}">
@csrf
<input type="text" name="one_time_password" placeholder="Enter 6-digit Code">
<button type="submit">Verify</button>
</form>
@endsection
recovery.blade.php
@extends('layouts.app')
@section('content')
<h1>Recovery Codes</h1>
@foreach ($recoveryCodes as $code)
<p>{{ $code }}</p>
@endforeach
<form method="POST" action="{{ route('two-factor.regenerate') }}">
@csrf
<button type="submit">Regenerate</button>
</form>
@endsection
10 Folder Structure
11 Run & Test the Application
php artisan serve
Visit: http://127.0.0.1:8000/
Testing Steps:
- Register or log in to your application.
- Navigate to /two-factor/setup to scan the QR code with Google Authenticator.
- Enter the 6-digit TOTP code to enable 2FA.
- Save your recovery codes securely.
- Log out and log back in — you will be prompted for the 2FA code before accessing the dashboard.
Security Best Practices:
- Always encrypt MFA secrets using Laravel's
encrypt()helper. - Use rate limiting on MFA verification attempts to prevent brute-force attacks.
- Offer backup recovery codes so users are not locked out if they lose their device.
- Allow users to disable MFA securely after re-authentication.
- Log MFA events (enable, disable, failed attempts) for security auditing.
12 Conclusion
In this tutorial, we covered the full MFA implementation flow: installing the PragmaRX Google2FA and BaconQrCode packages, adding migration columns for TOTP secrets, building a controller with setup, verification, and recovery logic, creating middleware to protect routes, and defining Blade views for the user interface.
If security matters to your product, MFA should not be optional — it should be standard. Consider extending this implementation with features like trusted device remembering, email-based fallback verification, or admin-enforced MFA policies for your team.
Related Laravel 12 Tutorials
If you found this MFA tutorial helpful, check out these related Laravel 12 guides on our site:
- Best Authentication Options in Laravel 12: A Comprehensive Tutorial Using WorkOS AuthKit — Explore modern authentication strategies for Laravel 12 including social login, SSO, and WorkOS integration.
- Laravel 12 Routing and Middleware: Complete Guide with Code Snippets — Master route definitions, middleware registration, and route protection in Laravel 12.
- Laravel 12 Performance Tips: Optimize Your App for Lightning-Fast Loading — Learn caching, query optimization, and deployment best practices.
- How to Build a REST API in Laravel 12: Step-by-Step Guide with Examples — Build secure, well-structured APIs with authentication and validation.
- Laravel 12 Eloquent Tips and Tricks for Beginners — Improve your database queries with Eloquent ORM best practices.
Written by Revathi M
PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel
Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.
Frequently Asked Questions
MFA (Multi-Factor Authentication) in Laravel is a security mechanism that requires users to provide a second form of verification beyond their password. This is typically implemented using a time-based one-time password (TOTP) generated by apps like Google Authenticator. In Laravel, you can add MFA using packages like PragmaRX Google2FA, which handles secret key generation, QR code URLs, and code verification.
Yes, Google Authenticator uses the industry-standard TOTP (Time-Based One-Time Password) algorithm defined in RFC 6238. It is considered highly secure when combined with best practices such as encrypting the secret key in your database, applying rate limiting on verification attempts, and providing backup recovery codes to users.
No, Laravel 12 does not include built-in TOTP-based multi-factor authentication. However, it can be implemented easily using community packages like PragmaRX Google2FA for TOTP logic and BaconQrCode for QR code rendering. Laravel Fortify also offers optional 2FA support if you prefer a first-party solution.
Yes, MFA can be disabled for a user in Laravel by clearing the stored google2fa_secret, setting the google2fa_enabled flag to false, and removing any saved recovery codes. It is important to require re-authentication (password or current TOTP code) before allowing users to disable MFA to prevent unauthorized changes.
Absolutely. MFA secrets must always be encrypted in the database using Laravel's encrypt() helper function. Storing secrets in plain text means that a database breach would expose all TOTP secrets, allowing attackers to generate valid codes. Encryption ensures the secrets are useless without your application's APP_KEY.
