route('login'); } // Check if device is already trusted (Redirect to verify immediately with cookie token) $userId = Session::get('auth.2fa_user_id'); $deviceId = request()->cookie('2fa_trust_device'); if ($deviceId && str_contains($deviceId, '|')) { $parts = explode('|', $deviceId, 2); if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) { return view('auth.two-factor'); } [$uuid, $secret] = $parts; $trust = UserTrustedDevice::where('user_id', $userId) ->where('device_id', $uuid) ->where('expires_at', '>', now()) ->first(); if ($trust && hash_equals($trust->token, hash('sha256', $secret))) { // Auto login and skip 2FA view $remember = Session::get('auth.2fa_remember', false); Auth::loginUsingId($userId, $remember); Session::forget(['auth.2fa_user_id', 'auth.2fa_code', 'auth.2fa_expires_at', 'auth.2fa_remember']); session()->regenerate(); return redirect()->intended(route('dashboard', absolute: false)); } } return view('auth.two-factor'); } public function verify(Request $request) { $request->validate([ 'code' => 'required|string|size:6', 'trust_device' => 'nullable|boolean', ]); $userId = Session::get('auth.2fa_user_id'); $storedCode = Session::get('auth.2fa_code'); $expiresAt = Session::get('auth.2fa_expires_at'); if (! $userId || ! $storedCode || ! hash_equals((string) $storedCode, (string) $request->code)) { return back()->with('error', __('Invalid verification code.')); } if (! $expiresAt || now()->timestamp > $expiresAt) { Session::forget(['auth.2fa_user_id', 'auth.2fa_code', 'auth.2fa_expires_at', 'auth.2fa_remember']); return redirect()->route('login')->with('error', __('Verification code has expired. Please log in again.')); } // Handle Trust Device if ($request->boolean('trust_device')) { $this->issueTrustToken($userId); } // Login user $remember = Session::get('auth.2fa_remember', false); Auth::loginUsingId($userId, $remember); // Clear 2FA session then regenerate to prevent fixation Session::forget(['auth.2fa_user_id', 'auth.2fa_code', 'auth.2fa_expires_at', 'auth.2fa_remember']); session()->regenerate(); return redirect()->intended(route('dashboard', absolute: false)); } protected function issueTrustToken($userId) { $deviceId = Str::uuid(); $token = Str::random(64); $days = get_setting('two_factor_trust_days', 30); UserTrustedDevice::create([ 'user_id' => $userId, 'device_id' => $deviceId, 'token' => hash('sha256', $token), 'expires_at' => now()->addDays($days), ]); // Queue cookie with both UUID and Secret cookie()->queue( '2fa_trust_device', $deviceId.'|'.$token, $days * 24 * 60, null, null, true, true // secure, httpOnly ); } public static function generateAndSendOtp($user) { $otp = str_pad((string) (hexdec(bin2hex(random_bytes(3))) % 1000000), 6, '0', STR_PAD_LEFT); $expiresAt = now()->addMinutes(10)->timestamp; Session::put('auth.2fa_user_id', $user->id); Session::put('auth.2fa_code', $otp); Session::put('auth.2fa_expires_at', $expiresAt); try { $request = request(); Mail::to($user->email)->send(new TwoFactorOtp( otp: $otp, userName: $user->name, ipAddress: $request->ip(), userAgent: $request->userAgent(), )); } catch (\Exception $e) { \Log::error('Failed to send 2FA Email: '.$e->getMessage()); } session()->flash('info', __('Verification code has been sent to your email.')); } }