CluPilotCloud/tests/Feature/CustomerTwoFactorTest.php

151 lines
5.4 KiB
PHP

<?php
use App\Livewire\ConfirmDisableTwoFactor;
use App\Livewire\Settings as PortalSettings;
use App\Models\Customer;
use Livewire\Livewire;
use PragmaRX\Google2FA\Google2FA;
/**
* Customers setting up two-factor themselves.
*
* The endpoints existed; only the screen was missing. What matters here is the
* gate: every action is re-checked against a recent password confirmation on
* the server, because a Livewire action is reachable by anyone who can post to
* /livewire/update, and "the button was not on screen" has never stopped anyone.
*/
function portalUser(): App\Models\User
{
$customer = Customer::factory()->create(['status' => 'active']);
$user = $customer->ensureUser();
$user->forceFill(['password' => bcrypt('password')])->save();
return $user;
}
it('refuses every two-factor action without a recent password confirmation', function () {
$user = portalUser();
// onTwoFaDisableConfirmed is the R23 forwarding listener
// ConfirmDisableTwoFactor reaches — it must lead straight into the same
// guard as disableTwoFactor, not around it.
foreach (['enableTwoFactor', 'confirmTwoFactor', 'disableTwoFactor', 'regenerateRecoveryCodes', 'onTwoFaDisableConfirmed'] as $action) {
Livewire::actingAs($user)
->test(PortalSettings::class)
->call($action)
->assertForbidden();
}
expect($user->refresh()->two_factor_secret)->toBeNull();
});
it('takes a password, then sets two-factor up and confirms it with a real code', function () {
$user = portalUser();
$page = Livewire::actingAs($user)
->test(PortalSettings::class)
->set('confirmablePassword', 'password')
->call('confirmPassword')
->assertHasNoErrors();
$page->call('enableTwoFactor');
expect($user->refresh()->two_factor_secret)->not->toBeNull()
// Not on until a code has been accepted: someone who scans nothing and
// closes the tab must not be locked out of their own account.
->and($user->two_factor_confirmed_at)->toBeNull();
$code = app(Google2FA::class)->getCurrentOtp(decrypt($user->two_factor_secret));
$page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors();
expect($user->refresh()->two_factor_confirmed_at)->not->toBeNull()
// Shown once, here, because this is the only moment anyone reads them.
->and($page->get('recoveryCodes'))->toBeArray()->not->toBeEmpty();
});
it('rejects a wrong code instead of switching it on', function () {
$user = portalUser();
Livewire::actingAs($user)
->test(PortalSettings::class)
->set('confirmablePassword', 'password')
->call('confirmPassword')
->call('enableTwoFactor')
->set('twoFactorCode', '000000')
->call('confirmTwoFactor')
->assertHasErrors('twoFactorCode');
expect($user->refresh()->two_factor_confirmed_at)->toBeNull();
});
it('does not accept a wrong password, and says so without hinting', function () {
$user = portalUser();
Livewire::actingAs($user)
->test(PortalSettings::class)
->set('confirmablePassword', 'falsch')
->call('confirmPassword')
->assertHasErrors('confirmablePassword')
->call('enableTwoFactor')
->assertForbidden();
});
it('never puts the secret where the browser can see it', function () {
// A Livewire property travels to the browser and back in the component
// snapshot. The QR image is derived from the secret; the secret is not.
$user = portalUser();
$page = Livewire::actingAs($user)
->test(PortalSettings::class)
->set('confirmablePassword', 'password')
->call('confirmPassword')
->call('enableTwoFactor');
$secret = decrypt($user->refresh()->two_factor_secret);
expect(json_encode($page->snapshot))->not->toContain($secret);
});
// R23: confirmation moved from wire:confirm to ConfirmDisableTwoFactor, which
// dispatches back to Settings::disableTwoFactor() instead of mutating
// anything itself.
it('confirms disabling two-factor through the modal without turning it off itself', function () {
$user = portalUser();
$page = Livewire::actingAs($user)
->test(PortalSettings::class)
->set('confirmablePassword', 'password')
->call('confirmPassword')
->call('enableTwoFactor');
$code = app(Google2FA::class)->getCurrentOtp(decrypt($user->refresh()->two_factor_secret));
$page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors();
Livewire::actingAs($user)
->test(ConfirmDisableTwoFactor::class)
->call('confirm')
->assertDispatched('twofa-disable-confirmed');
expect($user->refresh()->two_factor_confirmed_at)->not->toBeNull();
});
it('disables two-factor once the page receives the confirmed event', function () {
$user = portalUser();
$page = Livewire::actingAs($user)
->test(PortalSettings::class)
->set('confirmablePassword', 'password')
->call('confirmPassword')
->call('enableTwoFactor');
$code = app(Google2FA::class)->getCurrentOtp(decrypt($user->refresh()->two_factor_secret));
$page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors();
$page->call('onTwoFaDisableConfirmed')->assertHasNoErrors();
expect($user->refresh()->two_factor_confirmed_at)->toBeNull()
->and($user->two_factor_secret)->toBeNull();
});