mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
Hash recovery codes
This commit is contained in:
+17
-2
@@ -20,6 +20,7 @@ use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use PragmaRX\Google2FAQRCode\Google2FA;
|
||||
|
||||
@@ -89,7 +90,16 @@ class GoogleTwoFactorAuthentication implements MultiFactorAuthenticationProvider
|
||||
*/
|
||||
public function saveRecoveryCodes(HasGoogleTwoFactorAuthenticationRecovery $user, ?array $codes): void
|
||||
{
|
||||
$user->saveGoogleTwoFactorAuthenticationRecoveryCodes($codes);
|
||||
if (! is_array($codes)) {
|
||||
$user->saveGoogleTwoFactorAuthenticationRecoveryCodes(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user->saveGoogleTwoFactorAuthenticationRecoveryCodes(array_map(
|
||||
fn (string $code): string => Hash::make($code),
|
||||
$codes,
|
||||
));
|
||||
}
|
||||
|
||||
public function generateSecret(): string
|
||||
@@ -135,8 +145,13 @@ class GoogleTwoFactorAuthentication implements MultiFactorAuthenticationProvider
|
||||
$user ??= Filament::auth()->user();
|
||||
|
||||
/** @var HasGoogleTwoFactorAuthenticationRecovery $user */
|
||||
foreach ($this->getRecoveryCodes($user) as $hashedRecoveryCode) {
|
||||
if (Hash::check($recoveryCode, $hashedRecoveryCode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return in_array($recoveryCode, $this->getRecoveryCodes($user));
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ use Filament\MultiFactorAuthentication\EmailCode\EmailCodeAuthentication;
|
||||
use Filament\MultiFactorAuthentication\GoogleTwoFactor\GoogleTwoFactorAuthentication;
|
||||
use Filament\Tests\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserFactory extends Factory
|
||||
@@ -32,13 +33,19 @@ class UserFactory extends Factory
|
||||
]);
|
||||
}
|
||||
|
||||
public function hasGoogleTwoFactorAuthentication(): self
|
||||
/**
|
||||
* @param ?array<string> $recoveryCodes
|
||||
*/
|
||||
public function hasGoogleTwoFactorAuthentication(?array $recoveryCodes = null): self
|
||||
{
|
||||
$googleTwoFactorAuthentication = GoogleTwoFactorAuthentication::make();
|
||||
|
||||
return $this->state(fn (): array => [
|
||||
'google_two_factor_authentication_secret' => $googleTwoFactorAuthentication->generateSecret(),
|
||||
'google_two_factor_authentication_recovery_codes' => $googleTwoFactorAuthentication->generateRecoveryCodes(),
|
||||
'google_two_factor_authentication_recovery_codes' => array_map(
|
||||
fn (string $code): string => Hash::make($code),
|
||||
$recoveryCodes ?? $googleTwoFactorAuthentication->generateRecoveryCodes(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -16,8 +16,12 @@ uses(TestCase::class);
|
||||
beforeEach(function () {
|
||||
Filament::setCurrentPanel('google-two-factor-authentication');
|
||||
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$this->recoveryCodes = $googleTwoFactorAuthentication->generateRecoveryCodes();
|
||||
|
||||
actingAs(User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->hasGoogleTwoFactorAuthentication($this->recoveryCodes)
|
||||
->create());
|
||||
});
|
||||
|
||||
@@ -58,8 +62,6 @@ it('can remove authentication when valid challenge code is used', function () {
|
||||
it('can remove authentication when a valid recovery code is used', function () {
|
||||
$user = auth()->user();
|
||||
|
||||
$recoveryCodes = $user->getGoogleTwoFactorAuthenticationRecoveryCodes();
|
||||
|
||||
expect($user->hasGoogleTwoFactorAuthentication())
|
||||
->toBeTrue();
|
||||
|
||||
@@ -76,7 +78,7 @@ it('can remove authentication when a valid recovery code is used', function () {
|
||||
->callAction(TestAction::make('useRecoveryCode')
|
||||
->schemaComponent('mountedActionSchema0.code'))
|
||||
->setActionData([
|
||||
'recoveryCode' => Arr::first($recoveryCodes),
|
||||
'recoveryCode' => Arr::first($this->recoveryCodes),
|
||||
])
|
||||
->callMountedAction()
|
||||
->assertHasNoActionErrors();
|
||||
@@ -237,8 +239,6 @@ it('will not remove authentication with a recovery code if recovery is disabled'
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$recoveryCodes = $user->getGoogleTwoFactorAuthenticationRecoveryCodes();
|
||||
|
||||
expect($user->hasGoogleTwoFactorAuthentication())
|
||||
->toBeTrue();
|
||||
|
||||
@@ -253,7 +253,7 @@ it('will not remove authentication with a recovery code if recovery is disabled'
|
||||
->callAction(
|
||||
TestAction::make('removeGoogleTwoFactorAuthentication')
|
||||
->schemaComponent('form.google_two_factor.removeGoogleTwoFactorAuthenticationAction'),
|
||||
['recoveryCode' => Arr::first($recoveryCodes)],
|
||||
['recoveryCode' => Arr::first($this->recoveryCodes)],
|
||||
)
|
||||
->assertHasActionErrors();
|
||||
|
||||
+7
-2
@@ -6,6 +6,7 @@ use Filament\Pages\Auth\EditProfile;
|
||||
use Filament\Tests\Models\User;
|
||||
use Filament\Tests\TestCase;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use function Filament\Tests\livewire;
|
||||
@@ -94,8 +95,12 @@ it('can save the secret and recovery codes to the user when the action is submit
|
||||
|
||||
expect($user->getGoogleTwoFactorAuthenticationRecoveryCodes())
|
||||
->toBeArray()
|
||||
->toHaveCount(8)
|
||||
->toBe($recoveryCodes);
|
||||
->toHaveCount(8);
|
||||
|
||||
foreach ($user->getGoogleTwoFactorAuthenticationRecoveryCodes() as $hashedRecoveryCode) {
|
||||
expect(Hash::check(array_shift($recoveryCodes), $hashedRecoveryCode))
|
||||
->toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
it('will not set up authentication when an invalid code is used', function () {
|
||||
+4
-4
@@ -98,7 +98,7 @@ it('will authenticate the user after a valid recovery code is used', function ()
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->hasGoogleTwoFactorAuthentication($recoveryCodes = $googleTwoFactorAuthentication->generateRecoveryCodes())
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
@@ -113,7 +113,7 @@ it('will authenticate the user after a valid recovery code is used', function ()
|
||||
->schemaComponent("multiFactorChallengeForm.{$googleTwoFactorAuthentication->getId()}.code"))
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'recoveryCode' => Arr::random($googleTwoFactorAuthentication->getRecoveryCodes($userToAuthenticate)),
|
||||
'recoveryCode' => Arr::random($recoveryCodes),
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
@@ -321,7 +321,7 @@ it('will not authenticate the user with a valid recovery code if recovery is dis
|
||||
->recoverable(false);
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->hasGoogleTwoFactorAuthentication($recoveryCodes = $googleTwoFactorAuthentication->generateRecoveryCodes())
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
@@ -334,7 +334,7 @@ it('will not authenticate the user with a valid recovery code if recovery is dis
|
||||
->assertNoRedirect()
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'recoveryCode' => Arr::random($googleTwoFactorAuthentication->getRecoveryCodes($userToAuthenticate)),
|
||||
'recoveryCode' => Arr::random($recoveryCodes),
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
Reference in New Issue
Block a user