Two-factor authentication (2FA) adds an extra layer of security by requiring users to provide a second verification method beyond their password.
How 2FA Works
┌─────────────────────────────────────────────────────────────────┐
│ TWO-FACTOR AUTHENTICATION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Login with Email + Password │
│ ┌──────────────────────────────────────┐ │
│ │ Something you KNOW │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 2: Verify with TOTP Code or Recovery Code │
│ ┌──────────────────────────────────────┐ │
│ │ Something you HAVE (phone/app) │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Access Granted │
│ │
└─────────────────────────────────────────────────────────────────┘
Using Laravel Fortify
Laravel Fortify provides 2FA out of the box:
bashcomposer require laravel/fortify php artisan fortify:install php artisan migrate
Enable 2FA in config/fortify.php:
php'features' => [ Features::registration(), Features::resetPasswords(), Features::emailVerification(), Features::updateProfileInformation(), Features::updatePasswords(), Features::twoFactorAuthentication([ 'confirm' => true, 'confirmPassword' => true, ]), ],
Manual Implementation
Installation
bashcomposer require pragmarx/google2fa-laravel composer require bacon/bacon-qr-code php artisan vendor:publish --provider="PragmaRX\Google2FALaravel\ServiceProvider"
Migration
phppublic function up(): void { Schema::table('users', function (Blueprint $table) { $table->text('two_factor_secret')->nullable(); $table->text('two_factor_recovery_codes')->nullable(); $table->timestamp('two_factor_confirmed_at')->nullable(); }); }
User Model
phpuse PragmaRX\Google2FA\Google2FA; use Illuminate\Support\Str; class User extends Authenticatable { protected $hidden = [ 'password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes', ]; protected function casts(): array { return [ 'two_factor_secret' => 'encrypted', 'two_factor_recovery_codes' => 'encrypted:array', ]; } public function hasTwoFactorEnabled(): bool { return ! is_null($this->two_factor_confirmed_at); } public function enableTwoFactor(): void { $google2fa = new Google2FA(); $this->two_factor_secret = $google2fa->generateSecretKey(); $this->two_factor_recovery_codes = $this->generateRecoveryCodes(); $this->save(); } public function confirmTwoFactor(string $code): bool { $google2fa = new Google2FA(); if ($google2fa->verifyKey($this->two_factor_secret, $code)) { $this->two_factor_confirmed_at = now(); $this->save(); return true; } return false; } public function disableTwoFactor(): void { $this->two_factor_secret = null; $this->two_factor_recovery_codes = null; $this->two_factor_confirmed_at = null; $this->save(); } protected function generateRecoveryCodes(): array { return collect(range(1, 8)) ->map(fn () => Str::random(10) . '-' . Str::random(10)) ->all(); } public function getQrCodeUrl(): string { $google2fa = new Google2FA(); return $google2fa->getQRCodeUrl( config('app.name'), $this->email, $this->two_factor_secret ); } }
Controller
phpclass TwoFactorController extends Controller { public function show(Request $request) { $user = $request->user(); if ($user->hasTwoFactorEnabled()) { return view('settings.two-factor.enabled'); } // Generate secret if not exists if (! $user->two_factor_secret) { $user->enableTwoFactor(); } // Generate QR code $qrCode = (new \BaconQrCode\Renderer\Image\SvgImageBackEnd()) ->render($user->getQrCodeUrl()); return view('settings.two-factor.setup', [ 'qrCode' => $qrCode, 'secret' => $user->two_factor_secret, 'recoveryCodes' => $user->two_factor_recovery_codes, ]); } public function confirm(Request $request) { $request->validate([ 'code' => 'required|string|size:6', ]); if ($request->user()->confirmTwoFactor($request->code)) { return redirect()->route('settings') ->with('success', '2FA enabled successfully!'); } return back()->withErrors(['code' => 'Invalid verification code.']); } public function disable(Request $request) { $request->validate([ 'password' => 'required|current_password', ]); $request->user()->disableTwoFactor(); return redirect()->route('settings') ->with('success', '2FA disabled.'); } }
Login with 2FA
phpclass LoginController extends Controller { public function login(Request $request) { $credentials = $request->validate([ 'email' => 'required|email', 'password' => 'required', ]); if (! Auth::attempt($credentials)) { return back()->withErrors(['email' => 'Invalid credentials.']); } $user = Auth::user(); if ($user->hasTwoFactorEnabled()) { Auth::logout(); $request->session()->put('login.id', $user->id); return redirect()->route('two-factor.challenge'); } return redirect()->intended('/dashboard'); } }