61 lines
1.9 KiB
PHP
61 lines
1.9 KiB
PHP
<?php
|
|
|
|
namespace App\Actions\Auth;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class LoginAction
|
|
{
|
|
/**
|
|
* @return array{status: 'authenticated', user: User}|array{status: 'requires_2fa', challenge_token: string}
|
|
*/
|
|
public function execute(Request $request, string $login, string $password): array
|
|
{
|
|
$throttleKey = 'login:'.$request->ip().':'.$login;
|
|
|
|
if (RateLimiter::tooManyAttempts($throttleKey, 5)) {
|
|
throw ValidationException::withMessages([
|
|
'login' => 'Too many login attempts. Please try again in a minute.',
|
|
]);
|
|
}
|
|
|
|
$user = User::where('email', $login)->orWhere('username', $login)->first();
|
|
|
|
if (! $user || ! Hash::check($password, $user->password)) {
|
|
RateLimiter::hit($throttleKey, 60);
|
|
|
|
// Generic message on purpose — never reveal whether the login
|
|
// identifier itself exists (no user enumeration).
|
|
throw ValidationException::withMessages([
|
|
'login' => 'These credentials do not match our records.',
|
|
]);
|
|
}
|
|
|
|
RateLimiter::clear($throttleKey);
|
|
|
|
if ($user->two_factor_enabled) {
|
|
$challengeToken = Str::random(40);
|
|
|
|
Cache::put("2fa_challenge:{$challengeToken}", [
|
|
'user_id' => $user->id,
|
|
'ip' => $request->ip(),
|
|
'attempts' => 0,
|
|
], now()->addMinutes(5));
|
|
|
|
return ['status' => 'requires_2fa', 'challenge_token' => $challengeToken];
|
|
}
|
|
|
|
Auth::login($user);
|
|
$request->session()->regenerate();
|
|
|
|
return ['status' => 'authenticated', 'user' => $user];
|
|
}
|
|
}
|