├── resources ├── views │ ├── .gitkeep │ ├── livewire │ │ ├── webauthn-redirect-to-login-button.blade.php │ │ ├── webauthn-register-button.blade.php │ │ └── webauthn-login.blade.php │ ├── filament │ │ └── widgets │ │ │ └── webauthn-register.blade.php │ └── components │ │ └── login-form-extension.blade.php ├── assets │ ├── dist │ │ ├── mix-manifest.json │ │ └── filament-webauthn.js │ └── js │ │ └── filament-webauthn.js └── lang │ └── en │ └── filament-webauthn.php ├── images ├── login.png ├── widget.png └── reditect-to-login-page.png ├── CHANGELOG.md ├── src ├── FilamentWebauthn.php ├── Exceptions │ ├── LoginException.php │ └── RegistrationException.php ├── Auth │ ├── UserInterface.php │ ├── RegistratorInterface.php │ ├── AuthenticatorInterface.php │ ├── User.php │ ├── Authenticator.php │ └── Registrator.php ├── Repositories │ ├── UserRepositoryInterface.php │ ├── UserRepository.php │ └── PublicKeyRepository.php ├── Facades │ └── FilamentWebauthn.php ├── Widgets │ └── WebauthnRegisterWidget.php ├── Models │ └── WebauthnKey.php ├── Http │ └── Livewire │ │ ├── WebauthnRedirectToLoginButton.php │ │ ├── WebauthnRegisterButton.php │ │ └── WebauthnLogin.php ├── FilamentWebauthnServiceProvider.php └── Factories │ └── WebauthnFactory.php ├── package.json ├── database ├── factories │ └── ModelFactory.php └── migrations │ └── create_filament_webauthn_table.php.stub ├── routes └── web.php ├── webpack.mix.js ├── config └── filament-webauthn.php ├── LICENSE.md ├── composer.json └── README.md /resources/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontechs/filament-webauthn/HEAD/images/login.png -------------------------------------------------------------------------------- /images/widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontechs/filament-webauthn/HEAD/images/widget.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `filament-webauthn` will be documented in this file. 4 | -------------------------------------------------------------------------------- /src/FilamentWebauthn.php: -------------------------------------------------------------------------------- 1 | 7 | {{ __('filament-webauthn::filament-webauthn.login-button-text') }} 8 | 9 | -------------------------------------------------------------------------------- /resources/views/filament/widgets/webauthn-register.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('filament-webauthn::filament-webauthn.widget.title') }} 5 |

6 | 7 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | middleware(config('filament.middleware.base')) 7 | ->prefix(config('filament.path')) 8 | ->name('filament-webauthn.') 9 | ->group(function (): void { 10 | Route::get('/webauthn-login', \Moontechs\FilamentWebauthn\Http\Livewire\WebauthnLogin::class) 11 | ->name('login'); 12 | }); 13 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require("laravel-mix"); 2 | 3 | mix.disableNotifications(); 4 | 5 | mix.disableSuccessNotifications() 6 | 7 | mix.options({ 8 | terser: { 9 | extractComments: false, 10 | }, 11 | }) 12 | 13 | mix.setPublicPath("resources/assets/dist") 14 | mix.version() 15 | 16 | mix.js("./resources/assets/js/filament-webauthn.js", "./resources/assets/dist") 17 | 18 | mix.options({ 19 | processCssUrls: false, 20 | }); 21 | -------------------------------------------------------------------------------- /src/Widgets/WebauthnRegisterWidget.php: -------------------------------------------------------------------------------- 1 | columnSpan = config('filament-webauthn.widget.column_span', ''); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /resources/views/components/login-form-extension.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('filament-webauthn::filament-webauthn.or') }} 5 |

