277 lines
8.9 KiB
PHP
277 lines
8.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Inertia;
|
|
use PragmaRX\Google2FA\Google2FA;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
|
|
class TwoFactorController extends Controller
|
|
{
|
|
protected Google2FA $google2fa;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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()),
|
|
'email_2fa_enabled' => false, // Automatically disable Email 2FA
|
|
]);
|
|
|
|
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.');
|
|
}
|
|
|
|
public function toggleEmail(Request $request)
|
|
{
|
|
$request->validate([
|
|
'password' => 'required|current_password',
|
|
'enabled' => 'required|boolean',
|
|
]);
|
|
|
|
$user = auth()->user();
|
|
if ($request->enabled) {
|
|
// Live-verify SMTP configuration by sending a test validation email
|
|
try {
|
|
\Illuminate\Support\Facades\Mail::to($user->email)->send(
|
|
new \App\Mail\Send2FACode('123456')
|
|
);
|
|
} catch (\Exception $e) {
|
|
\Illuminate\Support\Facades\Log::error("SMTP verification failed: " . $e->getMessage());
|
|
|
|
return back()->withErrors([
|
|
'password' => 'Cannot enable Email 2FA: Your SMTP mail configuration is invalid or not working. We tried to send a validation email but failed. Error: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
|
|
$user->update([
|
|
'email_2fa_enabled' => true,
|
|
'two_factor_secret' => null,
|
|
'two_factor_recovery_codes' => null,
|
|
'two_factor_confirmed_at' => null,
|
|
]);
|
|
} else {
|
|
$user->update([
|
|
'email_2fa_enabled' => false,
|
|
]);
|
|
}
|
|
|
|
$status = $request->enabled ? 'enabled' : 'disabled';
|
|
return back()->with('success', "Two-Factor Authentication via Email has been {$status} successfully.");
|
|
}
|
|
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
|
|
$type = $request->session()->get('two_factor_type', 'totp');
|
|
|
|
return Inertia::render('TwoFactor/Challenge', [
|
|
'type' => $type,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
$type = $request->session()->get('two_factor_type', 'totp');
|
|
|
|
if (!$userId) {
|
|
return redirect()->route('login');
|
|
}
|
|
|
|
$user = \App\Models\User::find($userId);
|
|
|
|
if (!$user) {
|
|
$request->session()->forget(['two_factor_user_id', 'two_factor_type']);
|
|
return redirect()->route('login');
|
|
}
|
|
|
|
if ($type === 'email') {
|
|
if (!$user->email_2fa_enabled || !$user->email_2fa_code) {
|
|
$request->session()->forget(['two_factor_user_id', 'two_factor_type']);
|
|
return redirect()->route('login');
|
|
}
|
|
|
|
if ($user->email_2fa_code !== $request->code || !$user->email_2fa_expires_at || $user->email_2fa_expires_at->isPast()) {
|
|
return back()->withErrors(['code' => 'Invalid or expired authentication code. Please try again.']);
|
|
}
|
|
|
|
// Code is valid! Clear it
|
|
$user->update([
|
|
'email_2fa_code' => null,
|
|
'email_2fa_expires_at' => null,
|
|
]);
|
|
} else {
|
|
if (!$user->two_factor_secret) {
|
|
$request->session()->forget(['two_factor_user_id', 'two_factor_type']);
|
|
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', 'two_factor_type']);
|
|
|
|
\Illuminate\Support\Facades\Auth::login($user);
|
|
$request->session()->regenerate();
|
|
|
|
return redirect()->intended(route('dashboard'));
|
|
}
|
|
|
|
/**
|
|
* Resend Email 2FA verification code.
|
|
*/
|
|
public function resendCode(Request $request)
|
|
{
|
|
if (!$request->session()->has('two_factor_user_id') || $request->session()->get('two_factor_type') !== 'email') {
|
|
return redirect()->route('login');
|
|
}
|
|
|
|
$userId = $request->session()->get('two_factor_user_id');
|
|
$user = \App\Models\User::find($userId);
|
|
|
|
if (!$user || !$user->email_2fa_enabled) {
|
|
return redirect()->route('login');
|
|
}
|
|
|
|
$code = str_pad(mt_rand(100000, 999999), 6, '0', STR_PAD_LEFT);
|
|
$user->update([
|
|
'email_2fa_code' => $code,
|
|
'email_2fa_expires_at' => now()->addMinutes(10),
|
|
]);
|
|
|
|
try {
|
|
\Illuminate\Support\Facades\Mail::to($user->email)->send(new \App\Mail\Send2FACode($code));
|
|
} catch (\Exception $e) {
|
|
\Illuminate\Support\Facades\Log::error("Failed to resend 2FA Email Code: " . $e->getMessage());
|
|
return back()->withErrors(['code' => 'Failed to send email. Please check SMTP configuration or try again.']);
|
|
}
|
|
|
|
return back()->with('success', 'A new verification code has been sent to your email.');
|
|
}
|
|
}
|