feat: implement premium Email 2FA authentication integrated with auth flow

This commit is contained in:
2026-05-21 21:46:53 +07:00
parent a0673129ee
commit 0d083765ff
50 changed files with 543 additions and 162 deletions
@@ -38,6 +38,29 @@ class AuthenticatedSessionController extends Controller
// If user has 2FA enabled, redirect to challenge screen
if ($user->two_factor_confirmed_at && $user->two_factor_secret) {
$request->session()->put('two_factor_user_id', $user->id);
$request->session()->put('two_factor_type', 'totp');
Auth::guard('web')->logout();
$request->session()->forget('password_hash_web');
return redirect()->route('two-factor.challenge');
}
// If user has Email 2FA enabled, redirect to email challenge
if ($user->email_2fa_enabled) {
$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 send 2FA Email Code: " . $e->getMessage());
}
$request->session()->put('two_factor_user_id', $user->id);
$request->session()->put('two_factor_type', 'email');
Auth::guard('web')->logout();
$request->session()->forget('password_hash_web');
@@ -41,6 +41,7 @@ class SettingsController extends Controller
'enabled' => $twoFactorEnabled,
'qr_code' => $qrCode,
'secret' => $secret,
'email_enabled' => (bool)$user->email_2fa_enabled,
'recovery_codes' => $user->two_factor_recovery_codes
? json_decode($user->two_factor_recovery_codes, true)
: [],
+95 -16
View File
@@ -103,6 +103,26 @@ class TwoFactorController extends Controller
return back()->with('success', 'Two-Factor Authentication has been disabled.');
}
/**
* Enable/Disable Email 2FA.
*/
public function toggleEmail(Request $request)
{
$request->validate([
'password' => 'required|current_password',
'enabled' => 'required|boolean',
]);
$user = auth()->user();
$user->update([
'email_2fa_enabled' => $request->enabled,
]);
$status = $request->enabled ? 'enabled' : 'disabled';
return back()->with('success', "Two-Factor Authentication via Email has been {$status} successfully.");
}
/**
* Regenerate recovery codes.
*/
@@ -126,7 +146,11 @@ class TwoFactorController extends Controller
return redirect()->route('login');
}
return Inertia::render('TwoFactor/Challenge');
$type = $request->session()->get('two_factor_type', 'totp');
return Inertia::render('TwoFactor/Challenge', [
'type' => $type,
]);
}
/**
@@ -139,6 +163,7 @@ class TwoFactorController extends Controller
]);
$userId = $request->session()->get('two_factor_user_id');
$type = $request->session()->get('two_factor_type', 'totp');
if (!$userId) {
return redirect()->route('login');
@@ -146,32 +171,86 @@ class TwoFactorController extends Controller
$user = \App\Models\User::find($userId);
if (!$user || !$user->two_factor_secret) {
$request->session()->forget('two_factor_user_id');
if (!$user) {
$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 ($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 (!$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 ($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.']);
}
}
if (!$valid) {
return back()->withErrors(['code' => 'Invalid code. Please try again.']);
}
$request->session()->forget('two_factor_user_id');
$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.');
}
}