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); } } /** 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); } }