has('operator.2fa.id'), 403); } public function verify(): void { $operator = Operator::find(session('operator.2fa.id')); abort_if($operator === null, 403); $key = 'operator-2fa:'.$operator->id.'|'.request()->ip(); if (RateLimiter::tooManyAttempts($key, 5)) { throw ValidationException::withMessages([ 'code' => __('auth.throttle', ['seconds' => RateLimiter::availableIn($key)]), ]); } if (! $this->passes($operator)) { RateLimiter::hit($key, 60); $this->reset('code', 'recoveryCode'); throw ValidationException::withMessages(['code' => __('auth.twofa_invalid')]); } RateLimiter::clear($key); $remember = (bool) session('operator.2fa.remember', false); session()->forget(['operator.2fa.id', 'operator.2fa.remember']); if (! OperatorLogin::completeLogin($operator, $remember)) { // Disabled, or lost every console role, in the window between // the password step and this one. The code just entered may be // perfectly correct — the account behind it no longer is, so // reusing "wrong code" would mislead. The pending challenge is // spent either way (already forgotten above): no retry loop // against a session that can never complete now. throw ValidationException::withMessages(['code' => __('auth.not_an_operator')]); } $this->redirect(AdminArea::home(), navigate: false); } private function passes(Operator $operator): bool { if ($this->usingRecovery) { $codes = json_decode(decrypt($operator->two_factor_recovery_codes), true) ?: []; if (! in_array($this->recoveryCode, $codes, true)) { return false; } // A recovery code is single use — leaving it valid turns a one-time // escape hatch into a second password. $operator->forceFill([ 'two_factor_recovery_codes' => encrypt(json_encode(array_values( array_diff($codes, [$this->recoveryCode]) ))), ])->save(); return true; } return app(TwoFactorAuthenticationProvider::class) ->verify(decrypt($operator->two_factor_secret), $this->code); } public function render() { return view('livewire.auth.operator-two-factor-challenge'); } }