feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
<?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.'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user