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(); foreach (['enableTwoFactor', 'confirmTwoFactor', 'disableTwoFactor', 'regenerateRecoveryCodes'] 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(PragmaRX\Google2FA\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); });