google2fa = new Google2FA(); } /** * Show 2FA setup page (generate QR code data). */ public function show() { $user = auth()->user(); // If not yet set up, generate a new secret if (!$user->two_factor_secret) { $secret = $this->google2fa->generateSecretKey(); $user->update(['two_factor_secret' => $secret]); } $secret = $user->two_factor_secret; $otpUrl = $this->google2fa->getQRCodeUrl( config('app.name'), $user->email, $secret ); // Generate QR code as SVG using BaconQrCode $renderer = new \BaconQrCode\Renderer\ImageRenderer( new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200), new \BaconQrCode\Renderer\Image\SvgImageBackEnd() ); $qrCode = (new \BaconQrCode\Writer($renderer))->writeString($otpUrl); $qrCodeBase64 = 'data:image/svg+xml;base64,' . base64_encode($qrCode); return Inertia::render('TwoFactor/Setup', [ 'enabled' => !is_null($user->two_factor_confirmed_at), 'qr_code' => $qrCodeBase64, 'secret' => $secret, 'recovery_codes' => $user->two_factor_recovery_codes ? json_decode($user->two_factor_recovery_codes, true) : [], ]); } /** * Confirm & enable 2FA. */ public function enable(Request $request) { $request->validate([ 'code' => 'required|string', ]); $user = auth()->user(); $secret = $user->two_factor_secret; $valid = $this->google2fa->verifyKey($secret, $request->code); if (!$valid) { return back()->withErrors(['code' => 'Invalid authentication code. Please try again.']); } // Generate recovery codes $recoveryCodes = Collection::times(8, fn() => Str::random(10) . '-' . Str::random(10)); $user->update([ 'two_factor_confirmed_at' => now(), 'two_factor_recovery_codes' => json_encode($recoveryCodes->toArray()), ]); return back()->with('success', 'Two-Factor Authentication has been enabled successfully.'); } /** * Disable 2FA. */ public function disable(Request $request) { $request->validate([ 'password' => 'required|current_password', ]); auth()->user()->update([ 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, 'two_factor_confirmed_at' => null, ]); return back()->with('success', 'Two-Factor Authentication has been disabled.'); } /** * Regenerate recovery codes. */ public function regenerateCodes() { $recoveryCodes = Collection::times(8, fn() => Str::random(10) . '-' . Str::random(10)); auth()->user()->update([ 'two_factor_recovery_codes' => json_encode($recoveryCodes->toArray()), ]); return back()->with('success', 'Recovery codes have been regenerated.'); } /** * Show the 2FA challenge screen (after login). */ public function challenge(Request $request) { if (!$request->session()->has('two_factor_user_id')) { return redirect()->route('login'); } return Inertia::render('TwoFactor/Challenge'); } /** * Verify the 2FA challenge code after login. */ public function verify(Request $request) { $request->validate([ 'code' => 'required|string', ]); $userId = $request->session()->get('two_factor_user_id'); if (!$userId) { return redirect()->route('login'); } $user = \App\Models\User::find($userId); if (!$user || !$user->two_factor_secret) { $request->session()->forget('two_factor_user_id'); return redirect()->route('login'); } $code = $request->code; $valid = $this->google2fa->verifyKey($user->two_factor_secret, $code); if (!$valid) { $recoveryCodes = json_decode($user->two_factor_recovery_codes ?? '[]', true); if (in_array($code, $recoveryCodes)) { $remaining = array_filter($recoveryCodes, fn($c) => $c !== $code); $user->update(['two_factor_recovery_codes' => json_encode(array_values($remaining))]); $valid = true; } } if (!$valid) { return back()->withErrors(['code' => 'Invalid code. Please try again.']); } $request->session()->forget('two_factor_user_id'); \Illuminate\Support\Facades\Auth::login($user); $request->session()->regenerate(); return redirect()->intended(route('dashboard')); } }