6 |
7 | 8 |
9 | 10 |
11 | -------------------------------------------------------------------------------- /src/Auth/User.php: -------------------------------------------------------------------------------- 1 | user(); 16 | 17 | return $user->getAttributeValue(config('filament-webauthn.user.auth_identifier')); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Repositories/UserRepository.php: -------------------------------------------------------------------------------- 1 | where(config('filament-webauthn.user.auth_identifier'), '=', $credentialId) 13 | ->first(['id']); 14 | 15 | return $userEntity?->id; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Models/WebauthnKey.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => 'Register this device using WebAuth', 6 | ], 7 | 'notifications' => [ 8 | 'unsupported' => 'This app is not supported', 9 | 'registration' => [ 10 | 'success' => 'This device registered successfully', 11 | 'error' => 'Could not register this device', 12 | ], 13 | 'authentication' => [ 14 | 'success' => 'This device registered successfully', 15 | 'error' => 'Could not login using this device', 16 | ], 17 | ], 18 | 'register-button-text' => 'Register', 19 | 'login-button-text' => 'Sign in using this device', 20 | 'or' => 'Or', 21 | ]; 22 | -------------------------------------------------------------------------------- /src/Http/Livewire/WebauthnRedirectToLoginButton.php: -------------------------------------------------------------------------------- 1 | icon = config('filament-webauthn.login_button.icon', ''); 16 | } 17 | 18 | public function render() 19 | { 20 | return view('filament-webauthn::livewire.webauthn-redirect-to-login-button'); 21 | } 22 | 23 | public function redirectToLoginPage(): RedirectResponse|Redirector 24 | { 25 | return redirect()->route('filament-webauthn.login'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/assets/js/filament-webauthn.js: -------------------------------------------------------------------------------- 1 | import { 2 | create, 3 | get, 4 | parseCreationOptionsFromJSON, 5 | parseRequestOptionsFromJSON, 6 | supported, 7 | } from "@github/webauthn-json/browser-ponyfill"; 8 | 9 | const FilamentWebauthn = { 10 | create: async function (options) { 11 | return create(options); 12 | }, 13 | 14 | parseCreationOptionsFromJSON: function (requestJSON) { 15 | return parseCreationOptionsFromJSON(requestJSON); 16 | }, 17 | 18 | parseRequestOptionsFromJSON: function (requestJSON) { 19 | return parseRequestOptionsFromJSON(requestJSON); 20 | }, 21 | 22 | get: async function (options) { 23 | return get(options); 24 | }, 25 | 26 | supported: function () { 27 | return supported(); 28 | } 29 | } 30 | window.FilamentWebauthn = FilamentWebauthn 31 | -------------------------------------------------------------------------------- /resources/views/livewire/webauthn-register-button.blade.php: -------------------------------------------------------------------------------- 1 | 7 | {{ __('filament-webauthn::filament-webauthn.register-button-text') }} 8 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /database/migrations/create_filament_webauthn_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('credential_id'); 14 | $table->mediumText('public_key'); 15 | $table->mediumText('user_handle'); 16 | $table->foreignId('user_id'); 17 | $table->timestamps(); 18 | 19 | $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); 20 | }); 21 | } 22 | 23 | public function down(): void 24 | { 25 | Schema::table('webauthn_keys', function (Blueprint $table) { 26 | if (config('database.default') !== 'sqlite') { 27 | $table->dropConstrainedForeignId('user_id'); 28 | } 29 | }); 30 | Schema::dropIfExists('webauthn_keys'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /config/filament-webauthn.php: -------------------------------------------------------------------------------- 1 | '/webauthn-login', 6 | 'user' => [ 7 | 'auth_identifier' => 'email', // column in users table with unique user id 8 | ], 9 | 'widget' => [ 10 | 'column_span' => '', 11 | ], 12 | 'register_button' => [ 13 | 'icon' => 'heroicon-o-key', 14 | 'class' => 'w-full', 15 | ], 16 | 'login_button' => [ 17 | 'icon' => 'heroicon-o-key', 18 | 'class' => 'w-full', 19 | ], 20 | 'auth' => [ 21 | 'relying_party' => [ 22 | 'name' => env('APP_NAME'), 23 | 'origin' => env('APP_URL'), 24 | 'id' => env('APP_HOST', parse_url(env('APP_URL'))['host']), 25 | ], 26 | 'client_options' => [ 27 | 'timeout' => 60000, 28 | 'platform' => '', // available: platform, cross-platform, or leave empty 29 | 'attestation' => 'direct', // available: direct, indirect, none 30 | 'user_verification' => 'required', // available: required, preferred, discouraged 31 | ], 32 | ], 33 | ]; 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Moontechs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/views/livewire/webauthn-login.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ $this->form }} 3 | 4 | 11 | {{ __('filament::login.buttons.submit.label') }} 12 | 13 |
14 | 15 | 33 | -------------------------------------------------------------------------------- /src/FilamentWebauthnServiceProvider.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/../resources/assets/dist/filament-webauthn.js', 16 | ]; 17 | 18 | public function configurePackage(Package $package): void 19 | { 20 | $package 21 | ->name('filament-webauthn') 22 | ->hasConfigFile() 23 | ->hasViews() 24 | ->hasRoutes('web') 25 | ->hasTranslations() 26 | ->hasAssets() 27 | ->publishesServiceProvider('FilamentWebauthnServiceProvider') 28 | ->hasMigration('create_filament_webauthn_table'); 29 | } 30 | 31 | public function boot() 32 | { 33 | $serviceProvider = parent::boot(); 34 | Livewire::component('webauthn-register-button', WebauthnRegisterButton::class); 35 | Livewire::component('webauthn-login', WebauthnLogin::class); 36 | Livewire::component('webauthn-redirect-to-login-button', WebauthnRedirectToLoginButton::class); 37 | 38 | return $serviceProvider; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Repositories/PublicKeyRepository.php: -------------------------------------------------------------------------------- 1 | toString())->first(); 21 | 22 | if ($webauthnKey === null) { 23 | return null; 24 | } 25 | 26 | /** 27 | * @var CoseKeyInterface $key 28 | */ 29 | $key = unserialize(base64_decode($webauthnKey->public_key)); 30 | 31 | return new UserCredential( 32 | $credentialId, 33 | $key, 34 | UserHandle::fromString($webauthnKey->user_handle) 35 | ); 36 | } 37 | 38 | public function getSignatureCounter(CredentialId $credentialId): ?int 39 | { 40 | return null; 41 | } 42 | 43 | public function updateSignatureCounter(CredentialId $credentialId, int $counter): void 44 | { 45 | } 46 | 47 | public function getUserCredentialIds(UserHandle $userHandle): array 48 | { 49 | $webauthnKeys = WebauthnKey::where('user_handle', $userHandle->toString()) 50 | ->get(); 51 | 52 | if ($webauthnKeys->count() === 0) { 53 | return []; 54 | } 55 | 56 | return $webauthnKeys->pluck('credential_id')->map(function ($item, $key) { 57 | return CredentialId::fromString($item); 58 | })->all(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moontechs/filament-webauthn", 3 | "description": "Filament webauthn sign in and registration", 4 | "keywords": [ 5 | "moontechs", 6 | "laravel", 7 | "filament-webauthn", 8 | "webauthn", 9 | "fido" 10 | ], 11 | "homepage": "https://github.com/moontechs/filament-webauthn", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Michael Kozii", 16 | "email": "michael.kozii.mzlqe@aleeas.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.0", 22 | "ext-openssl": "*", 23 | "ext-json": "*", 24 | "ext-sodium": "*", 25 | "ext-gmp": "*", 26 | "filament/filament": "^2.0", 27 | "illuminate/contracts": "^9.0", 28 | "madwizard/webauthn": "^0.8.1", 29 | "spatie/laravel-package-tools": "^1.13.0" 30 | }, 31 | "require-dev": { 32 | "laravel/pint": "^1.0", 33 | "nunomaduro/collision": "^6.0", 34 | "nunomaduro/larastan": "^2.0.1", 35 | "orchestra/testbench": "^7.0", 36 | "phpstan/extension-installer": "^1.1", 37 | "phpstan/phpstan-deprecation-rules": "^1.0", 38 | "phpstan/phpstan-phpunit": "^1.0", 39 | "phpunit/phpunit": "^9.5" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Moontechs\\FilamentWebauthn\\": "src", 44 | "Moontechs\\FilamentWebauthn\\Database\\Factories\\": "database/factories" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Moontechs\\FilamentWebauthn\\Tests\\": "tests" 50 | } 51 | }, 52 | "scripts": { 53 | "analyse": "vendor/bin/phpstan analyse", 54 | "test": "vendor/bin/phpunit", 55 | "format": "vendor/bin/pint" 56 | }, 57 | "config": { 58 | "sort-packages": true, 59 | "allow-plugins": { 60 | "phpstan/extension-installer": true 61 | } 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "providers": [ 66 | "Moontechs\\FilamentWebauthn\\FilamentWebauthnServiceProvider" 67 | ], 68 | "aliases": { 69 | "FilamentWebauthn": "Moontechs\\FilamentWebauthn\\Facades\\FilamentWebauthn" 70 | } 71 | } 72 | }, 73 | "minimum-stability": "dev", 74 | "prefer-stable": true 75 | } 76 | -------------------------------------------------------------------------------- /src/Http/Livewire/WebauthnRegisterButton.php: -------------------------------------------------------------------------------- 1 | icon = config('filament-webauthn.register_button.icon', ''); 21 | $this->class = config('filament-webauthn.register_button.class', ''); 22 | 23 | if (config('check_if_supported')) { 24 | $this->emitSelf('supported'); 25 | } 26 | } 27 | 28 | public function render() 29 | { 30 | return view('filament-webauthn::livewire.webauthn-register-button'); 31 | } 32 | 33 | public function register(): void 34 | { 35 | $this->emitSelf('register', (new WebauthnFactory())->createRegistrator()->getClientOptions()); 36 | } 37 | 38 | public function notifyUnsupported(): void 39 | { 40 | Notification::make() 41 | ->title(__('filament-webauthn::filament-webauthn.notifications.registration.error')) 42 | ->body(__('filament-webauthn::filament-webauthn.notifications.unsupported')) 43 | ->danger() 44 | ->send(); 45 | } 46 | 47 | public function notifyError(string $text): void 48 | { 49 | Notification::make() 50 | ->title(__('filament-webauthn::filament-webauthn.notifications.registration.error')) 51 | ->body($text) 52 | ->danger() 53 | ->send(); 54 | } 55 | 56 | public function validateAndRegister(string $data): void 57 | { 58 | try { 59 | (new WebauthnFactory())->createRegistrator()->validateAndRegister($data); 60 | Notification::make() 61 | ->title(__('filament-webauthn::filament-webauthn.notifications.registration.success')) 62 | ->success() 63 | ->send(); 64 | } catch (RegistrationException $exception) { 65 | Notification::make() 66 | ->title(__('filament-webauthn::filament-webauthn.notifications.registration.error')) 67 | ->body($exception->getMessage()) 68 | ->danger() 69 | ->send(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Factories/WebauthnFactory.php: -------------------------------------------------------------------------------- 1 | createServer(), 29 | $this->createRegistrationOptions( 30 | $this->createUserIdentity() 31 | ), 32 | Filament::auth()->user() 33 | ); 34 | } 35 | 36 | public function createAuthenticator(string $loginId): AuthenticatorInterface 37 | { 38 | return new Authenticator( 39 | $this->createServer(), 40 | $this->createAuthenticationOptions( 41 | $this->createUserHandle($loginId) 42 | ), 43 | new UserRepository() 44 | ); 45 | } 46 | 47 | public function createUserIdentity(): UserIdentity 48 | { 49 | $userName = $this->createUser()->getUserLoginIdentificator(); 50 | 51 | return new UserIdentity( 52 | UserHandle::fromString(base64_encode($userName)), 53 | $userName, 54 | $userName 55 | ); 56 | } 57 | 58 | public function createUserHandle(string $loginId): UserHandle 59 | { 60 | return UserHandle::fromString( 61 | base64_encode($loginId) 62 | ); 63 | } 64 | 65 | public function createRegistrationOptions(UserIdentity $user): RegistrationOptions 66 | { 67 | $registrationOptions = RegistrationOptions::createForUser($user); 68 | $registrationOptions->setExcludeExistingCredentials(true); 69 | 70 | return $registrationOptions; 71 | } 72 | 73 | public function createAuthenticationOptions(UserHandle $user): AuthenticationOptions 74 | { 75 | return AuthenticationOptions::createForUser($user); 76 | } 77 | 78 | public function createServer(): ServerInterface 79 | { 80 | $relyingParty = new RelyingParty( 81 | Config::get('filament-webauthn.auth.relying_party.name'), 82 | Config::get('filament-webauthn.auth.relying_party.origin') 83 | ); 84 | $relyingParty->setId(Config::get('filament-webauthn.auth.relying_party.id')); 85 | 86 | return (new ServerBuilder()) 87 | ->setRelyingParty($relyingParty) 88 | ->setCredentialStore(new PublicKeyRepository()) 89 | ->build(); 90 | } 91 | 92 | protected function createUser(): UserInterface 93 | { 94 | return new User(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Auth/Authenticator.php: -------------------------------------------------------------------------------- 1 | server = $server; 29 | $this->authenticationOptions = $authenticationOptions; 30 | $this->userRepository = $userRepository; 31 | } 32 | 33 | public function getClientOptions(): string 34 | { 35 | try { 36 | if (! empty(Config::get('filament-webauthn.auth.client_options.user_verification'))) { 37 | $this->authenticationOptions->setUserVerification(Config::get('filament-webauthn.auth.client_options.user_verification')); 38 | } 39 | $this->authenticationOptions->setTimeout(Config::get('filament-webauthn.auth.client_options.timeout')); 40 | $authenticationRequest = $this->server->startAuthentication($this->authenticationOptions); 41 | 42 | Session::put( 43 | $this->getSessionKey( 44 | $this->authenticationOptions->getUserHandle()->toString() 45 | ), 46 | $authenticationRequest->getContext() 47 | ); 48 | 49 | return json_encode($authenticationRequest->getClientOptionsJson()); 50 | } catch (\Throwable $exception) { 51 | throw new LoginException(); 52 | } 53 | } 54 | 55 | public function validateAndLogin(string $data, bool $remember = false) 56 | { 57 | try { 58 | $authenticationResult = $this->server->finishAuthentication( 59 | JsonConverter::decodeCredential(json_decode($data, true), 'assertion'), 60 | Session::get($this->getSessionKey($this->authenticationOptions->getUserHandle()->toString())) 61 | ); 62 | 63 | if (! $authenticationResult->isUserVerified()) { 64 | return false; 65 | } 66 | Filament::auth()->loginUsingId( 67 | $this->userRepository->getUserIdByCredentialId( 68 | base64_decode($this->authenticationOptions->getUserHandle()->toString()) 69 | ), 70 | $remember 71 | ); 72 | Session::forget($this->getSessionKey($this->authenticationOptions->getUserHandle()->toString())); 73 | } catch (WebAuthnException $exception) { 74 | throw new LoginException($exception->getMessage()); 75 | } catch (\Throwable $throwable) { 76 | throw new LoginException(); 77 | } 78 | } 79 | 80 | private function getSessionKey(string $userHandle): string 81 | { 82 | return 'filament:webauthn:login:'.$userHandle; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Auth/Registrator.php: -------------------------------------------------------------------------------- 1 | server = $server; 29 | $this->registrationOptions = $registrationOptions; 30 | $this->user = $user; 31 | } 32 | 33 | public function getClientOptions(): string 34 | { 35 | $this->registrationOptions->setTimeout(Config::get('filament-webauthn.auth.client_options.timeout')); 36 | 37 | if (! empty(Config::get('filament-webauthn.auth.client_options.user_verification'))) { 38 | $this->registrationOptions->setUserVerification(Config::get('filament-webauthn.auth.client_options.user_verification')); 39 | } 40 | 41 | if (! empty(Config::get('filament-webauthn.auth.client_options.attestation'))) { 42 | $this->registrationOptions->setAttestation(Config::get('filament-webauthn.auth.client_options.attestation')); 43 | } 44 | 45 | if (! empty(Config::get('filament-webauthn.auth.client_options.platform'))) { 46 | $this->registrationOptions->setAuthenticatorAttachment(Config::get('filament-webauthn.auth.client_options.platform')); 47 | } 48 | $registrationRequest = $this->server->startRegistration($this->registrationOptions); 49 | 50 | Session::put($this->getSessionKey(), $registrationRequest->getContext()); 51 | 52 | return json_encode($registrationRequest->getClientOptionsJson()); 53 | } 54 | 55 | public function validateAndRegister(string $data) 56 | { 57 | try { 58 | $registrationResult = $this->server->finishRegistration( 59 | JsonConverter::decodeAttestation(json_decode($data, true)), 60 | Session::get($this->getSessionKey()) 61 | ); 62 | 63 | if (! $registrationResult->isUserVerified()) { 64 | throw new RegistrationException(); 65 | } 66 | WebauthnKey::create([ 67 | 'credential_id' => $registrationResult->getCredentialId()->toString(), 68 | 'user_id' => $this->user->getAuthIdentifier(), 69 | 'public_key' => base64_encode(serialize($registrationResult->getPublicKey())), 70 | 'user_handle' => $registrationResult->getUserHandle()->toString(), 71 | ]); 72 | Session::forget($this->getSessionKey()); 73 | } catch (WebAuthnException $exception) { 74 | throw new RegistrationException($exception->getMessage()); 75 | } catch (\Throwable $throwable) { 76 | throw new RegistrationException(); 77 | } 78 | 79 | return true; 80 | } 81 | 82 | private function getSessionKey(): string 83 | { 84 | return 'filament:webauthn:register:'.$this->user->getAuthIdentifier(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Http/Livewire/WebauthnLogin.php: -------------------------------------------------------------------------------- 1 | check()) { 39 | redirect()->intended(Filament::getUrl()); 40 | } 41 | $this->icon = config('filament-webauthn.login_button.icon', ''); 42 | $this->class = config('filament-webauthn.login_button.class', ''); 43 | 44 | if (config('check_if_supported')) { 45 | $this->emitSelf('supported'); 46 | } 47 | 48 | $this->form->fill(); 49 | } 50 | 51 | public function getClientOptions(): void 52 | { 53 | $formState = $this->form->getState(); 54 | $this->validate([ 55 | 'email' => 'required|email', 56 | ]); 57 | 58 | try { 59 | $this->clientOptions = (new WebauthnFactory()) 60 | ->createAuthenticator($formState['email']) 61 | ->getClientOptions(); 62 | } catch (LoginException $exception) { 63 | throw ValidationException::withMessages([ 64 | 'email' => __('filament::login.messages.failed'), 65 | ]); 66 | } 67 | 68 | $this->emitSelf('clientOptions'); 69 | } 70 | 71 | public function authenticate(string $data): ?LoginResponse 72 | { 73 | try { 74 | (new WebauthnFactory()) 75 | ->createAuthenticator($this->email) 76 | ->validateAndLogin($data, $this->remember); 77 | } catch (LoginException $exception) { 78 | throw ValidationException::withMessages([ 79 | 'email' => __('filament::login.messages.failed'), 80 | ])->redirectTo(config('filament-webauthn.login_page_url')); 81 | } 82 | 83 | return app(LoginResponse::class); 84 | } 85 | 86 | public function notifyUnsupported(): void 87 | { 88 | Notification::make() 89 | ->title(__('filament-webauthn::filament-webauthn.notifications.authentication.error')) 90 | ->body(__('filament-webauthn::filament-webauthn.notifications.unsupported')) 91 | ->danger() 92 | ->send(); 93 | } 94 | 95 | public function notifyError(string $text): void 96 | { 97 | Notification::make() 98 | ->title(__('filament-webauthn::filament-webauthn.notifications.authentication.error')) 99 | ->body($text) 100 | ->danger() 101 | ->send(); 102 | } 103 | 104 | protected function getFormSchema(): array 105 | { 106 | return [ 107 | TextInput::make('email') 108 | ->label(__('filament::login.fields.email.label')) 109 | ->email() 110 | ->required() 111 | ->autocomplete(), 112 | Checkbox::make('remember') 113 | ->label(__('filament::login.fields.remember.label')), 114 | ]; 115 | } 116 | 117 | public function render(): View 118 | { 119 | return view('filament-webauthn::livewire.webauthn-login') 120 | ->layout('filament::components.layouts.card', [ 121 | 'title' => __('filament::login.title'), 122 | ]); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filament Webauthn Authentication (FIDO) 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/moontechs/filament-webauthn.svg?style=flat-square)](https://packagist.org/packages/moontechs/filament-webauthn) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/moontechs/filament-webauthn/.github/workflows/run-tests.yml?branch=main)](https://github.com/moontechs/filament-webauthn/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/moontechs/filament-webauthn/.github/workflows/fix-php-code-style-issues.yml?branch=main)](https://github.com/moontechs/filament-webauthn/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/moontechs/filament-webauthn.svg?style=flat-square)](https://packagist.org/packages/moontechs/filament-webauthn) 7 | 8 | Passwordless login for your Filament app. Web Authentication server-side and front-end components. 9 | 10 | The package has the following components: 11 | * registration button and widget 12 | * login form extension to redirect to the webauthn login page 13 | * separate route and page with webauthn login form 14 | 15 | Should work with HTTPS and not localhost only. 16 | 17 | ## Installation 18 | 19 | You can install the package via composer: 20 | 21 | ```bash 22 | composer require moontechs/filament-webauthn 23 | ``` 24 | 25 | You should publish and run the migrations with: 26 | 27 | ```bash 28 | php artisan vendor:publish --tag="filament-webauthn-migrations" 29 | php artisan migrate 30 | ``` 31 | 32 | You can publish the config file with: 33 | 34 | ```bash 35 | php artisan vendor:publish --tag="filament-webauthn-config" 36 | ``` 37 | 38 | This is the contents of the published config file: 39 | 40 | ```php 41 | return [ 42 | 'login_page_url' => '/webauthn-login', 43 | 'user' => [ 44 | 'auth_identifier' => 'email', // column in users table with unique user id 45 | ], 46 | 'widget' => [ 47 | 'column_span' => '', 48 | ], 49 | 'register_button' => [ 50 | 'icon' => 'heroicon-o-key', 51 | 'class' => 'w-full', 52 | ], 53 | 'login_button' => [ 54 | 'icon' => 'heroicon-o-key', 55 | 'class' => 'w-full', 56 | ], 57 | 'auth' => [ 58 | 'relying_party' => [ 59 | 'name' => env('APP_NAME'), 60 | 'origin' => env('APP_URL'), 61 | 'id' => env('APP_HOST', parse_url(env('APP_URL'))['host']), 62 | ], 63 | 'client_options' => [ 64 | 'timeout' => 60000, 65 | 'platform' => '', // available: platform, cross-platform, or leave empty 66 | 'attestation' => 'direct', // available: direct, indirect, none 67 | 'user_verification' => 'required', // available: required, preferred, discouraged 68 | ], 69 | ], 70 | ]; 71 | ``` 72 | 73 | Optionally, you can publish the views using 74 | 75 | ```bash 76 | php artisan vendor:publish --tag="filament-webauthn-views" 77 | ``` 78 | 79 | You can publish the translation file with: 80 | 81 | ```bash 82 | php artisan vendor:publish --tag="filament-webauthn-translations" 83 | ``` 84 | 85 | ## Usage 86 | 87 | * Install the package. 88 | * Publish migrations and migrate. 89 | 90 | ### Registration widget 91 | Only signed-in users can register a device to be able to sign in to use it in the future. 92 | 93 | * Register `Moontechs\FilamentWebauthn\Widgets\WebauthnRegisterWidget::class` widget. 94 | Add it to the `widgets.register` array of the Filament config. 95 | 96 | ![widget](images/widget.png?raw=true) 97 | 98 | #### Customization 99 | * Publish the config file 100 | * `widget.column_span` - widget width ([docs](https://filamentphp.com/docs/2.x/admin/dashboard/getting-started#customizing-widget-width)) 101 | 102 | ### Registration button (without widget) 103 | * Add `` in any view. 104 | 105 | #### Customization 106 | * Publish the config file 107 | * `register_button.icon` - choose any available icon 108 | * `register_button.class` - add more classes or change the default one 109 | 110 | ### Redirect to the login page button 111 | 112 | * Publish Filament login page view `php artisan vendor:publish --tag=filament-views` 113 | * Add `` in the end of the login form. 114 | 115 | If you didn't want to use this button, you can use a simple redirect to a named route `filament-webauthn.login`. 116 | 117 | ![redirect to login page](images/reditect-to-login-page.png?raw=true) 118 | 119 | ### Login form 120 | #### Customization 121 | * Publish the config file 122 | * `login_button.icon` - choose any available icon 123 | * `login_button.class` - add more classes or change the default one 124 | 125 | ![login](images/login.png?raw=true) 126 | 127 | ## Testing 128 | 129 | ```bash 130 | composer test 131 | ``` 132 | 133 | ## Credits 134 | 135 | - [Michael Kozii](https://github.com/mkoziy) 136 | - [All Contributors](../../contributors) 137 | 138 | ## License 139 | 140 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 141 | -------------------------------------------------------------------------------- /resources/assets/dist/filament-webauthn.js: -------------------------------------------------------------------------------- 1 | /******/ (() => { // webpackBootstrap 2 | /******/ "use strict"; 3 | /******/ var __webpack_modules__ = ({ 4 | 5 | /***/ "./node_modules/@github/webauthn-json/dist/esm/webauthn-json.browser-ponyfill.js": 6 | /*!***************************************************************************************!*\ 7 | !*** ./node_modules/@github/webauthn-json/dist/esm/webauthn-json.browser-ponyfill.js ***! 8 | \***************************************************************************************/ 9 | /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { 10 | 11 | __webpack_require__.r(__webpack_exports__); 12 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 13 | /* harmony export */ "create": () => (/* binding */ create), 14 | /* harmony export */ "get": () => (/* binding */ get), 15 | /* harmony export */ "parseCreationOptionsFromJSON": () => (/* binding */ createRequestFromJSON), 16 | /* harmony export */ "parseRequestOptionsFromJSON": () => (/* binding */ getRequestFromJSON), 17 | /* harmony export */ "supported": () => (/* binding */ supported) 18 | /* harmony export */ }); 19 | // src/webauthn-json/base64url.ts 20 | function base64urlToBuffer(baseurl64String) { 21 | const padding = "==".slice(0, (4 - baseurl64String.length % 4) % 4); 22 | const base64String = baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding; 23 | const str = atob(base64String); 24 | const buffer = new ArrayBuffer(str.length); 25 | const byteView = new Uint8Array(buffer); 26 | for (let i = 0; i < str.length; i++) { 27 | byteView[i] = str.charCodeAt(i); 28 | } 29 | return buffer; 30 | } 31 | function bufferToBase64url(buffer) { 32 | const byteView = new Uint8Array(buffer); 33 | let str = ""; 34 | for (const charCode of byteView) { 35 | str += String.fromCharCode(charCode); 36 | } 37 | const base64String = btoa(str); 38 | const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); 39 | return base64urlString; 40 | } 41 | 42 | // src/webauthn-json/convert.ts 43 | var copyValue = "copy"; 44 | var convertValue = "convert"; 45 | function convert(conversionFn, schema, input) { 46 | if (schema === copyValue) { 47 | return input; 48 | } 49 | if (schema === convertValue) { 50 | return conversionFn(input); 51 | } 52 | if (schema instanceof Array) { 53 | return input.map((v) => convert(conversionFn, schema[0], v)); 54 | } 55 | if (schema instanceof Object) { 56 | const output = {}; 57 | for (const [key, schemaField] of Object.entries(schema)) { 58 | if (schemaField.derive) { 59 | const v = schemaField.derive(input); 60 | if (v !== void 0) { 61 | input[key] = v; 62 | } 63 | } 64 | if (!(key in input)) { 65 | if (schemaField.required) { 66 | throw new Error(`Missing key: ${key}`); 67 | } 68 | continue; 69 | } 70 | if (input[key] == null) { 71 | output[key] = null; 72 | continue; 73 | } 74 | output[key] = convert(conversionFn, schemaField.schema, input[key]); 75 | } 76 | return output; 77 | } 78 | } 79 | function derived(schema, derive) { 80 | return { 81 | required: true, 82 | schema, 83 | derive 84 | }; 85 | } 86 | function required(schema) { 87 | return { 88 | required: true, 89 | schema 90 | }; 91 | } 92 | function optional(schema) { 93 | return { 94 | required: false, 95 | schema 96 | }; 97 | } 98 | 99 | // src/webauthn-json/basic/schema.ts 100 | var publicKeyCredentialDescriptorSchema = { 101 | type: required(copyValue), 102 | id: required(convertValue), 103 | transports: optional(copyValue) 104 | }; 105 | var simplifiedExtensionsSchema = { 106 | appid: optional(copyValue), 107 | appidExclude: optional(copyValue), 108 | credProps: optional(copyValue) 109 | }; 110 | var simplifiedClientExtensionResultsSchema = { 111 | appid: optional(copyValue), 112 | appidExclude: optional(copyValue), 113 | credProps: optional(copyValue) 114 | }; 115 | var credentialCreationOptions = { 116 | publicKey: required({ 117 | rp: required(copyValue), 118 | user: required({ 119 | id: required(convertValue), 120 | name: required(copyValue), 121 | displayName: required(copyValue) 122 | }), 123 | challenge: required(convertValue), 124 | pubKeyCredParams: required(copyValue), 125 | timeout: optional(copyValue), 126 | excludeCredentials: optional([publicKeyCredentialDescriptorSchema]), 127 | authenticatorSelection: optional(copyValue), 128 | attestation: optional(copyValue), 129 | extensions: optional(simplifiedExtensionsSchema) 130 | }), 131 | signal: optional(copyValue) 132 | }; 133 | var publicKeyCredentialWithAttestation = { 134 | type: required(copyValue), 135 | id: required(copyValue), 136 | rawId: required(convertValue), 137 | authenticatorAttachment: optional(copyValue), 138 | response: required({ 139 | clientDataJSON: required(convertValue), 140 | attestationObject: required(convertValue), 141 | transports: derived(copyValue, (response) => { 142 | var _a; 143 | return ((_a = response.getTransports) == null ? void 0 : _a.call(response)) || []; 144 | }) 145 | }), 146 | clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults()) 147 | }; 148 | var credentialRequestOptions = { 149 | mediation: optional(copyValue), 150 | publicKey: required({ 151 | challenge: required(convertValue), 152 | timeout: optional(copyValue), 153 | rpId: optional(copyValue), 154 | allowCredentials: optional([publicKeyCredentialDescriptorSchema]), 155 | userVerification: optional(copyValue), 156 | extensions: optional(simplifiedExtensionsSchema) 157 | }), 158 | signal: optional(copyValue) 159 | }; 160 | var publicKeyCredentialWithAssertion = { 161 | type: required(copyValue), 162 | id: required(copyValue), 163 | rawId: required(convertValue), 164 | authenticatorAttachment: optional(copyValue), 165 | response: required({ 166 | clientDataJSON: required(convertValue), 167 | authenticatorData: required(convertValue), 168 | signature: required(convertValue), 169 | userHandle: required(convertValue) 170 | }), 171 | clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults()) 172 | }; 173 | 174 | // src/webauthn-json/basic/api.ts 175 | function createRequestFromJSON(requestJSON) { 176 | return convert(base64urlToBuffer, credentialCreationOptions, requestJSON); 177 | } 178 | function createResponseToJSON(credential) { 179 | return convert(bufferToBase64url, publicKeyCredentialWithAttestation, credential); 180 | } 181 | function getRequestFromJSON(requestJSON) { 182 | return convert(base64urlToBuffer, credentialRequestOptions, requestJSON); 183 | } 184 | function getResponseToJSON(credential) { 185 | return convert(bufferToBase64url, publicKeyCredentialWithAssertion, credential); 186 | } 187 | 188 | // src/webauthn-json/basic/supported.ts 189 | function supported() { 190 | return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential); 191 | } 192 | 193 | // src/webauthn-json/browser-ponyfill.ts 194 | async function create(options) { 195 | const response = await navigator.credentials.create(options); 196 | response.toJSON = () => createResponseToJSON(response); 197 | return response; 198 | } 199 | async function get(options) { 200 | const response = await navigator.credentials.get(options); 201 | response.toJSON = () => getResponseToJSON(response); 202 | return response; 203 | } 204 | 205 | //# sourceMappingURL=webauthn-json.browser-ponyfill.js.map 206 | 207 | 208 | /***/ }) 209 | 210 | /******/ }); 211 | /************************************************************************/ 212 | /******/ // The module cache 213 | /******/ var __webpack_module_cache__ = {}; 214 | /******/ 215 | /******/ // The require function 216 | /******/ function __webpack_require__(moduleId) { 217 | /******/ // Check if module is in cache 218 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 219 | /******/ if (cachedModule !== undefined) { 220 | /******/ return cachedModule.exports; 221 | /******/ } 222 | /******/ // Create a new module (and put it into the cache) 223 | /******/ var module = __webpack_module_cache__[moduleId] = { 224 | /******/ // no module.id needed 225 | /******/ // no module.loaded needed 226 | /******/ exports: {} 227 | /******/ }; 228 | /******/ 229 | /******/ // Execute the module function 230 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 231 | /******/ 232 | /******/ // Return the exports of the module 233 | /******/ return module.exports; 234 | /******/ } 235 | /******/ 236 | /************************************************************************/ 237 | /******/ /* webpack/runtime/define property getters */ 238 | /******/ (() => { 239 | /******/ // define getter functions for harmony exports 240 | /******/ __webpack_require__.d = (exports, definition) => { 241 | /******/ for(var key in definition) { 242 | /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { 243 | /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); 244 | /******/ } 245 | /******/ } 246 | /******/ }; 247 | /******/ })(); 248 | /******/ 249 | /******/ /* webpack/runtime/hasOwnProperty shorthand */ 250 | /******/ (() => { 251 | /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) 252 | /******/ })(); 253 | /******/ 254 | /******/ /* webpack/runtime/make namespace object */ 255 | /******/ (() => { 256 | /******/ // define __esModule on exports 257 | /******/ __webpack_require__.r = (exports) => { 258 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 259 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 260 | /******/ } 261 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 262 | /******/ }; 263 | /******/ })(); 264 | /******/ 265 | /************************************************************************/ 266 | var __webpack_exports__ = {}; 267 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 268 | (() => { 269 | /*!**************************************************!*\ 270 | !*** ./resources/assets/js/filament-webauthn.js ***! 271 | \**************************************************/ 272 | __webpack_require__.r(__webpack_exports__); 273 | /* harmony import */ var _github_webauthn_json_browser_ponyfill__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @github/webauthn-json/browser-ponyfill */ "./node_modules/@github/webauthn-json/dist/esm/webauthn-json.browser-ponyfill.js"); 274 | function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } 275 | 276 | function _regeneratorRuntime() { "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return exports; }; var exports = {}, Op = Object.prototype, hasOwn = Op.hasOwnProperty, $Symbol = "function" == typeof Symbol ? Symbol : {}, iteratorSymbol = $Symbol.iterator || "@@iterator", asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator", toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function define(obj, key, value) { return Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }), obj[key]; } try { define({}, ""); } catch (err) { define = function define(obj, key, value) { return obj[key] = value; }; } function wrap(innerFn, outerFn, self, tryLocsList) { var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator, generator = Object.create(protoGenerator.prototype), context = new Context(tryLocsList || []); return generator._invoke = function (innerFn, self, context) { var state = "suspendedStart"; return function (method, arg) { if ("executing" === state) throw new Error("Generator is already running"); if ("completed" === state) { if ("throw" === method) throw arg; return doneResult(); } for (context.method = method, context.arg = arg;;) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if ("next" === context.method) context.sent = context._sent = context.arg;else if ("throw" === context.method) { if ("suspendedStart" === state) throw state = "completed", context.arg; context.dispatchException(context.arg); } else "return" === context.method && context.abrupt("return", context.arg); state = "executing"; var record = tryCatch(innerFn, self, context); if ("normal" === record.type) { if (state = context.done ? "completed" : "suspendedYield", record.arg === ContinueSentinel) continue; return { value: record.arg, done: context.done }; } "throw" === record.type && (state = "completed", context.method = "throw", context.arg = record.arg); } }; }(innerFn, self, context), generator; } function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } exports.wrap = wrap; var ContinueSentinel = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var IteratorPrototype = {}; define(IteratorPrototype, iteratorSymbol, function () { return this; }); var getProto = Object.getPrototypeOf, NativeIteratorPrototype = getProto && getProto(getProto(values([]))); NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol) && (IteratorPrototype = NativeIteratorPrototype); var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function (method) { define(prototype, method, function (arg) { return this._invoke(method, arg); }); }); } function AsyncIterator(generator, PromiseImpl) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if ("throw" !== record.type) { var result = record.arg, value = result.value; return value && "object" == _typeof(value) && hasOwn.call(value, "__await") ? PromiseImpl.resolve(value.__await).then(function (value) { invoke("next", value, resolve, reject); }, function (err) { invoke("throw", err, resolve, reject); }) : PromiseImpl.resolve(value).then(function (unwrapped) { result.value = unwrapped, resolve(result); }, function (error) { return invoke("throw", error, resolve, reject); }); } reject(record.arg); } var previousPromise; this._invoke = function (method, arg) { function callInvokeWithMethodAndArg() { return new PromiseImpl(function (resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = previousPromise ? previousPromise.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); }; } function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (undefined === method) { if (context.delegate = null, "throw" === context.method) { if (delegate.iterator["return"] && (context.method = "return", context.arg = undefined, maybeInvokeDelegate(delegate, context), "throw" === context.method)) return ContinueSentinel; context.method = "throw", context.arg = new TypeError("The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if ("throw" === record.type) return context.method = "throw", context.arg = record.arg, context.delegate = null, ContinueSentinel; var info = record.arg; return info ? info.done ? (context[delegate.resultName] = info.value, context.next = delegate.nextLoc, "return" !== context.method && (context.method = "next", context.arg = undefined), context.delegate = null, ContinueSentinel) : info : (context.method = "throw", context.arg = new TypeError("iterator result is not an object"), context.delegate = null, ContinueSentinel); } function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; 1 in locs && (entry.catchLoc = locs[1]), 2 in locs && (entry.finallyLoc = locs[2], entry.afterLoc = locs[3]), this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal", delete record.arg, entry.completion = record; } function Context(tryLocsList) { this.tryEntries = [{ tryLoc: "root" }], tryLocsList.forEach(pushTryEntry, this), this.reset(!0); } function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) return iteratorMethod.call(iterable); if ("function" == typeof iterable.next) return iterable; if (!isNaN(iterable.length)) { var i = -1, next = function next() { for (; ++i < iterable.length;) { if (hasOwn.call(iterable, i)) return next.value = iterable[i], next.done = !1, next; } return next.value = undefined, next.done = !0, next; }; return next.next = next; } } return { next: doneResult }; } function doneResult() { return { value: undefined, done: !0 }; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, define(Gp, "constructor", GeneratorFunctionPrototype), define(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, toStringTagSymbol, "GeneratorFunction"), exports.isGeneratorFunction = function (genFun) { var ctor = "function" == typeof genFun && genFun.constructor; return !!ctor && (ctor === GeneratorFunction || "GeneratorFunction" === (ctor.displayName || ctor.name)); }, exports.mark = function (genFun) { return Object.setPrototypeOf ? Object.setPrototypeOf(genFun, GeneratorFunctionPrototype) : (genFun.__proto__ = GeneratorFunctionPrototype, define(genFun, toStringTagSymbol, "GeneratorFunction")), genFun.prototype = Object.create(Gp), genFun; }, exports.awrap = function (arg) { return { __await: arg }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, asyncIteratorSymbol, function () { return this; }), exports.AsyncIterator = AsyncIterator, exports.async = function (innerFn, outerFn, self, tryLocsList, PromiseImpl) { void 0 === PromiseImpl && (PromiseImpl = Promise); var iter = new AsyncIterator(wrap(innerFn, outerFn, self, tryLocsList), PromiseImpl); return exports.isGeneratorFunction(outerFn) ? iter : iter.next().then(function (result) { return result.done ? result.value : iter.next(); }); }, defineIteratorMethods(Gp), define(Gp, toStringTagSymbol, "Generator"), define(Gp, iteratorSymbol, function () { return this; }), define(Gp, "toString", function () { return "[object Generator]"; }), exports.keys = function (object) { var keys = []; for (var key in object) { keys.push(key); } return keys.reverse(), function next() { for (; keys.length;) { var key = keys.pop(); if (key in object) return next.value = key, next.done = !1, next; } return next.done = !0, next; }; }, exports.values = values, Context.prototype = { constructor: Context, reset: function reset(skipTempReset) { if (this.prev = 0, this.next = 0, this.sent = this._sent = undefined, this.done = !1, this.delegate = null, this.method = "next", this.arg = undefined, this.tryEntries.forEach(resetTryEntry), !skipTempReset) for (var name in this) { "t" === name.charAt(0) && hasOwn.call(this, name) && !isNaN(+name.slice(1)) && (this[name] = undefined); } }, stop: function stop() { this.done = !0; var rootRecord = this.tryEntries[0].completion; if ("throw" === rootRecord.type) throw rootRecord.arg; return this.rval; }, dispatchException: function dispatchException(exception) { if (this.done) throw exception; var context = this; function handle(loc, caught) { return record.type = "throw", record.arg = exception, context.next = loc, caught && (context.method = "next", context.arg = undefined), !!caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i], record = entry.completion; if ("root" === entry.tryLoc) return handle("end"); if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"), hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) return handle(entry.catchLoc, !0); if (this.prev < entry.finallyLoc) return handle(entry.finallyLoc); } else if (hasCatch) { if (this.prev < entry.catchLoc) return handle(entry.catchLoc, !0); } else { if (!hasFinally) throw new Error("try statement without catch or finally"); if (this.prev < entry.finallyLoc) return handle(entry.finallyLoc); } } } }, abrupt: function abrupt(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } finallyEntry && ("break" === type || "continue" === type) && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc && (finallyEntry = null); var record = finallyEntry ? finallyEntry.completion : {}; return record.type = type, record.arg = arg, finallyEntry ? (this.method = "next", this.next = finallyEntry.finallyLoc, ContinueSentinel) : this.complete(record); }, complete: function complete(record, afterLoc) { if ("throw" === record.type) throw record.arg; return "break" === record.type || "continue" === record.type ? this.next = record.arg : "return" === record.type ? (this.rval = this.arg = record.arg, this.method = "return", this.next = "end") : "normal" === record.type && afterLoc && (this.next = afterLoc), ContinueSentinel; }, finish: function finish(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) return this.complete(entry.completion, entry.afterLoc), resetTryEntry(entry), ContinueSentinel; } }, "catch": function _catch(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if ("throw" === record.type) { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } throw new Error("illegal catch attempt"); }, delegateYield: function delegateYield(iterable, resultName, nextLoc) { return this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }, "next" === this.method && (this.arg = undefined), ContinueSentinel; } }, exports; } 277 | 278 | function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } 279 | 280 | function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } 281 | 282 | 283 | var FilamentWebauthn = { 284 | create: function () { 285 | var _create2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(options) { 286 | return _regeneratorRuntime().wrap(function _callee$(_context) { 287 | while (1) { 288 | switch (_context.prev = _context.next) { 289 | case 0: 290 | return _context.abrupt("return", (0,_github_webauthn_json_browser_ponyfill__WEBPACK_IMPORTED_MODULE_0__.create)(options)); 291 | 292 | case 1: 293 | case "end": 294 | return _context.stop(); 295 | } 296 | } 297 | }, _callee); 298 | })); 299 | 300 | function create(_x) { 301 | return _create2.apply(this, arguments); 302 | } 303 | 304 | return create; 305 | }(), 306 | parseCreationOptionsFromJSON: function parseCreationOptionsFromJSON(requestJSON) { 307 | return (0,_github_webauthn_json_browser_ponyfill__WEBPACK_IMPORTED_MODULE_0__.parseCreationOptionsFromJSON)(requestJSON); 308 | }, 309 | parseRequestOptionsFromJSON: function parseRequestOptionsFromJSON(requestJSON) { 310 | return (0,_github_webauthn_json_browser_ponyfill__WEBPACK_IMPORTED_MODULE_0__.parseRequestOptionsFromJSON)(requestJSON); 311 | }, 312 | get: function () { 313 | var _get2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(options) { 314 | return _regeneratorRuntime().wrap(function _callee2$(_context2) { 315 | while (1) { 316 | switch (_context2.prev = _context2.next) { 317 | case 0: 318 | return _context2.abrupt("return", (0,_github_webauthn_json_browser_ponyfill__WEBPACK_IMPORTED_MODULE_0__.get)(options)); 319 | 320 | case 1: 321 | case "end": 322 | return _context2.stop(); 323 | } 324 | } 325 | }, _callee2); 326 | })); 327 | 328 | function get(_x2) { 329 | return _get2.apply(this, arguments); 330 | } 331 | 332 | return get; 333 | }(), 334 | supported: function supported() { 335 | return (0,_github_webauthn_json_browser_ponyfill__WEBPACK_IMPORTED_MODULE_0__.supported)(); 336 | } 337 | }; 338 | window.FilamentWebauthn = FilamentWebauthn; 339 | })(); 340 | 341 | /******/ })() 342 | ; --------------------------------------------------------------------------------