validate(); // Three auto-expiring counters, none of which can permanently lock the control plane // (all decay in minutes; the host CLI `clusev:reset-admin` is always available): // - pair (email+IP): the tight 5/min bucket (unchanged). // - ip : caps one source hammering many accounts AND the bcrypt CPU it burns. // - acct : an IP-independent backstop so a DISTRIBUTED (multi-IP) brute-force of the // single admin account is still capped instead of scaling linearly with IPs. $email = strtolower($this->email); $pairKey = 'login:'.md5($email.'|'.request()->ip()); $ipKey = 'login-ip:'.md5(request()->ip()); $acctKey = 'login-acct:'.md5($email); foreach ([[$pairKey, 5], [$ipKey, 20], [$acctKey, 30]] as [$k, $max]) { if (RateLimiter::tooManyAttempts($k, $max)) { throw ValidationException::withMessages([ 'email' => __('auth.too_many_attempts', ['seconds' => RateLimiter::availableIn($k)]), ]); } } $user = User::where('email', $this->email)->first(); // Evaluate the bcrypt FIRST (never short-circuit it): an unknown e-mail must still spend // one Hash::check against DUMMY_HASH, otherwise `! $user ||` would skip the hash and the // faster no-account response would re-open the enumeration timing oracle. $validPassword = Hash::check($this->password, $user?->password ?? self::DUMMY_HASH); if (! $user || ! $validPassword) { RateLimiter::hit($pairKey, 60); // 5 / minute RateLimiter::hit($ipKey, 60); // 20 / minute RateLimiter::hit($acctKey, 900); // 30 / 15 minutes throw ValidationException::withMessages(['email' => __('auth.invalid_credentials')]); } // Only the tight pair bucket is cleared on success; the broader ip/acct counters // decay on their own so one valid login can't reset a flood's budget. RateLimiter::clear($pairKey); // 2FA gate: hold the login until the TOTP code is verified. if ($user->hasTwoFactorEnabled()) { session()->put('2fa.user', $user->id); session()->put('2fa.remember', $this->remember); return $this->redirect(route('two-factor.challenge'), navigate: true); } Auth::login($user, $this->remember); session()->regenerate(); return $this->redirectIntended(route('dashboard'), navigate: true); } public function render() { return view('livewire.auth.login')->title(__('auth.title_login')); } }