From 513cadc5ceeca0a1bbed76169895ceee5b925b0e Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 28 Jul 2026 11:17:34 +0200 Subject: [PATCH] Confirm a console password against the console's own record --- app/Livewire/Admin/Mail.php | 5 +++ app/Livewire/Admin/Secrets.php | 5 +++ app/Livewire/Concerns/ConfirmsPassword.php | 18 ++++++++- app/Livewire/EditMailbox.php | 5 +++ .../Admin/PasswordConfirmationGuardTest.php | 40 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/Admin/PasswordConfirmationGuardTest.php diff --git a/app/Livewire/Admin/Mail.php b/app/Livewire/Admin/Mail.php index 2bacd7b..2e73439 100644 --- a/app/Livewire/Admin/Mail.php +++ b/app/Livewire/Admin/Mail.php @@ -50,6 +50,11 @@ class Mail extends Component */ public bool $usable = true; + protected function confirmationGuard(): string + { + return 'operator'; + } + public function mount(): void { $this->authorize('mail.manage'); diff --git a/app/Livewire/Admin/Secrets.php b/app/Livewire/Admin/Secrets.php index 07117ac..b90b01c 100644 --- a/app/Livewire/Admin/Secrets.php +++ b/app/Livewire/Admin/Secrets.php @@ -44,6 +44,11 @@ class Secrets extends Component /** Result of the last connection test, for display only. */ public ?array $check = null; + protected function confirmationGuard(): string + { + return 'operator'; + } + public function mount(): void { $this->authorize('secrets.manage'); diff --git a/app/Livewire/Concerns/ConfirmsPassword.php b/app/Livewire/Concerns/ConfirmsPassword.php index fc31c06..6d545a1 100644 --- a/app/Livewire/Concerns/ConfirmsPassword.php +++ b/app/Livewire/Concerns/ConfirmsPassword.php @@ -39,9 +39,23 @@ trait ConfirmsPassword return $at > 0 && (time() - $at) < $this->confirmationWindow(); } + /** + * Which guard's record this component confirms against. + * + * Overridden to 'operator' by the console components. A default of 'web' + * keeps the portal working unchanged — and an override is explicit, where + * sniffing the request would be one more thing to get wrong on a page that + * gates access to credentials. + */ + protected function confirmationGuard(): string + { + return 'web'; + } + public function confirmPassword(): void { - $key = 'confirm-password:'.auth()->id().'|'.request()->ip(); + $account = auth()->guard($this->confirmationGuard())->user(); + $key = 'confirm-password:'.$this->confirmationGuard().'|'.$account?->getAuthIdentifier().'|'.request()->ip(); if (RateLimiter::tooManyAttempts($key, 5)) { $this->addError('confirmablePassword', __('auth.throttle', [ @@ -51,7 +65,7 @@ trait ConfirmsPassword return; } - if (! Hash::check($this->confirmablePassword, auth()->user()->password)) { + if ($account === null || ! Hash::check($this->confirmablePassword, $account->password)) { RateLimiter::hit($key, 60); $this->addError('confirmablePassword', __('admin_settings.password_wrong')); $this->reset('confirmablePassword'); diff --git a/app/Livewire/EditMailbox.php b/app/Livewire/EditMailbox.php index 28909e9..bb9cca5 100644 --- a/app/Livewire/EditMailbox.php +++ b/app/Livewire/EditMailbox.php @@ -41,6 +41,11 @@ class EditMailbox extends ModalComponent /** Whether this mailbox logs in before it sends — Codex R15#4, P1b. */ public bool $authenticates = true; + protected function confirmationGuard(): string + { + return 'operator'; + } + public function mount(string $uuid): void { $this->authorize('mail.manage'); diff --git a/tests/Feature/Admin/PasswordConfirmationGuardTest.php b/tests/Feature/Admin/PasswordConfirmationGuardTest.php new file mode 100644 index 0000000..42976af --- /dev/null +++ b/tests/Feature/Admin/PasswordConfirmationGuardTest.php @@ -0,0 +1,40 @@ + config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)))); + +it('confirms a console password against the operator record, not a portal user', function () { + $operator = Operator::factory()->role('Owner')->create(['password' => 'operator-passwort']); + + Livewire::actingAs($operator, 'operator')->test(Secrets::class) + ->set('confirmablePassword', 'operator-passwort') + ->call('confirmPassword') + ->assertHasNoErrors(); +}); + +it('rejects a password that belongs to a portal user with the same address', function () { + // The trap: two tables, one address. Confirming against the wrong one would + // let a customer's password unlock the console. + $operator = Operator::factory()->role('Owner')->create([ + 'email' => 'doppelt@clupilot.test', 'password' => 'operator-passwort', + ]); + User::factory()->create(['email' => 'doppelt@clupilot.test', 'password' => 'kunden-passwort']); + + Livewire::actingAs($operator, 'operator')->test(Secrets::class) + ->set('confirmablePassword', 'kunden-passwort') + ->call('confirmPassword') + ->assertHasErrors('confirmablePassword'); +}); + +it('still confirms a portal password on the web guard', function () { + $user = User::factory()->create(['password' => 'kunden-passwort']); + + Livewire::actingAs($user)->test(App\Livewire\Settings::class) + ->set('confirmablePassword', 'kunden-passwort') + ->call('confirmPassword') + ->assertHasNoErrors(); +});