mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
Add Google login tests
This commit is contained in:
@@ -23,8 +23,7 @@ class OneTimeCodeInput extends Field
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->rule('numeric');
|
||||
$this->rule('integer');
|
||||
$this->rule('numeric'); // Integer validation does not allow leading zeros.
|
||||
$this->rule(static fn (OneTimeCodeInput $component): string => "digits:{$component->getLength()}");
|
||||
}
|
||||
|
||||
|
||||
@@ -60,11 +60,16 @@ class EmailCodeAuthentication implements HasBeforeChallengeHook, MultiFactorAuth
|
||||
}
|
||||
|
||||
$user->notify(app($this->getCodeNotification(), [
|
||||
'code' => $this->google2FA->getCurrentOtp($secret ?? $this->getSecret($user)),
|
||||
'code' => $this->getCurrentCode($user, $secret),
|
||||
'codeWindow' => $this->getCodeWindow(),
|
||||
]));
|
||||
}
|
||||
|
||||
public function getCurrentCode(HasEmailCodeAuthentication $user, ?string $secret = null): string
|
||||
{
|
||||
return $this->google2FA->getCurrentOtp($secret ?? $this->getSecret($user));
|
||||
}
|
||||
|
||||
public function getSecret(HasEmailCodeAuthentication $user): string
|
||||
{
|
||||
return $user->getEmailCodeAuthenticationSecret();
|
||||
|
||||
+5
@@ -97,6 +97,11 @@ class GoogleTwoFactorAuthentication implements MultiFactorAuthenticationProvider
|
||||
return $this->google2FA->generateSecretKey();
|
||||
}
|
||||
|
||||
public function getCurrentCode(HasGoogleTwoFactorAuthentication $user, ?string $secret = null): string
|
||||
{
|
||||
return $this->google2FA->getCurrentOtp($secret ?? $this->getSecret($user));
|
||||
}
|
||||
|
||||
public function generateQRCodeDataUri(string $secret): string
|
||||
{
|
||||
/** @var HasGoogleTwoFactorAuthentication $user */
|
||||
|
||||
@@ -33,7 +33,7 @@ use Livewire\Attributes\Locked;
|
||||
/**
|
||||
* @property-read Action $registerAction
|
||||
* @property-read Schema $form
|
||||
* @property-read Schema $multiFactorForm
|
||||
* @property-read Schema $multiFactorChallengeForm
|
||||
*/
|
||||
class Login extends SimplePage
|
||||
{
|
||||
@@ -83,7 +83,7 @@ class Login extends SimplePage
|
||||
filled($this->userUndertakingMultiFactorAuthentication) &&
|
||||
(decrypt($this->userUndertakingMultiFactorAuthentication) === $user->getAuthIdentifier())
|
||||
) {
|
||||
$this->multiFactorForm->validate();
|
||||
$this->multiFactorChallengeForm->validate();
|
||||
} else {
|
||||
foreach (Filament::getMultiFactorAuthenticationProviders() as $multiFactorAuthenticationProvider) {
|
||||
if (! $multiFactorAuthenticationProvider->isEnabled($user)) {
|
||||
@@ -98,7 +98,7 @@ class Login extends SimplePage
|
||||
}
|
||||
|
||||
if (filled($this->userUndertakingMultiFactorAuthentication)) {
|
||||
$this->multiFactorForm->fill();
|
||||
$this->multiFactorChallengeForm->fill();
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class Login extends SimplePage
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function multiFactorForm(Schema $form): Schema
|
||||
public function multiFactorChallengeForm(Schema $form): Schema
|
||||
{
|
||||
return $form;
|
||||
}
|
||||
@@ -170,7 +170,7 @@ class Login extends SimplePage
|
||||
])
|
||||
->statePath('data'),
|
||||
),
|
||||
'multiFactorForm' => $this->multiFactorForm(
|
||||
'multiFactorChallengeForm' => $this->multiFactorChallengeForm(
|
||||
$this->makeSchema()
|
||||
->schema(function (): array {
|
||||
if (blank($this->userUndertakingMultiFactorAuthentication)) {
|
||||
@@ -262,7 +262,7 @@ class Login extends SimplePage
|
||||
/**
|
||||
* @return array<Action | ActionGroup>
|
||||
*/
|
||||
protected function getMultiFactorFormActions(): array
|
||||
protected function getMultiFactorChallengeFormActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getMultiFactorAuthenticateFormAction(),
|
||||
@@ -281,7 +281,7 @@ class Login extends SimplePage
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function hasFullWidthMultiFactorFormActions(): bool
|
||||
protected function hasFullWidthMultiFactorChallengeFormActions(): bool
|
||||
{
|
||||
return $this->hasFullWidthFormActions();
|
||||
}
|
||||
@@ -317,7 +317,7 @@ class Login extends SimplePage
|
||||
->components([
|
||||
RenderHook::make(PanelsRenderHook::AUTH_LOGIN_FORM_BEFORE),
|
||||
$this->getFormContentComponent(),
|
||||
$this->getMultiFactorFormContentComponent(),
|
||||
$this->getMultiFactorChallengeFormContentComponent(),
|
||||
RenderHook::make(PanelsRenderHook::AUTH_LOGIN_FORM_AFTER),
|
||||
]);
|
||||
}
|
||||
@@ -333,18 +333,18 @@ class Login extends SimplePage
|
||||
->visible(fn (): bool => blank($this->userUndertakingMultiFactorAuthentication));
|
||||
}
|
||||
|
||||
public function getMultiFactorFormContentComponent(): Component
|
||||
public function getMultiFactorChallengeFormContentComponent(): Component
|
||||
{
|
||||
return Form::make([NestedSchema::make('multiFactorForm')])
|
||||
->id('multiFactorForm')
|
||||
return Form::make([NestedSchema::make('multiFactorChallengeForm')])
|
||||
->id('multiFactorChallengeForm')
|
||||
->livewireSubmitHandler('authenticate')
|
||||
->footer(FormActionsDecorations::make($this->getMultiFactorFormActions())
|
||||
->alignment($this->getMultiFactorFormActionsAlignment())
|
||||
->fullWidth($this->hasFullWidthMultiFactorFormActions()))
|
||||
->footer(FormActionsDecorations::make($this->getMultiFactorChallengeFormActions())
|
||||
->alignment($this->getMultiFactorChallengeFormActionsAlignment())
|
||||
->fullWidth($this->hasFullWidthMultiFactorChallengeFormActions()))
|
||||
->visible(fn (): bool => filled($this->userUndertakingMultiFactorAuthentication));
|
||||
}
|
||||
|
||||
public function getMultiFactorFormActionsAlignment(): string | Alignment
|
||||
public function getMultiFactorChallengeFormActionsAlignment(): string | Alignment
|
||||
{
|
||||
return $this->getFormActionsAlignment();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Filament\Tests\Database\Factories;
|
||||
|
||||
use Filament\MultiFactorAuthentication\EmailCode\EmailCodeAuthentication;
|
||||
use Filament\MultiFactorAuthentication\GoogleTwoFactor\GoogleTwoFactorAuthentication;
|
||||
use Filament\Tests\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -20,4 +22,23 @@ class UserFactory extends Factory
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
public function hasEmailCodeAuthentication(): self
|
||||
{
|
||||
$emailCodeAuthentication = EmailCodeAuthentication::make();
|
||||
|
||||
return $this->state(fn (): array => [
|
||||
'email_code_authentication_secret' => $emailCodeAuthentication->generateSecret(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function hasGoogleTwoFactorAuthentication(): self
|
||||
{
|
||||
$googleTwoFactorAuthentication = GoogleTwoFactorAuthentication::make();
|
||||
|
||||
return $this->state(fn (): array => [
|
||||
'google_two_factor_authentication_secret' => $googleTwoFactorAuthentication->generateSecret(),
|
||||
'google_two_factor_authentication_recovery_codes' => $googleTwoFactorAuthentication->generateRecoveryCodes(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->after('password', function (Blueprint $table) {
|
||||
$table->string('email_code_authentication_secret')->nullable();
|
||||
$table->string('google_two_factor_authentication_secret')->nullable();
|
||||
$table->text('google_two_factor_authentication_recovery_codes')->nullable();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'email_code_authentication_secret',
|
||||
'google_two_factor_authentication_secret',
|
||||
'google_two_factor_authentication_recovery_codes',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Tests;
|
||||
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\MultiFactorAuthentication\EmailCode\EmailCodeAuthentication;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class EmailCodeAuthenticationPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->id('email-code-authentication')
|
||||
->path('email-code-authentication')
|
||||
->login()
|
||||
->multiFactorAuthentication(EmailCodeAuthentication::make())
|
||||
->resources([])
|
||||
->pages([])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Tests;
|
||||
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\MultiFactorAuthentication\GoogleTwoFactor\GoogleTwoFactorAuthentication;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class GoogleTwoFactorAuthenticationPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->id('google-two-factor-authentication')
|
||||
->path('google-two-factor-authentication')
|
||||
->login()
|
||||
->multiFactorAuthentication(GoogleTwoFactorAuthentication::make()->recoverable())
|
||||
->resources([])
|
||||
->pages([])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ namespace Filament\Tests\Models;
|
||||
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Models\Contracts\HasTenants;
|
||||
use Filament\MultiFactorAuthentication\GoogleTwoFactor\Contracts\HasGoogleTwoFactorAuthentication;
|
||||
use Filament\MultiFactorAuthentication\GoogleTwoFactor\Contracts\HasGoogleTwoFactorAuthenticationRecovery;
|
||||
use Filament\Panel;
|
||||
use Filament\Tests\Database\Factories\UserFactory;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
@@ -14,7 +16,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class User extends Authenticatable implements FilamentUser, HasTenants, MustVerifyEmail
|
||||
class User extends Authenticatable implements FilamentUser, HasGoogleTwoFactorAuthentication, HasGoogleTwoFactorAuthenticationRecovery, HasTenants, MustVerifyEmail
|
||||
{
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
@@ -24,11 +26,24 @@ class User extends Authenticatable implements FilamentUser, HasTenants, MustVeri
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'google_two_factor_authentication_secret',
|
||||
'google_two_factor_authentication_recovery_codes',
|
||||
'email_code_authentication_secret',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'google_two_factor_authentication_secret' => 'encrypted',
|
||||
'google_two_factor_authentication_recovery_codes' => 'encrypted:array',
|
||||
'email_code_authentication_secret' => 'encrypted',
|
||||
];
|
||||
|
||||
public function canAccessPanel(Panel $panel): bool
|
||||
{
|
||||
return in_array($panel->getId(), ['admin', 'slugs']);
|
||||
return in_array($panel->getId(), ['admin', 'slugs', 'google-two-factor-authentication', 'email-code-authentication']);
|
||||
}
|
||||
|
||||
public function posts(): HasMany
|
||||
@@ -50,4 +65,52 @@ class User extends Authenticatable implements FilamentUser, HasTenants, MustVeri
|
||||
{
|
||||
return Team::all();
|
||||
}
|
||||
|
||||
public function hasGoogleTwoFactorAuthentication(): bool
|
||||
{
|
||||
return filled($this->google_two_factor_authentication_secret);
|
||||
}
|
||||
|
||||
public function getGoogleTwoFactorAuthenticationSecret(): string
|
||||
{
|
||||
return $this->google_two_factor_authentication_secret;
|
||||
}
|
||||
|
||||
public function saveGoogleTwoFactorAuthenticationSecret(?string $secret): void
|
||||
{
|
||||
$this->google_two_factor_authentication_secret = $secret;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function getGoogleTwoFactorAuthenticationRecoveryCodes(): array
|
||||
{
|
||||
return $this->google_two_factor_authentication_recovery_codes;
|
||||
}
|
||||
|
||||
public function saveGoogleTwoFactorAuthenticationRecoveryCodes(?array $codes): void
|
||||
{
|
||||
$this->google_two_factor_authentication_recovery_codes = $codes;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function getGoogleTwoFactorAuthenticationHolderName(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function hasEmailCodeAuthentication(): bool
|
||||
{
|
||||
return filled($this->email_code_authentication_secret);
|
||||
}
|
||||
|
||||
public function getEmailCodeAuthenticationSecret(): string
|
||||
{
|
||||
return $this->email_code_authentication_secret;
|
||||
}
|
||||
|
||||
public function saveEmailCodeAuthenticationSecret(?string $secret): void
|
||||
{
|
||||
$this->email_code_authentication_secret = $secret;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
use Filament\Actions\Testing\Fixtures\TestAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Pages\Auth\Login;
|
||||
use Filament\Tests\Models\User;
|
||||
use Filament\Tests\TestCase;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use function Filament\Tests\livewire;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Filament::setCurrentPanel('google-two-factor-authentication');
|
||||
});
|
||||
|
||||
it('can render the challenge form after valid login credentials are successfully used', function () {
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
$livewire = livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->assertSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect();
|
||||
|
||||
expect(decrypt($livewire->instance()->userUndertakingMultiFactorAuthentication))
|
||||
->toBe($userToAuthenticate->getKey());
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
it('will authenticate the user after a valid challenge code is used', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'code' => $googleTwoFactorAuthentication->getCurrentCode($userToAuthenticate),
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(Filament::getUrl());
|
||||
|
||||
$this->assertAuthenticatedAs($userToAuthenticate);
|
||||
});
|
||||
|
||||
it('will make the recovery code field visible when the user requests it', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->assertFormFieldExists(
|
||||
"{$googleTwoFactorAuthentication->getId()}.recoveryCode",
|
||||
'multiFactorChallengeForm',
|
||||
fn (TextInput $field): bool => $field->isHidden(),
|
||||
)
|
||||
->callAction(TestAction::make('useRecoveryCode')
|
||||
->schemaComponent("multiFactorChallengeForm.{$googleTwoFactorAuthentication->getId()}.code"))
|
||||
->assertFormFieldExists(
|
||||
"{$googleTwoFactorAuthentication->getId()}.recoveryCode",
|
||||
'multiFactorChallengeForm',
|
||||
fn (TextInput $field): bool => $field->isVisible(),
|
||||
);
|
||||
});
|
||||
|
||||
it('will authenticate the user after a valid recovery code is used', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->callAction(TestAction::make('useRecoveryCode')
|
||||
->schemaComponent("multiFactorChallengeForm.{$googleTwoFactorAuthentication->getId()}.code"))
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'recoveryCode' => Arr::random($googleTwoFactorAuthentication->getRecoveryCodes($userToAuthenticate)),
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(Filament::getUrl());
|
||||
|
||||
$this->assertAuthenticatedAs($userToAuthenticate);
|
||||
});
|
||||
|
||||
it('will not render the challenge form after invalid login credentials are used', function () {
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'incorrect-password',
|
||||
])
|
||||
->assertSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->call('authenticate')
|
||||
->assertSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
it('will not render the challenge form if a user does not have multi-factor authentication enabled', function () {
|
||||
$userToAuthenticate = User::factory()->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->assertSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->call('authenticate')
|
||||
->assertSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertRedirect(Filament::getUrl());
|
||||
|
||||
$this->assertAuthenticatedAs($userToAuthenticate);
|
||||
});
|
||||
|
||||
it('will not authenticate the user when an invalid challenge code is used', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'code' => ($googleTwoFactorAuthentication->getCurrentCode($userToAuthenticate) === '000000')
|
||||
? '111111'
|
||||
: '000000',
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
->assertHasFormErrors([
|
||||
"{$googleTwoFactorAuthentication->getId()}.code",
|
||||
], 'multiFactorChallengeForm')
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('challenge codes are required', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'code' => '',
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
->assertHasFormErrors([
|
||||
"{$googleTwoFactorAuthentication->getId()}.code" => 'required',
|
||||
], 'multiFactorChallengeForm')
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('challenge codes must be numeric', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'code' => Str::random(6),
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
->assertHasFormErrors([
|
||||
"{$googleTwoFactorAuthentication->getId()}.code" => 'numeric',
|
||||
], 'multiFactorChallengeForm')
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('challenge codes must be 6 digits', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'code' => Str::limit($googleTwoFactorAuthentication->getCurrentCode($userToAuthenticate), limit: 5, end: ''),
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
->assertHasFormErrors([
|
||||
"{$googleTwoFactorAuthentication->getId()}.code" => 'digits',
|
||||
], 'multiFactorChallengeForm')
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
it('will not authenticate the user when an invalid recovery code is used', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(filament::getCurrentPanel()->getMultiFactorAuthenticationProviders());
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->assertFormFieldExists(
|
||||
"{$googleTwoFactorAuthentication->getId()}.recoveryCode",
|
||||
'multiFactorChallengeForm',
|
||||
fn (TextInput $field): bool => $field->isHidden(),
|
||||
)
|
||||
->callAction(TestAction::make('useRecoveryCode')
|
||||
->schemaComponent("multiFactorChallengeForm.{$googleTwoFactorAuthentication->getId()}.code"))
|
||||
->assertFormFieldExists(
|
||||
"{$googleTwoFactorAuthentication->getId()}.recoveryCode",
|
||||
'multiFactorChallengeForm',
|
||||
fn (TextInput $field): bool => $field->isVisible(),
|
||||
)
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'recoveryCode' => 'invalid-recovery-code',
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
->assertHasFormErrors([
|
||||
"{$googleTwoFactorAuthentication->getId()}.recoveryCode",
|
||||
], 'multiFactorChallengeForm')
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
it('will not authenticate the user with a valid recovery code if recovery is disabled', function () {
|
||||
$googleTwoFactorAuthentication = Arr::first(Filament::getCurrentPanel()->getMultiFactorAuthenticationProviders())
|
||||
->recoverable(false);
|
||||
|
||||
$userToAuthenticate = User::factory()
|
||||
->hasGoogleTwoFactorAuthentication()
|
||||
->create();
|
||||
|
||||
livewire(Login::class)
|
||||
->fillForm([
|
||||
'email' => $userToAuthenticate->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->call('authenticate')
|
||||
->assertNotSet('userUndertakingMultiFactorAuthentication', null)
|
||||
->assertNoRedirect()
|
||||
->fillForm([
|
||||
$googleTwoFactorAuthentication->getId() => [
|
||||
'recoveryCode' => Arr::random($googleTwoFactorAuthentication->getRecoveryCodes($userToAuthenticate)),
|
||||
],
|
||||
], 'multiFactorChallengeForm')
|
||||
->call('authenticate')
|
||||
->assertHasErrors()
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
@@ -47,11 +47,13 @@ abstract class TestCase extends BaseTestCase
|
||||
WidgetsServiceProvider::class,
|
||||
AdminPanelProvider::class,
|
||||
CustomPanelProvider::class,
|
||||
SlugsPanelProvider::class,
|
||||
SingleDomainPanel::class,
|
||||
MultiDomainPanel::class,
|
||||
TenancyPanelProvider::class,
|
||||
EmailCodeAuthenticationPanelProvider::class,
|
||||
GoogleTwoFactorAuthenticationPanelProvider::class,
|
||||
DomainTenancyPanelProvider::class,
|
||||
MultiDomainPanel::class,
|
||||
SingleDomainPanel::class,
|
||||
SlugsPanelProvider::class,
|
||||
TenancyPanelProvider::class,
|
||||
];
|
||||
|
||||
sort($providers);
|
||||
|
||||
Reference in New Issue
Block a user