123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- <?php
- namespace Tests\Feature\Http\Auth;
- use App\Extensions\WebauthnTwoFAuthUserProvider;
- use App\Http\Controllers\Auth\WebAuthnLoginController;
- use App\Http\Middleware\SkipIfAuthenticated;
- use App\Models\User;
- use Illuminate\Support\Facades\Config;
- use Illuminate\Support\Facades\DB;
- use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
- use Laragear\WebAuthn\Enums\UserVerification;
- use PHPUnit\Framework\Attributes\CoversClass;
- use PHPUnit\Framework\Attributes\CoversMethod;
- use PHPUnit\Framework\Attributes\Test;
- use Tests\FeatureTestCase;
- /**
- * WebAuthnLoginControllerTest test class
- */
- #[CoversClass(WebAuthnLoginController::class)]
- #[CoversClass(User::class)]
- #[CoversClass(WebauthnTwoFAuthUserProvider::class)]
- #[CoversMethod(SkipIfAuthenticated::class, 'handle')]
- class WebAuthnLoginControllerTest extends FeatureTestCase
- {
- /**
- * @var \App\Models\User
- */
- protected $user;
- /**
- * @var \App\Models\User
- */
- protected $admin;
- const CREDENTIAL_ID = 's06aG41wsIYh5X1YUhB-SlH8y3F2RzdJZVse8iXRXOCd3oqQdEyCOsBawzxrYBtJRQA2azAMEN_q19TUp6iMgg';
- const CREDENTIAL_ID_ALT = '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg';
- const CREDENTIAL_ID_ALT_RAW = '+VOLFKPY+/FuMI/sJ7gMllK76L3VoRUINj6lL/Z3qDg=';
- const PUBLIC_KEY = 'eyJpdiI6ImYyUHlJOEJML0pwTXJ2UDkveTQwZFE9PSIsInZhbHVlIjoiQWFSYi9LVEszazlBRUZsWHp0cGNRNktGeEQ3aTBsbU9zZ1g5MEgrWFJJNmgraElsNU9hV0VsRVlWc3NoUVVHUjRRdlcxTS9pVklnOWtVYWY5TFJQTTFhR1Rxb1ZzTFkxTWE4VUVvK1lyU3pYQ1M3VlBMWWxZcDVaYWFnK25iaXVyWGR6ZFRmMFVoSmdPZ3UvSnptbVZER0FYdEEyYmNYcW43RkV5aTVqSjNwZEFsUjhUYSs0YjU2Z2V2bUJXa0E0aVB1VC8xSjdJZ2llRGlHY2RwOGk3MmNPTyt6eDFDWUs1dVBOSWp1ZUFSeUlkclgwRW16RE9sUUpDSWV6Sk50TSIsIm1hYyI6IjI3ODQ5NzcxZGY1MzMwYTNiZjAwZmEwMDJkZjYzMGU4N2UzZjZlOGM0ZWE3NDkyYWMxMThhNmE5NWZiMTVjNGEiLCJ0YWciOiIifQ==';
- const USER_ID = '3b758ac868b74307a7e96e69ae187339';
- const USER_ID_ALT = 'e8af6f703f8042aa91c30cf72289aa07';
- const EMAIL = 'john.doe@example.com';
- const ASSERTION_RESPONSE = [
- 'id' => self::CREDENTIAL_ID_ALT,
- 'rawId' => self::CREDENTIAL_ID_ALT_RAW,
- 'type' => 'public-key',
- 'response' => [
- 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
- 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
- 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
- 'userHandle' => self::USER_ID_ALT,
- ],
- 'email' => self::EMAIL,
- ];
- const ASSERTION_RESPONSE_NO_HANDLE = [
- 'id' => self::CREDENTIAL_ID_ALT,
- 'rawId' => self::CREDENTIAL_ID_ALT_RAW,
- 'type' => 'public-key',
- 'response' => [
- 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
- 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
- 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
- 'userHandle' => null,
- ],
- 'email' => self::EMAIL,
- ];
- const ASSERTION_RESPONSE_INVALID = [
- 'id' => self::CREDENTIAL_ID_ALT,
- 'rawId' => self::CREDENTIAL_ID_ALT_RAW,
- 'type' => 'public-key',
- 'response' => [
- 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
- 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
- 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
- 'userHandle' => self::USER_ID_ALT,
- ],
- 'email' => self::EMAIL,
- ];
- const ASSERTION_CHALLENGE = 'iXozmynKi+YD2iRvKNbSPA==';
- public function setUp() : void
- {
- parent::setUp();
- DB::table('users')->delete();
- }
- #[Test]
- public function test_webauthn_login_returns_success()
- {
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- DB::table('webauthn_credentials')->insert([
- 'id' => self::CREDENTIAL_ID_ALT,
- 'authenticatable_type' => \App\Models\User::class,
- 'authenticatable_id' => $this->user->id,
- 'user_id' => self::USER_ID_ALT,
- 'counter' => 0,
- 'rp_id' => 'http://localhost',
- 'origin' => 'http://localhost',
- 'aaguid' => '00000000-0000-0000-0000-000000000000',
- 'attestation_format' => 'none',
- 'public_key' => self::PUBLIC_KEY,
- 'updated_at' => now(),
- 'created_at' => now(),
- ]);
- $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
- new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
- 60,
- false,
- )]);
- $this->mock(AssertionValidator::class)
- ->expects('send->thenReturn')
- ->andReturn();
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
- ->assertOk()
- ->assertJsonFragment([
- 'message' => 'authenticated',
- 'id' => $this->user->id,
- 'name' => $this->user->name,
- 'email' => $this->user->email,
- 'is_admin' => false,
- ])
- ->assertJsonStructure([
- 'preferences',
- ]);
- }
- #[Test]
- public function test_webauthn_admin_login_returns_admin_role()
- {
- $this->admin = User::factory()->administrator()->create(['email' => self::EMAIL]);
- DB::table('webauthn_credentials')->insert([
- 'id' => self::CREDENTIAL_ID_ALT,
- 'authenticatable_type' => \App\Models\User::class,
- 'authenticatable_id' => $this->admin->id,
- 'user_id' => self::USER_ID_ALT,
- 'counter' => 0,
- 'rp_id' => 'http://localhost',
- 'origin' => 'http://localhost',
- 'aaguid' => '00000000-0000-0000-0000-000000000000',
- 'attestation_format' => 'none',
- 'public_key' => self::PUBLIC_KEY,
- 'updated_at' => now(),
- 'created_at' => now(),
- ]);
- $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
- new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
- 60,
- false,
- )]);
- $this->mock(AssertionValidator::class)
- ->expects('send->thenReturn')
- ->andReturn();
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
- ->assertOk()
- ->assertJsonFragment([
- 'is_admin' => true,
- ]);
- }
- #[Test]
- public function test_webauthn_login_merge_handle_if_missing()
- {
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- DB::table('webauthn_credentials')->insert([
- 'id' => self::CREDENTIAL_ID_ALT,
- 'authenticatable_type' => \App\Models\User::class,
- 'authenticatable_id' => $this->user->id,
- 'user_id' => self::USER_ID_ALT,
- 'counter' => 0,
- 'rp_id' => 'http://localhost',
- 'origin' => 'http://localhost',
- 'aaguid' => '00000000-0000-0000-0000-000000000000',
- 'attestation_format' => 'none',
- 'public_key' => self::PUBLIC_KEY,
- 'updated_at' => now(),
- 'created_at' => now(),
- ]);
- $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
- new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
- 60,
- false,
- )]);
- $this->mock(AssertionValidator::class)
- ->expects('send->thenReturn')
- ->andReturn();
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_NO_HANDLE)
- ->assertOk()
- ->assertJsonFragment([
- 'message' => 'authenticated',
- 'name' => $this->user->name,
- ])
- ->assertJsonStructure([
- 'message',
- 'name',
- 'preferences',
- ]);
- }
- #[Test]
- public function test_legacy_login_is_rejected_when_webauthn_only_is_enable()
- {
- $this->user = User::factory()->create([
- 'email' => self::EMAIL,
- ]);
- // Set to webauthn only
- $this->user['preferences->useWebauthnOnly'] = true;
- $this->user->save();
- $response = $this->json('POST', '/user/login', [
- 'email' => self::EMAIL,
- 'password' => 'password',
- ])
- ->assertUnauthorized();
- }
- #[Test]
- public function test_webauthn_login_already_authenticated_is_rejected()
- {
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- DB::table('webauthn_credentials')->insert([
- 'id' => self::CREDENTIAL_ID_ALT,
- 'authenticatable_type' => \App\Models\User::class,
- 'authenticatable_id' => $this->user->id,
- 'user_id' => self::USER_ID_ALT,
- 'counter' => 0,
- 'rp_id' => 'http://localhost',
- 'origin' => 'http://localhost',
- 'aaguid' => '00000000-0000-0000-0000-000000000000',
- 'attestation_format' => 'none',
- 'public_key' => self::PUBLIC_KEY,
- 'updated_at' => now(),
- 'created_at' => now(),
- ]);
- $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
- new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
- 60,
- false,
- )]);
- $this->mock(AssertionValidator::class)
- ->expects('send->thenReturn')
- ->andReturn();
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
- ->assertOk();
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
- ->assertStatus(400)
- ->assertJsonStructure([
- 'message',
- ]);
- }
- #[Test]
- public function test_webauthn_login_with_missing_data_returns_validation_error()
- {
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- $data = [
- 'id' => '',
- 'rawId' => '',
- 'type' => '',
- 'response' => [
- 'authenticatorData' => '',
- 'clientDataJSON' => '',
- 'signature' => '',
- 'userHandle' => null,
- ],
- ];
- $response = $this->json('POST', '/webauthn/login', $data)
- ->assertStatus(422)
- ->assertJsonValidationErrors([
- 'id',
- 'rawId',
- 'type',
- 'response.authenticatorData',
- 'response.clientDataJSON',
- 'response.signature',
- ]);
- }
- #[Test]
- public function test_webauthn_invalid_login_returns_unauthorized()
- {
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
- new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
- 60,
- false,
- )]);
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
- ->assertUnauthorized();
- }
- #[Test]
- public function test_too_many_invalid_login_attempts_returns_too_many_request_error()
- {
- $throttle = 8;
- Config::set('auth.throttle.login', $throttle);
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
- new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
- 60,
- false,
- )]);
- for ($i = 0; $i < $throttle - 1; $i++) {
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
- }
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
- ->assertUnauthorized();
- $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
- ->assertStatus(429);
- }
- #[Test]
- public function test_get_options_returns_success()
- {
- Config::set('webauthn.user_verification', UserVerification::PREFERRED);
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- DB::table('webauthn_credentials')->insert([
- 'id' => self::CREDENTIAL_ID,
- 'authenticatable_type' => \App\Models\User::class,
- 'authenticatable_id' => $this->user->id,
- 'user_id' => self::USER_ID,
- 'counter' => 0,
- 'rp_id' => 'http://localhost',
- 'origin' => 'http://localhost',
- 'aaguid' => '00000000-0000-0000-0000-000000000000',
- 'attestation_format' => 'none',
- 'public_key' => self::PUBLIC_KEY,
- 'updated_at' => now(),
- 'created_at' => now(),
- ]);
- $response = $this->json('POST', '/webauthn/login/options', [
- 'email' => $this->user->email,
- ])
- ->assertOk()
- ->assertJsonStructure([
- 'challenge',
- 'timeout',
- ])
- ->assertJsonFragment([
- 'allowCredentials' => [[
- 'id' => self::CREDENTIAL_ID,
- 'type' => 'public-key',
- ]],
- ]);
- }
- #[Test]
- public function test_get_options_for_securelogin_returns_required_userVerification()
- {
- Config::set('webauthn.user_verification', UserVerification::REQUIRED);
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- DB::table('webauthn_credentials')->insert([
- 'id' => self::CREDENTIAL_ID,
- 'authenticatable_type' => \App\Models\User::class,
- 'authenticatable_id' => $this->user->id,
- 'user_id' => self::USER_ID,
- 'counter' => 0,
- 'rp_id' => 'http://localhost',
- 'origin' => 'http://localhost',
- 'aaguid' => '00000000-0000-0000-0000-000000000000',
- 'attestation_format' => 'none',
- 'public_key' => self::PUBLIC_KEY,
- 'updated_at' => now(),
- 'created_at' => now(),
- ]);
- $response = $this->json('POST', '/webauthn/login/options', [
- 'email' => $this->user->email,
- ])
- ->assertOk()
- ->assertJsonStructure([
- 'challenge',
- 'userVerification',
- 'timeout',
- ])
- ->assertJsonFragment([
- 'userVerification' => 'required',
- 'allowCredentials' => [[
- 'id' => self::CREDENTIAL_ID,
- 'type' => 'public-key',
- ]],
- ]);
- }
- #[Test]
- public function test_get_options_for_fastlogin_returns_discouraged_userVerification()
- {
- Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- DB::table('webauthn_credentials')->insert([
- 'id' => self::CREDENTIAL_ID,
- 'authenticatable_type' => \App\Models\User::class,
- 'authenticatable_id' => $this->user->id,
- 'user_id' => self::USER_ID,
- 'counter' => 0,
- 'rp_id' => 'http://localhost',
- 'origin' => 'http://localhost',
- 'aaguid' => '00000000-0000-0000-0000-000000000000',
- 'attestation_format' => 'none',
- 'public_key' => self::PUBLIC_KEY,
- 'updated_at' => now(),
- 'created_at' => now(),
- ]);
- $response = $this->json('POST', '/webauthn/login/options', [
- 'email' => $this->user->email,
- ])
- ->assertOk()
- ->assertJsonStructure([
- 'challenge',
- 'userVerification',
- 'timeout',
- ])
- ->assertJsonFragment([
- 'userVerification' => 'discouraged',
- 'allowCredentials' => [[
- 'id' => self::CREDENTIAL_ID,
- 'type' => 'public-key',
- ]],
- ]);
- }
- #[Test]
- public function test_get_options_with_capitalized_email_returns_success()
- {
- $this->user = User::factory()->create(['email' => self::EMAIL]);
- $this->json('POST', '/webauthn/login/options', [
- 'email' => strtoupper($this->user->email),
- ])
- ->assertOk();
- }
- #[Test]
- public function test_get_options_with_missing_email_returns_validation_errors()
- {
- $this->json('POST', '/webauthn/login/options', [
- 'email' => null,
- ])
- ->assertStatus(422)
- ->assertJsonValidationErrors([
- 'email',
- ]);
- }
- #[Test]
- public function test_get_options_with_invalid_email_returns_validation_errors()
- {
- $this->json('POST', '/webauthn/login/options', [
- 'email' => 'invalid',
- ])
- ->assertStatus(422)
- ->assertJsonValidationErrors([
- 'email',
- ]);
- }
- #[Test]
- public function test_get_options_with_unknown_email_returns_validation_errors()
- {
- $this->json('POST', '/webauthn/login/options', [
- 'email' => 'john@example.com',
- ])
- ->assertStatus(422)
- ->assertJsonValidationErrors([
- 'email',
- ]);
- }
- }
|