Laravel 2FA Tutorial
Add TOTP two-factor authentication to a Laravel app using pragmarx/google2fa-laravel. This guide covers secret generation, QR code display, code verification, and how to test the flow against 2FAA's authenticator.
1. Install the package
composer require pragmarx/google2fa-laravel
composer require bacon/bacon-qr-codegoogle2fa-laravel handles TOTP generation/verification. bacon-qr-code renders the QR code so users can scan with an authenticator.
2. Add a 2FA secret column
// database/migrations/2026_xx_xx_add_2fa_secret_to_users.php
Schema::table('users', function (Blueprint $table) {
$table->string('two_factor_secret')->nullable();
$table->boolean('two_factor_enabled')->default(false);
});3. Generate and show the secret
use PragmaRX\Google2FA\Google2FA;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
public function setup(Request $request)
{
$g2fa = new Google2FA();
$secret = $g2fa->generateSecretKey();
$request->user()->update(['two_factor_secret' => $secret]);
$otpauthUri = $g2fa->getQRCodeUrl(
config('app.name'),
$request->user()->email,
$secret
);
$writer = new Writer(new ImageRenderer(
new RendererStyle(192),
new SvgImageBackEnd()
));
$qrSvg = $writer->writeString($otpauthUri);
return view('two-factor.setup', compact('secret', 'qrSvg'));
}4. Verify the code on confirm
public function confirm(Request $request)
{
$request->validate(['code' => 'required|digits:6']);
$g2fa = new Google2FA();
$valid = $g2fa->verifyKey(
$request->user()->two_factor_secret,
$request->code,
2 // accept ±1 window for clock drift
);
if (! $valid) {
return back()->withErrors(['code' => 'Invalid 2FA code']);
}
$request->user()->update(['two_factor_enabled' => true]);
return redirect()->route('dashboard')
->with('status', '2FA enabled');
}5. Require 2FA at login
// app/Http/Middleware/Require2FA.php
public function handle($request, Closure $next)
{
$user = $request->user();
if ($user && $user->two_factor_enabled && ! session('2fa_passed')) {
return redirect()->route('two-factor.challenge');
}
return $next($request);
}On the challenge route, accept a 6-digit code and call verifyKey() the same way as in step 4. On success, set session(['2fa_passed' => true]) and redirect.
Test the flow with 2FAA
Two ways to test your Laravel 2FA implementation:
- Web: open 2FAA's authenticator, paste the generated Base32 secret, and use the 6-digit code to confirm setup.
- From your AI agent: install the 2faa-mcp server and have Claude Code generate test codes on demand — useful when writing end-to-end tests.
Recovery codes (recommended)
Generate 8–10 single-use recovery codes when 2FA is enabled. Store them hashed (just like passwords). Let the user enter one if they lose their authenticator.
$codes = collect(range(1, 10))
->map(fn() => Str::random(10))
->all();
$user->update([
'two_factor_recovery_codes' => encrypt(json_encode($codes)),
]);
// show $codes to the user once