WebAuthnLoginControllerTest.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <?php
  2. namespace Tests\Feature\Http\Auth;
  3. use App\Models\User;
  4. use Illuminate\Support\Facades\DB;
  5. use Laragear\WebAuthn\Http\Requests\AssertedRequest;
  6. use Tests\FeatureTestCase;
  7. use Laragear\WebAuthn\WebAuthn;
  8. use Illuminate\Support\Facades\Config;
  9. use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
  10. /**
  11. * @covers \App\Http\Controllers\Auth\WebAuthnLoginController
  12. * @covers \App\Models\User
  13. */
  14. class WebAuthnLoginControllerTest extends FeatureTestCase
  15. {
  16. /**
  17. * @var \App\Models\User
  18. */
  19. protected $user;
  20. const CREDENTIAL_ID = 's06aG41wsIYh5X1YUhB-SlH8y3F2RzdJZVse8iXRXOCd3oqQdEyCOsBawzxrYBtJRQA2azAMEN_q19TUp6iMgg';
  21. const CREDENTIAL_ID_ALT = '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg';
  22. const CREDENTIAL_ID_ALT_RAW = '+VOLFKPY+/FuMI/sJ7gMllK76L3VoRUINj6lL/Z3qDg=';
  23. const PUBLIC_KEY = 'eyJpdiI6ImYyUHlJOEJML0pwTXJ2UDkveTQwZFE9PSIsInZhbHVlIjoiQWFSYi9LVEszazlBRUZsWHp0cGNRNktGeEQ3aTBsbU9zZ1g5MEgrWFJJNmgraElsNU9hV0VsRVlWc3NoUVVHUjRRdlcxTS9pVklnOWtVYWY5TFJQTTFhR1Rxb1ZzTFkxTWE4VUVvK1lyU3pYQ1M3VlBMWWxZcDVaYWFnK25iaXVyWGR6ZFRmMFVoSmdPZ3UvSnptbVZER0FYdEEyYmNYcW43RkV5aTVqSjNwZEFsUjhUYSs0YjU2Z2V2bUJXa0E0aVB1VC8xSjdJZ2llRGlHY2RwOGk3MmNPTyt6eDFDWUs1dVBOSWp1ZUFSeUlkclgwRW16RE9sUUpDSWV6Sk50TSIsIm1hYyI6IjI3ODQ5NzcxZGY1MzMwYTNiZjAwZmEwMDJkZjYzMGU4N2UzZjZlOGM0ZWE3NDkyYWMxMThhNmE5NWZiMTVjNGEiLCJ0YWciOiIifQ==';
  24. const USER_ID = '3b758ac868b74307a7e96e69ae187339';
  25. const USER_ID_ALT = 'e8af6f703f8042aa91c30cf72289aa07';
  26. const ASSERTION_RESPONSE = [
  27. 'id' => self::CREDENTIAL_ID_ALT,
  28. 'rawId' => self::CREDENTIAL_ID_ALT_RAW,
  29. 'type' => 'public-key',
  30. 'response' => [
  31. 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
  32. 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
  33. 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
  34. 'userHandle' => self::USER_ID_ALT,
  35. ]
  36. ];
  37. const ASSERTION_RESPONSE_NO_HANDLE = [
  38. 'id' => self::CREDENTIAL_ID_ALT,
  39. 'rawId' => self::CREDENTIAL_ID_ALT_RAW,
  40. 'type' => 'public-key',
  41. 'response' => [
  42. 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
  43. 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
  44. 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
  45. 'userHandle' => null,
  46. ]
  47. ];
  48. const ASSERTION_CHALLENGE = 'iXozmynKi+YD2iRvKNbSPA==';
  49. /**
  50. * @test
  51. */
  52. public function setUp(): void
  53. {
  54. parent::setUp();
  55. DB::table('users')->delete();
  56. }
  57. /**
  58. * @test
  59. */
  60. public function test_webauthn_login_uses_login_and_returns_no_content()
  61. {
  62. $this->user = User::factory()->create();
  63. $mock = $this->mock(AssertedRequest::class)->makePartial()->shouldIgnoreMissing();
  64. $mock->shouldReceive([
  65. 'has' => false,
  66. 'login' => $this->user,
  67. ]);
  68. $this->json('POST', '/webauthn/login')
  69. ->assertNoContent();
  70. }
  71. /**
  72. * @test
  73. */
  74. public function test_webauthn_login_merge_handle_if_missing()
  75. {
  76. $this->user = User::factory()->create();
  77. DB::table('webauthn_credentials')->insert([
  78. 'id' => self::CREDENTIAL_ID_ALT,
  79. 'authenticatable_type' => \App\Models\User::class,
  80. 'authenticatable_id' => $this->user->id,
  81. 'user_id' => self::USER_ID_ALT,
  82. 'counter' => 0,
  83. 'rp_id' => 'http://localhost',
  84. 'origin' => 'http://localhost',
  85. 'aaguid' => '00000000-0000-0000-0000-000000000000',
  86. 'attestation_format' => 'none',
  87. 'public_key' => self::PUBLIC_KEY,
  88. 'updated_at' => now(),
  89. 'created_at' => now(),
  90. ]);
  91. $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
  92. new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
  93. 60,
  94. false,
  95. )]);
  96. $this->mock(AssertionValidator::class)
  97. ->expects('send->thenReturn')
  98. ->andReturn();
  99. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_NO_HANDLE)
  100. ->assertNoContent();
  101. }
  102. /**
  103. * @test
  104. */
  105. public function test_webauthn_invalid_login_returns_error()
  106. {
  107. $this->user = User::factory()->create();
  108. $mock = $this->mock(AssertedRequest::class)->makePartial()->shouldIgnoreMissing();
  109. $mock->shouldReceive([
  110. 'has' => false,
  111. 'login' => null,
  112. ]);
  113. $this->json('POST', '/webauthn/login')
  114. ->assertNoContent(422);
  115. }
  116. /**
  117. * @test
  118. */
  119. public function test_webauthn_login_with_missing_data_returns_validation_error()
  120. {
  121. $this->user = User::factory()->create();
  122. $data = [
  123. 'id' => '',
  124. 'rawId' => '',
  125. 'type' => '',
  126. 'response' => [
  127. 'authenticatorData' => '',
  128. 'clientDataJSON' => '',
  129. 'signature' => '',
  130. 'userHandle' => null,
  131. ],
  132. ];
  133. $response = $this->json('POST', '/webauthn/login', $data)
  134. ->assertStatus(422)
  135. ->assertJsonValidationErrors([
  136. 'id',
  137. 'rawId',
  138. 'type',
  139. 'response.authenticatorData',
  140. 'response.clientDataJSON',
  141. 'response.signature',
  142. ]);
  143. }
  144. /**
  145. * @test
  146. */
  147. public function test_get_options_for_securelogin_returns_success()
  148. {
  149. Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_REQUIRED);
  150. $this->user = User::factory()->create();
  151. DB::table('webauthn_credentials')->insert([
  152. 'id' => self::CREDENTIAL_ID,
  153. 'authenticatable_type' => \App\Models\User::class,
  154. 'authenticatable_id' => $this->user->id,
  155. 'user_id' => self::USER_ID,
  156. 'counter' => 0,
  157. 'rp_id' => 'http://localhost',
  158. 'origin' => 'http://localhost',
  159. 'aaguid' => '00000000-0000-0000-0000-000000000000',
  160. 'attestation_format' => 'none',
  161. 'public_key' => self::PUBLIC_KEY,
  162. 'updated_at' => now(),
  163. 'created_at' => now(),
  164. ]);
  165. $response = $this->json('POST', '/webauthn/login/options')
  166. ->assertOk()
  167. ->assertJsonStructure([
  168. 'challenge',
  169. 'userVerification',
  170. 'timeout',
  171. ])
  172. ->assertJsonFragment([
  173. 'userVerification' => 'required',
  174. 'allowCredentials' => [[
  175. 'id' => self::CREDENTIAL_ID,
  176. 'type' => 'public-key',
  177. ]],
  178. ]);
  179. }
  180. /**
  181. * @test
  182. */
  183. public function test_get_options_for_fastlogin_returns_success()
  184. {
  185. Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_DISCOURAGED);
  186. $this->user = User::factory()->create();
  187. DB::table('webauthn_credentials')->insert([
  188. 'id' => self::CREDENTIAL_ID,
  189. 'authenticatable_type' => \App\Models\User::class,
  190. 'authenticatable_id' => $this->user->id,
  191. 'user_id' => self::USER_ID,
  192. 'counter' => 0,
  193. 'rp_id' => 'http://localhost',
  194. 'origin' => 'http://localhost',
  195. 'aaguid' => '00000000-0000-0000-0000-000000000000',
  196. 'attestation_format' => 'none',
  197. 'public_key' => self::PUBLIC_KEY,
  198. 'updated_at' => now(),
  199. 'created_at' => now(),
  200. ]);
  201. $response = $this->json('POST', '/webauthn/login/options')
  202. ->assertOk()
  203. ->assertJsonStructure([
  204. 'challenge',
  205. 'userVerification',
  206. 'timeout',
  207. ])
  208. ->assertJsonFragment([
  209. 'userVerification' => 'discouraged',
  210. 'allowCredentials' => [[
  211. 'id' => self::CREDENTIAL_ID,
  212. 'type' => 'public-key',
  213. ]],
  214. ]);
  215. }
  216. /**
  217. * @test
  218. */
  219. public function test_get_options_with_no_registred_user_returns_error()
  220. {
  221. $this->json('POST', '/webauthn/login/options')
  222. ->assertStatus(400)
  223. ->assertJsonStructure([
  224. 'message',
  225. ]);
  226. }
  227. }