94 lines
2.8 KiB
PHP
94 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Auth;
|
|
|
|
use App\Models\Operator;
|
|
use App\Support\AdminArea;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Component;
|
|
|
|
/**
|
|
* The operator's two-factor step.
|
|
*
|
|
* Fortify's TOTP provider does the arithmetic — there is no second
|
|
* implementation of the algorithm here, only a second flow, because Fortify's
|
|
* own flow is bound to the web guard.
|
|
*/
|
|
#[Layout('layouts.portal')]
|
|
class OperatorTwoFactorChallenge extends Component
|
|
{
|
|
public string $code = '';
|
|
|
|
public string $recoveryCode = '';
|
|
|
|
public bool $usingRecovery = false;
|
|
|
|
public function mount(): void
|
|
{
|
|
// Reachable only between password and code. Landing here directly is a
|
|
// dead end, not a way in.
|
|
abort_if(! session()->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']);
|
|
|
|
OperatorLogin::completeLogin($operator, $remember);
|
|
$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');
|
|
}
|
|
}
|