135 lines
4.6 KiB
PHP
135 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Auth;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Mail\TwoFactorOtp;
|
|
use App\Models\UserTrustedDevice;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Session;
|
|
use Illuminate\Support\Str;
|
|
|
|
class TwoFactorController extends Controller
|
|
{
|
|
public function index()
|
|
{
|
|
if (! Session::has('auth.2fa_user_id')) {
|
|
return redirect()->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.'));
|
|
}
|
|
}
|