Introduction
In today's digital landscape, password-only authentication is no longer sufficient. Two-Factor Authentication (2FA) adds a critical security layer by requiring users to verify their identity through a second device—typically a smartphone app like Google Authenticator or Authy.
Whether you're building a SaaS application, financial platform, or any system handling sensitive data, implementing 2FA is essential for protecting user accounts from unauthorized access, phishing attacks, and credential theft.
In this comprehensive guide, we'll implement 2FA in Laravel 12 using PragmaRX Google2FA Laravel—a robust package that integrates Google's Time-based One-Time Password (TOTP) algorithm seamlessly with Laravel's authentication system.
By the end of this tutorial, you'll have a production-ready 2FA system with QR code generation, backup codes, and a smooth user experience.
Why Two-Factor Authentication Matters
The Security Gap
Passwords alone are vulnerable to:
- Brute force attacks – Automated password guessing
- Phishing – Social engineering to steal credentials
- Data breaches – Leaked password databases
- Keyloggers – Malware capturing keystrokes
How 2FA Protects You
2FA requires two authentication factors:
- Something you know – Your password
- Something you have – Your smartphone with authenticator app
Even if attackers steal passwords, they cannot access accounts without the second factor, reducing account takeover risk by 99.9% according to Microsoft's research.
Why Choose PragmaRX Google2FA?
The PragmaRX Google2FA Laravel package offers:
- Laravel Integration – Built specifically for Laravel applications
- QR Code Generation – Easy setup with visual QR codes
- TOTP Standard – Compatible with Google Authenticator, Authy, Microsoft Authenticator
- Window Tolerance – Handles time synchronization issues
- Backup Codes – Recovery options when devices are unavailable
- Well Maintained – Regular updates and Laravel 12 compatibility
- Lightweight – Minimal dependencies and overhead
Installation and Setup
Step 1: Install the Package
Install PragmaRX Google2FA Laravel via Composer:
composer require pragmarx/google2fa-laravel
The package will auto-register its service provider in Laravel 12.
Step 2: Install QR Code Generator
For generating QR codes, install BaconQrCode:
composer require bacon/bacon-qr-code
Step 3: Publish Configuration
Publish the configuration file (optional):
php artisan vendor:publish --provider="PragmaRX\Google2FALaravel\ServiceProvider"
This creates config/google2fa.php where you can customize settings.
Step 4: Add Database Columns
Create a migration to add 2FA fields to the users table:
php artisan make:migration add_two_factor_columns_to_users_table
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
Step 5: Update User Model
Add fillable fields and casts to your App\Models\User model:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
'google2fa_secret',
'google2fa_enabled',
'google2fa_enabled_at',
'recovery_codes',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'google2fa_enabled' => 'boolean',
'google2fa_enabled_at' => 'datetime',
'recovery_codes' => 'array',
];
protected $hidden = [
'password',
'remember_token',
'google2fa_secret',
'recovery_codes',
];
}
Implementing 2FA Setup Flow
Create Controller for 2FA Management
Generate a controller to handle 2FA operations:
php artisan make:controller TwoFactorController
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use PragmaRX\Google2FA\Google2FA;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
class TwoFactorController extends Controller
{
protected $google2fa;
public function __construct()
{
$this->middleware('auth');
$this->google2fa = new Google2FA();
}
/**
* Show 2FA setup page
*/
public function index()
{
$user = auth()->user();
return view('auth.two-factor.index', [
'enabled' => $user->google2fa_enabled,
'enabledAt' => $user->google2fa_enabled_at,
]);
}
/**
* Generate secret and QR code
*/
public function setup()
{
$user = auth()->user();
// Generate secret key
$secret = $this->google2fa->generateSecretKey();
// Store temporarily (not enabled yet)
$user->update(['google2fa_secret' => $secret]);
// Generate QR Code
$qrCodeUrl = $this->google2fa->getQRCodeUrl(
config('app.name'),
$user->email,
$secret
);
$writer = new Writer(
new ImageRenderer(
new RendererStyle(200),
new SvgImageBackEnd()
)
);
$qrCodeSvg = $writer->writeString($qrCodeUrl);
return view('auth.two-factor.setup', [
'secret' => $secret,
'qrCode' => $qrCodeSvg,
]);
}
/**
* Enable 2FA after verification
*/
public function enable(Request $request)
{
$request->validate([
'one_time_password' => 'required|numeric',
'password' => 'required',
]);
$user = auth()->user();
// Verify password
if (!Hash::check($request->password, $user->password)) {
return back()->withErrors(['password' => 'Invalid password']);
}
// Verify OTP
$valid = $this->google2fa->verifyKey(
$user->google2fa_secret,
$request->one_time_password
);
if (!$valid) {
return back()->withErrors([
'one_time_password' => 'Invalid verification code'
]);
}
// Generate recovery codes
$recoveryCodes = $this->generateRecoveryCodes();
// Enable 2FA
$user->update([
'google2fa_enabled' => true,
'google2fa_enabled_at' => now(),
'recovery_codes' => $recoveryCodes,
]);
return redirect()
->route('two-factor.recovery-codes')
->with('success', '2FA enabled successfully');
}
/**
* Show recovery codes
*/
public function showRecoveryCodes()
{
$user = auth()->user();
if (!$user->google2fa_enabled) {
return redirect()->route('two-factor.index');
}
return view('auth.two-factor.recovery-codes', [
'recoveryCodes' => $user->recovery_codes,
]);
}
/**
* Regenerate recovery codes
*/
public function regenerateRecoveryCodes(Request $request)
{
$request->validate(['password' => 'required']);
$user = auth()->user();
if (!Hash::check($request->password, $user->password)) {
return back()->withErrors(['password' => 'Invalid password']);
}
$recoveryCodes = $this->generateRecoveryCodes();
$user->update(['recovery_codes' => $recoveryCodes]);
return back()->with('success', 'Recovery codes regenerated');
}
/**
* Disable 2FA
*/
public function disable(Request $request)
{
$request->validate([
'password' => 'required',
'one_time_password' => 'required|numeric',
]);
$user = auth()->user();
if (!Hash::check($request->password, $user->password)) {
return back()->withErrors(['password' => 'Invalid password']);
}
// Verify OTP
$valid = $this->google2fa->verifyKey(
$user->google2fa_secret,
$request->one_time_password
);
if (!$valid) {
return back()->withErrors([
'one_time_password' => 'Invalid verification code'
]);
}
// Disable 2FA
$user->update([
'google2fa_secret' => null,
'google2fa_enabled' => false,
'google2fa_enabled_at' => null,
'recovery_codes' => null,
]);
return redirect()
->route('two-factor.index')
->with('success', '2FA disabled successfully');
}
/**
* Generate recovery codes
*/
protected function generateRecoveryCodes(): array
{
$codes = [];
for ($i = 0; $i < 8; $i++) {
$codes[] = Str::random(10);
}
return $codes;
}
}
Creating Authentication Middleware
Create middleware to enforce 2FA verification during login:
php artisan make:middleware TwoFactorAuthentication
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class TwoFactorAuthentication
{
public function handle(Request $request, Closure $next): Response
{
$user = auth()->user();
// Skip if 2FA not enabled
if (!$user || !$user->google2fa_enabled) {
return $next($request);
}
// Skip if already verified in this session
if (session('2fa_verified')) {
return $next($request);
}
// Redirect to 2FA verification page
return redirect()->route('two-factor.verify');
}
}
Register the middleware in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'2fa' => \App\Http\Middleware\TwoFactorAuthentication::class,
]);
})
2FA Verification During Login
Create a controller for login verification:
php artisan make:controller TwoFactorVerificationController
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use PragmaRX\Google2FA\Google2FA;
class TwoFactorVerificationController extends Controller
{
protected $google2fa;
public function __construct()
{
$this->middleware('auth');
$this->google2fa = new Google2FA();
}
public function show()
{
return view('auth.two-factor.verify');
}
public function verify(Request $request)
{
$request->validate([
'one_time_password' => 'required|numeric',
]);
$user = auth()->user();
// Verify OTP
$valid = $this->google2fa->verifyKey(
$user->google2fa_secret,
$request->one_time_password,
8 // Window tolerance for time sync issues
);
if ($valid) {
// Mark session as 2FA verified
session(['2fa_verified' => true]);
return redirect()
->intended(route('dashboard'))
->with('success', 'Successfully authenticated');
}
return back()
->withErrors(['one_time_password' => 'Invalid verification code'])
->withInput();
}
public function useRecoveryCode(Request $request)
{
$request->validate([
'recovery_code' => 'required|string',
]);
$user = auth()->user();
$recoveryCodes = $user->recovery_codes ?? [];
$code = $request->recovery_code;
// Check if recovery code exists
if (!in_array($code, $recoveryCodes)) {
return back()->withErrors([
'recovery_code' => 'Invalid recovery code'
]);
}
// Remove used recovery code
$remainingCodes = array_filter($recoveryCodes, fn($c) => $c !== $code);
$user->update(['recovery_codes' => array_values($remainingCodes)]);
// Mark session as verified
session(['2fa_verified' => true]);
return redirect()
->route('dashboard')
->with('warning', 'Recovery code used. Please regenerate new codes.');
}
}
Defining Routes
Add routes in routes/web.php:
use App\Http\Controllers\TwoFactorController;
use App\Http\Controllers\TwoFactorVerificationController;
Route::middleware(['auth'])->group(function () {
// 2FA Management
Route::get('/two-factor', [TwoFactorController::class, 'index'])
->name('two-factor.index');
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::post('/two-factor/disable', [TwoFactorController::class, 'disable'])
->name('two-factor.disable');
Route::get('/two-factor/recovery-codes', [TwoFactorController::class, 'showRecoveryCodes'])
->name('two-factor.recovery-codes');
Route::post('/two-factor/recovery-codes/regenerate', [TwoFactorController::class, 'regenerateRecoveryCodes'])
->name('two-factor.recovery-codes.regenerate');
// 2FA Verification
Route::get('/two-factor/verify', [TwoFactorVerificationController::class, 'show'])
->name('two-factor.verify')
->withoutMiddleware(['2fa']);
Route::post('/two-factor/verify', [TwoFactorVerificationController::class, 'verify'])
->name('two-factor.verify.post')
->withoutMiddleware(['2fa']);
Route::post('/two-factor/recovery', [TwoFactorVerificationController::class, 'useRecoveryCode'])
->name('two-factor.recovery')
->withoutMiddleware(['2fa']);
});
// Protected routes requiring 2FA
Route::middleware(['auth', '2fa'])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
});
Creating Blade Views
Setup Page (resources/views/auth/two-factor/setup.blade.php)
<x-app-layout>
<div class="py-12">
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h2 class="text-2xl font-bold mb-6">Set Up Two-Factor Authentication</h2>
<div class="mb-6">
<p class="text-gray-600 mb-4">
Scan this QR code with your authenticator app (Google Authenticator, Authy, etc.)
</p>
<div class="flex justify-center mb-4">
{!! $qrCode !!}
</div>
<div class="bg-gray-100 p-4 rounded">
<p class="text-sm text-gray-600 mb-2">Or enter this code manually:</p>
<code class="text-lg font-mono">{{ $secret }}</code>
</div>
</div>
<form method="POST" action="{{ route('two-factor.enable') }}">
@csrf
<div class="mb-4">
<label class="block text-sm font-medium mb-2">
Verification Code
</label>
<input
type="text"
name="one_time_password"
class="w-full border rounded px-3 py-2"
placeholder="Enter 6-digit code"
required
autofocus
>
@error('one_time_password')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">
Confirm Password
</label>
<input
type="password"
name="password"
class="w-full border rounded px-3 py-2"
required
>
@error('password')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="bg-blue-500 text-white px-6 py-2 rounded">
Enable 2FA
</button>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
Verification Page (resources/views/auth/two-factor/verify.blade.php)
<x-guest-layout>
<div class="max-w-md mx-auto">
<h2 class="text-2xl font-bold mb-6">Two-Factor Authentication</h2>
<form method="POST" action="{{ route('two-factor.verify.post') }}">
@csrf
<div class="mb-4">
<label class="block text-sm font-medium mb-2">
Enter 6-digit code from your authenticator app
</label>
<input
type="text"
name="one_time_password"
class="w-full border rounded px-3 py-2 text-center text-2xl"
maxlength="6"
required
autofocus
>
@error('one_time_password')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="w-full bg-blue-500 text-white py-2 rounded mb-4">
Verify
</button>
</form>
<div class="text-center">
<button
onclick="document.getElementById('recovery-form').classList.toggle('hidden')"
class="text-sm text-blue-500"
>
Use recovery code instead
</button>
</div>
<form
id="recovery-form"
method="POST"
action="{{ route('two-factor.recovery') }}"
class="hidden mt-4"
>
@csrf
<input
type="text"
name="recovery_code"
class="w-full border rounded px-3 py-2 mb-2"
placeholder="Enter recovery code"
>
<button type="submit" class="w-full bg-gray-500 text-white py-2 rounded">
Use Recovery Code
</button>
</form>
</div>
</x-guest-layout>
Best Practices and Security Considerations
1. Time Window Tolerance
The Google2FA library supports window tolerance to handle time synchronization issues:
$valid = $this->google2fa->verifyKey($secret, $code, 8);
This checks 8 time windows (4 before, current, 3 after) for better user experience.
2. Rate Limiting
Add rate limiting to verification endpoints:
Route::post('/two-factor/verify', [TwoFactorVerificationController::class, 'verify'])
->middleware('throttle:5,1'); // 5 attempts per minute
3. Session Security
Clear 2FA verification status on logout:
public function logout(Request $request)
{
Auth::logout();
$request->session()->forget('2fa_verified');
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
4. Encrypted Storage
Consider encrypting secrets in the database:
protected $casts = [
'google2fa_secret' => 'encrypted',
'recovery_codes' => 'encrypted:array',
];
Conclusion
Implementing Two-Factor Authentication in Laravel 12 using PragmaRX Google2FA Laravel dramatically improves your application's security posture. With QR code setup, backup recovery codes, and smooth user flows, you provide enterprise-grade protection without sacrificing user experience.
Key takeaways:
- Install and configure Google2FA in minutes
- Generate QR codes for easy authenticator app setup
- Create recovery codes for account recovery
- Protect routes with 2FA middleware
- Handle time synchronization gracefully
Need help implementing 2FA or other security features in your Laravel application? NeedLaravelSite specializes in building secure, production-ready Laravel applications. Contact us for expert Laravel development services.
Related Resources: