WebAuthnLoginControllerTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. <?php
  2. namespace Tests\Feature\Http\Auth;
  3. use App\Extensions\WebauthnTwoFAuthUserProvider;
  4. use App\Facades\Settings;
  5. use App\Http\Controllers\Auth\WebAuthnLoginController;
  6. use App\Http\Middleware\RejectIfSsoOnlyAndNotForAdmin;
  7. use App\Listeners\Authentication\FailedLoginListener;
  8. use App\Listeners\Authentication\LoginListener;
  9. use App\Models\User;
  10. use App\Notifications\SignedInWithNewDeviceNotification;
  11. use Illuminate\Support\Facades\Config;
  12. use Illuminate\Support\Facades\DB;
  13. use Illuminate\Support\Facades\Notification;
  14. use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
  15. use Laragear\WebAuthn\Enums\UserVerification;
  16. use PHPUnit\Framework\Attributes\CoversClass;
  17. use PHPUnit\Framework\Attributes\CoversMethod;
  18. use PHPUnit\Framework\Attributes\Test;
  19. use Tests\FeatureTestCase;
  20. /**
  21. * WebAuthnLoginControllerTest test class
  22. */
  23. #[CoversClass(WebAuthnLoginController::class)]
  24. #[CoversClass(User::class)]
  25. #[CoversClass(WebauthnTwoFAuthUserProvider::class)]
  26. #[CoversClass(LoginListener::class)]
  27. #[CoversClass(FailedLoginListener::class)]
  28. #[CoversMethod(RejectIfSsoOnlyAndNotForAdmin::class, 'handle')]
  29. class WebAuthnLoginControllerTest extends FeatureTestCase
  30. {
  31. /**
  32. * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
  33. */
  34. protected $user;
  35. const CREDENTIAL_ID = 's06aG41wsIYh5X1YUhB-SlH8y3F2RzdJZVse8iXRXOCd3oqQdEyCOsBawzxrYBtJRQA2azAMEN_q19TUp6iMgg';
  36. const CREDENTIAL_ID_ALT = '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg';
  37. const CREDENTIAL_ID_ALT_RAW = '+VOLFKPY+/FuMI/sJ7gMllK76L3VoRUINj6lL/Z3qDg=';
  38. const PUBLIC_KEY = 'eyJpdiI6ImYyUHlJOEJML0pwTXJ2UDkveTQwZFE9PSIsInZhbHVlIjoiQWFSYi9LVEszazlBRUZsWHp0cGNRNktGeEQ3aTBsbU9zZ1g5MEgrWFJJNmgraElsNU9hV0VsRVlWc3NoUVVHUjRRdlcxTS9pVklnOWtVYWY5TFJQTTFhR1Rxb1ZzTFkxTWE4VUVvK1lyU3pYQ1M3VlBMWWxZcDVaYWFnK25iaXVyWGR6ZFRmMFVoSmdPZ3UvSnptbVZER0FYdEEyYmNYcW43RkV5aTVqSjNwZEFsUjhUYSs0YjU2Z2V2bUJXa0E0aVB1VC8xSjdJZ2llRGlHY2RwOGk3MmNPTyt6eDFDWUs1dVBOSWp1ZUFSeUlkclgwRW16RE9sUUpDSWV6Sk50TSIsIm1hYyI6IjI3ODQ5NzcxZGY1MzMwYTNiZjAwZmEwMDJkZjYzMGU4N2UzZjZlOGM0ZWE3NDkyYWMxMThhNmE5NWZiMTVjNGEiLCJ0YWciOiIifQ==';
  39. const USER_ID = '3b758ac868b74307a7e96e69ae187339';
  40. const USER_ID_ALT = 'e8af6f703f8042aa91c30cf72289aa07';
  41. const EMAIL = 'john.doe@example.com';
  42. private const GUARD = 'web-guard';
  43. private const AUTH_METHOD = 'webauthn';
  44. const ASSERTION_RESPONSE = [
  45. 'id' => self::CREDENTIAL_ID_ALT,
  46. 'rawId' => self::CREDENTIAL_ID_ALT_RAW,
  47. 'type' => 'public-key',
  48. 'response' => [
  49. 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
  50. 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
  51. 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
  52. 'userHandle' => self::USER_ID_ALT,
  53. ],
  54. 'email' => self::EMAIL,
  55. ];
  56. const ASSERTION_RESPONSE_NO_HANDLE = [
  57. 'id' => self::CREDENTIAL_ID_ALT,
  58. 'rawId' => self::CREDENTIAL_ID_ALT_RAW,
  59. 'type' => 'public-key',
  60. 'response' => [
  61. 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
  62. 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
  63. 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
  64. 'userHandle' => null,
  65. ],
  66. 'email' => self::EMAIL,
  67. ];
  68. const ASSERTION_RESPONSE_INVALID = [
  69. 'id' => self::CREDENTIAL_ID_ALT,
  70. 'rawId' => self::CREDENTIAL_ID_ALT_RAW,
  71. 'type' => 'public-key',
  72. 'response' => [
  73. 'clientDataJSON' => 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVhvem15bktpLVlEMmlSdktOYlNQQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
  74. 'authenticatorData' => 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==',
  75. 'signature' => 'ca4IJ9h8bZnjMbEFuHX1zfX5LcbiPyDVz6sD1/ppR4t8++1DxKa5EdBIrfNlo8FSOv/JSzMrGGUCQvc/Ngj1KnZpO3s9OdTb54/gMDewH/K8EG4wSvxzHdL6sMbP7UUc5Wq1pcdu9MgXY8V+1gftXpzcoaae0X+mLEETgU7eB8jG0mZhVWvE4yQKuDnZA1i9r8oQhqsvG4nUw1BxvR8wAGiRR+R287LaL41k+xum5mS8zEojUmuLSH50miyVxZ4Y+/oyfxG7i+wSYGNSXlW5iNPB+2WupGS7ce4TuOgaFeMmP2a9rzP4m2IBSQoJ2FyrdzR7HwBEewqqrUVbGQw3Aw==',
  76. 'userHandle' => self::USER_ID_ALT,
  77. ],
  78. 'email' => self::EMAIL,
  79. ];
  80. const ASSERTION_CHALLENGE = 'iXozmynKi+YD2iRvKNbSPA==';
  81. public function setUp() : void
  82. {
  83. parent::setUp();
  84. DB::table('users')->delete();
  85. $this->user = User::factory()->create(['email' => self::EMAIL]);
  86. $this->mock(AssertionValidator::class)
  87. ->shouldReceive('send->thenReturn')
  88. ->andReturn();
  89. }
  90. #[Test]
  91. public function test_webauthn_login_returns_success()
  92. {
  93. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  94. $this->addWebauthnChallengeToSession();
  95. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  96. ->assertOk()
  97. ->assertJsonFragment([
  98. 'message' => 'authenticated',
  99. 'id' => $this->user->id,
  100. 'name' => $this->user->name,
  101. 'email' => $this->user->email,
  102. 'is_admin' => false,
  103. ])
  104. ->assertJsonStructure([
  105. 'preferences',
  106. ]);
  107. }
  108. #[Test]
  109. public function test_webauthn_login_of_admin_returns_success_even_with_sso_only_enabled()
  110. {
  111. Settings::set('useSsoOnly', true);
  112. $this->user->promoteToAdministrator(true);
  113. $this->user->save();
  114. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  115. $this->addWebauthnChallengeToSession();
  116. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  117. ->assertOk()
  118. ->assertJsonFragment([
  119. 'message' => 'authenticated',
  120. 'id' => $this->user->id,
  121. 'name' => $this->user->name,
  122. 'email' => $this->user->email,
  123. 'is_admin' => true,
  124. ])
  125. ->assertJsonStructure([
  126. 'preferences',
  127. ]);
  128. $this->user->promoteToAdministrator(false);
  129. $this->user->save();
  130. }
  131. #[Test]
  132. public function test_webauthn_login_sends_new_device_notification_to_existing_user()
  133. {
  134. Notification::fake();
  135. $this->user['preferences->notifyOnNewAuthDevice'] = 1;
  136. $this->user->save();
  137. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  138. $this->addWebauthnChallengeToSession();
  139. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  140. ->assertOk();
  141. $this->actingAs($this->user, self::GUARD)
  142. ->json('GET', '/user/logout');
  143. $this->travel(1)->minute();
  144. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE, [
  145. 'HTTP_USER_AGENT' => 'another_useragent_to_be_identified_as_new_device',
  146. ])->assertOk();
  147. Notification::assertSentTo($this->user, SignedInWithNewDeviceNotification::class);
  148. }
  149. #[Test]
  150. public function test_webauthn_login_does_not_send_new_device_notification_to_new_user()
  151. {
  152. Notification::fake();
  153. $this->user['preferences->notifyOnNewAuthDevice'] = 1;
  154. $this->user->save();
  155. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  156. $this->addWebauthnChallengeToSession();
  157. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  158. ->assertOk();
  159. Notification::assertNothingSentTo($this->user);
  160. }
  161. #[Test]
  162. public function test_webauthn_login_does_not_send_new_device_notification_if_user_disabled_it()
  163. {
  164. Notification::fake();
  165. $this->user['preferences->notifyOnNewAuthDevice'] = 0;
  166. $this->user->save();
  167. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  168. $this->addWebauthnChallengeToSession();
  169. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  170. ->assertOk();
  171. Notification::assertNothingSentTo($this->user);
  172. }
  173. #[Test]
  174. public function test_webauthn_admin_login_returns_admin_role()
  175. {
  176. DB::table('users')->delete();
  177. $this->user = User::factory()->administrator()->create(['email' => self::EMAIL]);
  178. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  179. $this->addWebauthnChallengeToSession();
  180. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  181. ->assertOk()
  182. ->assertJsonFragment([
  183. 'is_admin' => true,
  184. ]);
  185. }
  186. #[Test]
  187. public function test_webauthn_login_merge_handle_if_missing()
  188. {
  189. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  190. $this->addWebauthnChallengeToSession();
  191. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_NO_HANDLE)
  192. ->assertOk()
  193. ->assertJsonFragment([
  194. 'message' => 'authenticated',
  195. 'name' => $this->user->name,
  196. ])
  197. ->assertJsonStructure([
  198. 'message',
  199. 'name',
  200. 'preferences',
  201. ]);
  202. }
  203. #[Test]
  204. public function test_legacy_login_is_rejected_when_webauthn_only_is_enable()
  205. {
  206. // Set to webauthn only
  207. $this->user['preferences->useWebauthnOnly'] = true;
  208. $this->user->save();
  209. $response = $this->json('POST', '/user/login', [
  210. 'email' => self::EMAIL,
  211. 'password' => 'password',
  212. ])
  213. ->assertUnauthorized();
  214. }
  215. #[Test]
  216. public function test_webauthn_login_already_authenticated_is_rejected()
  217. {
  218. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  219. $this->addWebauthnChallengeToSession();
  220. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  221. ->assertOk();
  222. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  223. ->assertStatus(400)
  224. ->assertJsonStructure([
  225. 'message',
  226. ]);
  227. }
  228. #[Test]
  229. public function test_webauthn_login_with_missing_data_returns_validation_error()
  230. {
  231. $data = [
  232. 'id' => '',
  233. 'rawId' => '',
  234. 'type' => '',
  235. 'response' => [
  236. 'authenticatorData' => '',
  237. 'clientDataJSON' => '',
  238. 'signature' => '',
  239. 'userHandle' => null,
  240. ],
  241. ];
  242. $response = $this->json('POST', '/webauthn/login', $data)
  243. ->assertStatus(422)
  244. ->assertJsonValidationErrors([
  245. 'id',
  246. 'rawId',
  247. 'type',
  248. 'response.authenticatorData',
  249. 'response.clientDataJSON',
  250. 'response.signature',
  251. ]);
  252. }
  253. #[Test]
  254. public function test_webauthn_invalid_login_returns_unauthorized()
  255. {
  256. $this->addWebauthnChallengeToSession();
  257. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
  258. ->assertUnauthorized();
  259. }
  260. #[Test]
  261. public function test_too_many_invalid_login_attempts_returns_too_many_request_error()
  262. {
  263. $throttle = 8;
  264. Config::set('auth.throttle.login', $throttle);
  265. $this->addWebauthnChallengeToSession();
  266. for ($i = 0; $i < $throttle - 1; $i++) {
  267. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID);
  268. }
  269. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
  270. ->assertUnauthorized();
  271. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
  272. ->assertStatus(429);
  273. }
  274. #[Test]
  275. public function test_get_options_returns_success()
  276. {
  277. Config::set('webauthn.user_verification', UserVerification::PREFERRED);
  278. $this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
  279. $response = $this->json('POST', '/webauthn/login/options', [
  280. 'email' => $this->user->email,
  281. ])
  282. ->assertOk()
  283. ->assertJsonStructure([
  284. 'challenge',
  285. 'timeout',
  286. ])
  287. ->assertJsonFragment([
  288. 'allowCredentials' => [[
  289. 'id' => self::CREDENTIAL_ID,
  290. 'type' => 'public-key',
  291. ]],
  292. ]);
  293. }
  294. #[Test]
  295. public function test_get_options_for_securelogin_returns_required_userVerification()
  296. {
  297. Config::set('webauthn.user_verification', UserVerification::REQUIRED);
  298. $this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
  299. $response = $this->json('POST', '/webauthn/login/options', [
  300. 'email' => $this->user->email,
  301. ])
  302. ->assertOk()
  303. ->assertJsonStructure([
  304. 'challenge',
  305. 'userVerification',
  306. 'timeout',
  307. ])
  308. ->assertJsonFragment([
  309. 'userVerification' => 'required',
  310. 'allowCredentials' => [[
  311. 'id' => self::CREDENTIAL_ID,
  312. 'type' => 'public-key',
  313. ]],
  314. ]);
  315. }
  316. #[Test]
  317. public function test_get_options_for_fastlogin_returns_discouraged_userVerification()
  318. {
  319. Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
  320. $this->createWebauthnCredential(self::CREDENTIAL_ID, $this->user->id, self::USER_ID);
  321. $response = $this->json('POST', '/webauthn/login/options', [
  322. 'email' => $this->user->email,
  323. ])
  324. ->assertOk()
  325. ->assertJsonStructure([
  326. 'challenge',
  327. 'userVerification',
  328. 'timeout',
  329. ])
  330. ->assertJsonFragment([
  331. 'userVerification' => 'discouraged',
  332. 'allowCredentials' => [[
  333. 'id' => self::CREDENTIAL_ID,
  334. 'type' => 'public-key',
  335. ]],
  336. ]);
  337. }
  338. #[Test]
  339. public function test_get_options_with_capitalized_email_returns_success()
  340. {
  341. $this->json('POST', '/webauthn/login/options', [
  342. 'email' => strtoupper($this->user->email),
  343. ])
  344. ->assertOk();
  345. }
  346. #[Test]
  347. public function test_get_options_with_missing_email_returns_validation_errors()
  348. {
  349. $this->json('POST', '/webauthn/login/options', [
  350. 'email' => null,
  351. ])
  352. ->assertStatus(422)
  353. ->assertJsonValidationErrors([
  354. 'email',
  355. ]);
  356. }
  357. #[Test]
  358. public function test_get_options_with_invalid_email_returns_validation_errors()
  359. {
  360. $this->json('POST', '/webauthn/login/options', [
  361. 'email' => 'invalid',
  362. ])
  363. ->assertStatus(422)
  364. ->assertJsonValidationErrors([
  365. 'email',
  366. ]);
  367. }
  368. #[Test]
  369. public function test_get_options_with_unknown_email_returns_validation_errors()
  370. {
  371. $this->json('POST', '/webauthn/login/options', [
  372. 'email' => 'john@example.com',
  373. ])
  374. ->assertStatus(422)
  375. ->assertJsonValidationErrors([
  376. 'email',
  377. ]);
  378. }
  379. #[Test]
  380. public function test_successful_webauthn_login_is_logged()
  381. {
  382. $this->createWebauthnCredential(self::CREDENTIAL_ID_ALT, $this->user->id, self::USER_ID_ALT);
  383. $this->addWebauthnChallengeToSession();
  384. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE)
  385. ->assertOk();
  386. $this->assertDatabaseHas('auth_logs', [
  387. 'authenticatable_id' => $this->user->id,
  388. 'login_successful' => true,
  389. 'guard' => self::GUARD,
  390. 'login_method' => self::AUTH_METHOD,
  391. 'logout_at' => null,
  392. ]);
  393. }
  394. #[Test]
  395. public function test_failed_webauthn_login_is_not_logged()
  396. {
  397. $this->addWebauthnChallengeToSession();
  398. $this->json('POST', '/webauthn/login', self::ASSERTION_RESPONSE_INVALID)
  399. ->assertUnauthorized();
  400. // When webauthn fails, the fireFailedEvent() of the sessionGuard returns
  401. // a null $user so nothing should be logged
  402. $this->assertDatabaseMissing('auth_logs', [
  403. 'authenticatable_id' => $this->user->id,
  404. 'guard' => self::GUARD,
  405. 'login_method' => self::AUTH_METHOD,
  406. ]);
  407. }
  408. /**
  409. * Set a session
  410. */
  411. protected function addWebauthnChallengeToSession() : void
  412. {
  413. $this->session(['_webauthn' => new \Laragear\WebAuthn\Challenge(
  414. new \Laragear\WebAuthn\ByteBuffer(base64_decode(self::ASSERTION_CHALLENGE)),
  415. 60,
  416. false,
  417. )]);
  418. }
  419. /**
  420. * Inserts a webauthn credential in database
  421. */
  422. protected function createWebauthnCredential(string $credentialId, int $authenticatableId, string $userId) : void
  423. {
  424. DB::table('webauthn_credentials')->insert([
  425. 'id' => $credentialId,
  426. 'authenticatable_type' => \App\Models\User::class,
  427. 'authenticatable_id' => $authenticatableId,
  428. 'user_id' => $userId,
  429. 'counter' => 0,
  430. 'rp_id' => 'http://localhost',
  431. 'origin' => 'http://localhost',
  432. 'aaguid' => '00000000-0000-0000-0000-000000000000',
  433. 'attestation_format' => 'none',
  434. 'public_key' => self::PUBLIC_KEY,
  435. 'updated_at' => now(),
  436. 'created_at' => now(),
  437. ]);
  438. }
  439. }