pendingResolved) { $this->pendingUser = User::find(session('2fa.user')); $this->pendingResolved = true; } return $this->pendingUser; } /** Whether the PENDING user has TOTP — the authenticator code field renders only then. */ public function getPendingHasTotpProperty(): bool { return (bool) $this->pendingUser()?->hasTotp(); } /** Whether to offer the security-key option for the pending user (domain+HTTPS + has a key). */ public function webauthnAvailable(): bool { $user = $this->pendingUser(); return $user !== null && app(WebauthnService::class)->available() && $user->hasWebauthnCredentials(); } /** * Two auto-expiring 2FA brute-force buckets, keyed on the SERVER-side pending user id * (not client-controlled) — a tight per-(user+IP) one plus an IP-independent per-user * backstop, so a distributed (multi-IP) brute-force of the code is still capped. Both * decay in minutes (never a permanent lockout; clusev:reset-admin stays open). * * @return array key => [maxAttempts, decaySeconds] */ protected function rateLimitBuckets(): array { $uid = (string) session('2fa.user'); return [ 'two-factor:'.md5($uid.'|'.request()->ip()) => [5, 60], 'two-factor-acct:'.md5($uid) => [20, 900], ]; } protected function assertNotRateLimited(): void { foreach ($this->rateLimitBuckets() as $key => [$max]) { if (RateLimiter::tooManyAttempts($key, $max)) { throw ValidationException::withMessages([ 'code' => __('auth.too_many_attempts', ['seconds' => RateLimiter::availableIn($key)]), ]); } } } protected function hitRateLimit(): void { foreach ($this->rateLimitBuckets() as $key => [, $decay]) { RateLimiter::hit($key, $decay); } } protected function clearRateLimit(): void { foreach (array_keys($this->rateLimitBuckets()) as $key) { RateLimiter::clear($key); } } /** A failed 2FA attempt: throttle (Layer 1) + feed the persistent ban (Layer 2) + audit. */ protected function recordFailedAttempt(string $reason): void { $this->hitRateLimit(); $ip = request()->ip(); app(BruteforceGuard::class)->record($ip, $reason); AuditEvent::create([ 'user_id' => null, 'actor' => 'system', 'action' => 'auth.2fa_failed', 'target' => (string) session('2fa.user'), 'ip' => $ip, ]); } /** Log the verified pending user in and finish the challenge (shared success tail). */ protected function completeLogin(User $user): mixed { $remember = (bool) session('2fa.remember'); session()->forget(['2fa.user', '2fa.remember']); Auth::login($user, $remember); session()->regenerate(); return $this->redirectIntended(route('dashboard'), navigate: true); } }