diff --git a/README.md b/README.md index d639171..e061c05 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,9 @@ public function login(Request $request) Behind the scenes, once the User is retrieved and validated from your guard of choice, it makes an additional check for a valid TOTP code. If it's invalid, it will return false and no authentication will happen. +> For Laravel Breeze, you may need to edit the `LoginRequest::authenticate()` call. +> For Laravel Fortify and Jetstream, you may need to set a custom callback with the `Fortify::authenticateUsing()` method. + #### Separating the TOTP requirement In some occasions you will want to tell the user the authentication failed not because the credentials were incorrect, but because of the TOTP code was invalid. diff --git a/src/Laraguard.php b/src/Laraguard.php index b20f580..fdf6a48 100644 --- a/src/Laraguard.php +++ b/src/Laraguard.php @@ -113,7 +113,7 @@ protected function isSafeDevicesEnabled(): bool protected function requestHasCode(): bool { return !validator($this->request->only($this->input), [ - $this->input => 'required|numeric', + $this->input => 'required|alpha_num', ])->fails(); } diff --git a/tests/LaraguardTest.php b/tests/LaraguardTest.php index 3d6d42b..e1924da 100644 --- a/tests/LaraguardTest.php +++ b/tests/LaraguardTest.php @@ -15,6 +15,8 @@ use Tests\Stubs\UserStub; use Tests\Stubs\UserTwoFactorStub; +use function now; + class LaraguardTest extends TestCase { use DatabaseMigrations; @@ -65,6 +67,40 @@ public function test_authenticates_with_when_with_no_exceptions(): void static::assertTrue(Auth::attemptWhen($credentials, Laraguard::hasCodeOrFails())); } + public function test_authenticates_with_when_with_recovery_code(): void + { + $credentials = [ + 'email' => $this->user->email, + 'password' => 'secret' + ]; + + $this->instance('request', Request::create('test', 'POST', [ + '2fa_code' => $this->user->getRecoveryCodes()->first()['code'] + ])); + + $this->travelTo($now = now()); + + static::assertTrue(Auth::attemptWhen($credentials, Laraguard::hasCode())); + static::assertEquals($now->toIso8601ZuluString('microsecond'), $this->user->fresh()->getRecoveryCodes()->first()['used_at']); + } + + public function test_authenticates_with_when_with_recovery_code_with_no_exceptions(): void + { + $credentials = [ + 'email' => $this->user->email, + 'password' => 'secret' + ]; + + $this->instance('request', Request::create('test', 'POST', [ + '2fa_code' => $this->user->getRecoveryCodes()->first()['code'] + ])); + + $this->travelTo($now = now()); + + static::assertTrue(Auth::attemptWhen($credentials, Laraguard::hasCodeOrFails())); + static::assertEquals($now->toIso8601ZuluString('microsecond'), $this->user->fresh()->getRecoveryCodes()->first()['used_at']); + } + public function test_authenticates_with_different_input_name(): void { $credentials = [ @@ -142,6 +178,27 @@ public function test_validation_exception_when_code_invalid(): void } } + public function test_validation_exception_when_code_empty(): void + { + $this->expectException(ValidationException::class); + + $credentials = [ + 'email' => $this->user->email, + 'password' => 'secret' + ]; + + $this->instance('request', Request::create('test', 'POST', [ + '2fa_code' => '' + ])); + + try { + Auth::attemptWhen($credentials, Laraguard::hasCodeOrFails()); + } catch (ValidationException $exception) { + static::assertSame(['2fa_code' => ['The Code is invalid or has expired.']], $exception->errors()); + throw $exception; + } + } + public function test_validation_exception_with_message_when_code_invalid(): void { $this->expectException(ValidationException::class);