├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── webauthn.php ├── database ├── factories │ └── WebauthnKeyFactory.php └── migrations │ └── create_webauthn_table.php.stub ├── dist ├── assets │ └── webauthn.js └── mix-manifest.json ├── resources ├── js │ ├── helpers │ │ ├── base64URLStringToBuffer.js │ │ ├── browserSupportsWebAuthn.js │ │ ├── bufferToBase64URLString.js │ │ ├── bufferToUTF8String.js │ │ ├── guessDeviceName.js │ │ ├── identifyAuthenticationError.js │ │ ├── isValidDomain.js │ │ ├── preparePublicKeyCredentials.js │ │ ├── toPublicKeyCredentialDescriptor.js │ │ ├── utf8StringToBuffer.js │ │ └── webAuthnAbortService.js │ └── webauthn.js └── lang │ └── en │ ├── alerts.php │ └── labels.php └── src ├── Actions ├── PrepareAssertionData.php ├── PrepareKeyCreationData.php └── RegisterNewKeyAction.php ├── Casts ├── Base64.php ├── TrustPath.php └── Uuid.php ├── Contracts └── WebauthnKey.php ├── Enums ├── AttestationConveyancePreference.php ├── UserVerification.php └── Userless.php ├── Events ├── WebauthnKeyWasRegistered.php ├── WebauthnKeyWasUsed.php ├── WebauthnLoginDataGenerated.php ├── WebauthnRegisterData.php └── WebauthnRegistrationFailed.php ├── Exceptions ├── ResponseMismatchException.php ├── WebauthnRegisterException.php └── WrongUserHandleException.php ├── Facades └── Webauthn.php ├── Http └── Controllers │ ├── AssetsController.php │ └── Concerns │ └── CanPretendToBeAFile.php ├── Models └── WebauthnKey.php ├── Services ├── Webauthn.php ├── Webauthn │ ├── CreationOptionsFactory.php │ ├── CredentialAssertionValidator.php │ ├── CredentialAttestationValidator.php │ ├── CredentialRepository.php │ ├── CredentialValidator.php │ ├── OptionsFactory.php │ └── RequestOptionsFactory.php └── WebauthnRepository.php ├── Support └── WebauthnAssets.php └── WebauthnServiceProvider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-webauthn` will be documented in this file 4 | 5 | ## v1.1.0 - 2025-03-18 6 | 7 | ### What's Changed 8 | 9 | * Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/17 10 | * Bump dependabot/fetch-metadata from 1.4.0 to 1.5.0 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/18 11 | * Bump aglipanci/laravel-pint-action from 2.2.0 to 2.3.0 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/19 12 | * Bump dependabot/fetch-metadata from 1.5.0 to 1.5.1 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/20 13 | * Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/21 14 | * Support Laravel 11 by @MiniCodeMonkey in https://github.com/rawilk/laravel-webauthn/pull/25 15 | * Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/22 16 | * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/23 17 | * Laravel 12.x su pport 18 | 19 | ### New Contributors 20 | 21 | * @MiniCodeMonkey made their first contribution in https://github.com/rawilk/laravel-webauthn/pull/25 22 | 23 | **Full Changelog**: https://github.com/rawilk/laravel-webauthn/compare/v1.0.6...v1.1.0 24 | 25 | ## v1.0.6 - 2023-04-14 26 | 27 | ### What's Changed 28 | 29 | - Bump aglipanci/laravel-pint-action from 2.1.0 to 2.2.0 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/15 30 | - Content Security Policy Support by @rawilk in https://github.com/rawilk/laravel-webauthn/pull/16 31 | 32 | **Full Changelog**: https://github.com/rawilk/laravel-webauthn/compare/v1.0.5...v1.0.6 33 | 34 | ## v1.0.5 - 2023-03-20 35 | 36 | ### What's Changed 37 | 38 | - Bump creyD/prettier_action from 4.2 to 4.3 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/12 39 | - Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/13 40 | - Fix failing container bindings by @eivee in https://github.com/rawilk/laravel-webauthn/pull/11 41 | 42 | ### New Contributors 43 | 44 | - @eivee made their first contribution in https://github.com/rawilk/laravel-webauthn/pull/11 45 | 46 | **Full Changelog**: https://github.com/rawilk/laravel-webauthn/compare/v1.0.4...v1.0.5 47 | 48 | ## v1.0.4 - 2023-02-15 49 | 50 | #### What's Changed 51 | 52 | - Bump aglipanci/laravel-pint-action from 0.1.0 to 1.0.0 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/1 53 | - Bump creyD/prettier_action from 3.0 to 4.2 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/2 54 | - Bump dependabot/fetch-metadata from 1.3.4 to 1.3.5 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/5 55 | - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/9 56 | - Bump aglipanci/laravel-pint-action from 1.0.0 to 2.1.0 by @dependabot in https://github.com/rawilk/laravel-webauthn/pull/7 57 | - Laravel 10.x Compatibility by @rawilk in https://github.com/rawilk/laravel-webauthn/pull/10 58 | - Php 8.2 Compatibility by @rawilk in https://github.com/rawilk/laravel-webauthn/pull/10 59 | 60 | ### New Contributors 61 | 62 | - @dependabot made their first contribution in https://github.com/rawilk/laravel-webauthn/pull/1 63 | 64 | **Full Changelog**: https://github.com/rawilk/laravel-webauthn/compare/v1.0.3...v1.0.4 65 | 66 | ## 1.0.3 - 2022-09-29 67 | 68 | ### Changed 69 | 70 | - Revert build process to use laravel-mix 71 | 72 | ## 1.0.2 - 2022-09-29 73 | 74 | ### Fixed 75 | 76 | - Fix manifest.json path in `WebauthnAssets.php` helper 77 | 78 | ## 1.0.1 - 2022-09-29 79 | 80 | ### Fixed 81 | 82 | - Fix decodeNoPadding() doesn't tolerate padding issues caused from front-end scripts - [See web-auth/webauthn-framework #285](https://github.com/web-auth/webauthn-framework/issues/285) 83 | 84 | ### Changed 85 | 86 | - Change build process from laravel-mix to vite 87 | 88 | ## 1.0.0 - 2022-06-06 89 | 90 | - initial release 91 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Randall Wilk 4 | 5 | Copyright (c) 2020 Matthew Miller 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Note: This package is not actively maintained currently, and I'm not sure if I'll end up archiving it or not yet. Use at your own risk. 2 | 3 | # WebAuthn for Laravel 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/rawilk/laravel-webauthn.svg?style=flat-square)](https://packagist.org/packages/rawilk/laravel-webauthn) 6 | ![Tests](https://github.com/rawilk/laravel-webauthn/workflows/Tests/badge.svg?style=flat-square) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/rawilk/laravel-webauthn.svg?style=flat-square)](https://packagist.org/packages/rawilk/laravel-webauthn) 8 | [![PHP from Packagist](https://img.shields.io/packagist/php-v/rawilk/laravel-webauthn?style=flat-square)](https://packagist.org/packages/rawilk/laravel-webauthn) 9 | [![License](https://img.shields.io/github/license/rawilk/laravel-webauthn?style=flat-square)](https://github.com/rawilk/laravel-webauthn/blob/main/LICENSE.md) 10 | 11 | ![Social image](https://banners.beyondco.de/WebAuthn%20for%20Laravel.png?theme=light&packageManager=composer+require&packageName=rawilk%2Flaravel-webauthn&pattern=randomShapes&style=style_1&description=Add+WebAuthn+functionality+to+Laravel.&md=1&showWatermark=0&fontSize=100px&images=key) 12 | 13 | Add the ability to add a hardware based two-factor authentication via a security key, fingerprint or biometric data. Using WebAuthn as a second factor of authentication can help your users better secure their accounts on your application. For more info on WebAuthn, please check out this [guide](https://webauthn.guide/). 14 | 15 | ## Documentation 16 | 17 | For more documentation, please visit the [docs](https://randallwilk.dev/docs/laravel-webauthn). 18 | 19 | ## Installation 20 | 21 | You can install the package via composer: 22 | 23 | ```bash 24 | composer require rawilk/laravel-webauthn 25 | ``` 26 | 27 | You can publish and run the migrations with: 28 | 29 | ```bash 30 | php artisan vendor:publish --tag="webauthn-migrations" 31 | php artisan migrate 32 | ``` 33 | 34 | You can publish the config file with: 35 | 36 | ```bash 37 | php artisan vendor:publish --tag="webauthn-config" 38 | ``` 39 | 40 | You can view the default configuration here: https://github.com/rawilk/laravel-webauthn/blob/main/config/laravel-webauthn.php 41 | 42 | ## Testing 43 | 44 | ```bash 45 | composer test 46 | ``` 47 | 48 | ## Changelog 49 | 50 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 51 | 52 | ## Contributing 53 | 54 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 55 | 56 | ## Security 57 | 58 | Please review [my security policy](.github/SECURITY.md) on how to report security vulnerabilities. 59 | 60 | ## Credits 61 | 62 | This package is heavily inspired from Larapass and asbiin/laravel-webauthn. 63 | 64 | - [Randall Wilk](https://github.com/rawilk) 65 | - [All Contributors](../../contributors) 66 | 67 | Since the `v1.0.1` patch, a considerable amount of the JavaScript portion of this package has been sourced from the [SimpleWebAuthn](https://github.com/MasterKale/SimpleWebAuthn) package made by [Matthew Miller](https://github.com/MasterKale). His copyright has been added to the license file, and copyright notices have been placed in JS files where the code is extremely close to what his is. 68 | 69 | ## Alternatives 70 | 71 | This package aims to provide only the bare necessities required to utilize WebAuthn in your application, which provides the freedom to incorporate it into your project based on your own needs and desires. If you're looking for a more complete solution, consider one of these alternatives: 72 | 73 | - [Laragear Webauthn](https://github.com/Laragear/WebAuthn) 74 | - [asbiin/laravel-webauthn](https://github.com/asbiin/laravel-webauthn) 75 | 76 | ## Disclaimer 77 | 78 | This package is not affiliated with, maintained, authorized, endorsed or sponsored by Laravel or any of its affiliates. 79 | 80 | ## License 81 | 82 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 83 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rawilk/laravel-webauthn", 3 | "description": "Add webauthn functionality to Laravel", 4 | "keywords": [ 5 | "rawilk", 6 | "laravel", 7 | "webauthn" 8 | ], 9 | "homepage": "https://github.com/rawilk/laravel-webauthn", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Randall Wilk", 14 | "email": "randall@randallwilk.dev", 15 | "homepage": "https://randallwilk.dev", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1|^8.2", 21 | "illuminate/contracts": "^9.12|^10.0|^11.0|^12.0", 22 | "nyholm/psr7": "^1.5", 23 | "spatie/laravel-package-tools": "^1.9", 24 | "web-auth/webauthn-lib": "^4.0" 25 | }, 26 | "require-dev": { 27 | "laravel/pint": "^1.5", 28 | "nunomaduro/collision": "^6.0", 29 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", 30 | "pestphp/pest": "^1.21|^2.0|^3.0", 31 | "pestphp/pest-plugin-laravel": "^1.1|^2.0|^3.0", 32 | "spatie/laravel-ray": "^1.26" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Rawilk\\Webauthn\\": "src", 37 | "Rawilk\\Webauthn\\Database\\Factories\\": "database/factories" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Rawilk\\Webauthn\\Tests\\": "tests" 43 | } 44 | }, 45 | "suggest": { 46 | "web-token/jwt-signature-algorithm-rsa": "Required for the AndroidSafetyNet Attestation Statement support", 47 | "web-token/jwt-signature-algorithm-ecdsa": "Required for the AndroidSafetyNet Attestation Statement support", 48 | "web-token/jwt-signature-algorithm-eddsa": "Required for the AndroidSafetyNet Attestation Statement support", 49 | "web-token/jwt-key-mgmt": "Required for the AndroidSafetyNet Attestation Statement support" 50 | }, 51 | "scripts": { 52 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 53 | "test": "vendor/bin/pest -p", 54 | "format": "vendor/bin/pint --dirty" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "pestphp/pest-plugin": true 60 | } 61 | }, 62 | "extra": { 63 | "laravel": { 64 | "providers": [ 65 | "Rawilk\\Webauthn\\WebauthnServiceProvider" 66 | ], 67 | "aliases": { 68 | "Webauthn": "Rawilk\\Webauthn\\Facades\\Webauthn" 69 | } 70 | } 71 | }, 72 | "minimum-stability": "dev", 73 | "prefer-stable": true 74 | } 75 | -------------------------------------------------------------------------------- /config/webauthn.php: -------------------------------------------------------------------------------- 1 | env('WEBAUTHN_ENABLED', true), 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Relying Party 17 | |-------------------------------------------------------------------------- 18 | | 19 | | We will use your application information to inform the device who is the 20 | | relying party. While only the name is enough, you can further set a 21 | | custom domain as the ID and even an icon image data encoded as base64. 22 | | 23 | */ 24 | 'relying_party' => [ 25 | 'name' => env('WEBAUTHN_NAME', env('APP_NAME')), 26 | 'id' => env('WEBAUTHN_ID'), 27 | 'icon' => env('WEBAUTHN_ICON'), 28 | ], 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Webauthn Challenge Length 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Length of random string used in the challenge request. 36 | | 37 | */ 38 | 'challenge_length' => 32, 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Webauthn Timeout (milliseconds) 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Time that the caller is willing to wait for the call to complete. 46 | | 47 | */ 48 | 'timeout' => 60000, 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Credentials Attachment 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Authentication can be tied to the current device (i.e. Windows Hello 56 | | or Touch ID) or a cross-platform device (USB key). When this 57 | | is `null`, the user will decide where to store their authentication 58 | | information. 59 | | 60 | | See https://www.w3.org/TR/webauthn/#enum-attachment 61 | | 62 | | Supported: `null`, `cross-platform`, `platform` 63 | | 64 | */ 65 | 'attachment_mode' => env('WEBAUTHN_ATTACHMENT_MODE'), 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Database 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Basic configuration settings for how the package stores webauthn 73 | | credentials. 74 | | 75 | */ 76 | 'database' => [ 77 | 'table' => 'webauthn_keys', 78 | 79 | /* 80 | * You may either extend our model or use your own model 81 | * to represent a webauthn key credential. 82 | * 83 | * If you use your own model, it must implement the 84 | * \Rawilk\Webauthn\Contracts\WebauthnKey interface. 85 | */ 86 | 'model' => \Rawilk\Webauthn\Models\WebauthnKey::class, 87 | ], 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Username / Email 92 | |-------------------------------------------------------------------------- 93 | | 94 | | This value defines which model attribute should be considered as your 95 | | application's "username" field. Typically, this might be the email 96 | | address of the users, but you are free to use a different value. 97 | | 98 | */ 99 | 'username' => 'email', 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Webauthn Public Key Credential Parameters 104 | |-------------------------------------------------------------------------- 105 | | 106 | | List of allowed Cryptographic Algorithm Identifiers. 107 | | See https://www.w3.org/TR/webauthn/#sctn-alg-identifier 108 | | 109 | */ 110 | 'public_key_credential_parameters' => [ 111 | (string) \Cose\Algorithms::COSE_ALGORITHM_ES256, // ECDSA with SHA-256 112 | (string) \Cose\Algorithms::COSE_ALGORITHM_ES512, // ECDSA with SHA-512 113 | (string) \Cose\Algorithms::COSE_ALGORITHM_RS256, // RSASSA-PKCS1-v1_5 with SHA-256 114 | (string) \Cose\Algorithms::COSE_ALGORITHM_EdDSA, // EdDSA 115 | (string) \Cose\Algorithms::COSE_ALGORITHM_ES384, // ECDSA with SHA-384 116 | ], 117 | 118 | /* 119 | |-------------------------------------------------------------------------- 120 | | Webauthn Attestation Conveyance 121 | |-------------------------------------------------------------------------- 122 | | 123 | | This parameter specifies the preference regarding the attestation conveyance 124 | | during credential generation. 125 | | 126 | | See https://www.w3.org/TR/webauthn/#enum-attestation-convey 127 | | 128 | | Supported: `none`, `indirect`, `direct`, `enterprise` 129 | | 130 | */ 131 | 'attestation_conveyance' => env('WEBAUTHN_ATTESTATION_CONVEYANCE', \Rawilk\Webauthn\Enums\AttestationConveyancePreference::NONE->value), 132 | 133 | /* 134 | |-------------------------------------------------------------------------- 135 | | Google Safetynet Api Key 136 | |-------------------------------------------------------------------------- 137 | | 138 | | Api key to use Google Safetynet when `attestation_conveyance` 139 | | is set to something other than `none`. 140 | | 141 | | See https://developer.android.com/training/safetynet/attestation 142 | | 143 | */ 144 | 'google_safetynet_api_key' => env('GOOGLE_SAFETYNET_API_KEY'), 145 | 146 | /* 147 | |-------------------------------------------------------------------------- 148 | | User Presence and Verification 149 | |-------------------------------------------------------------------------- 150 | | 151 | | Most authenticators and smartphones will ask the user to actively verify 152 | | themselves to log in. Use `required` to always ask to verify, `preferred` 153 | | to ask when possible, and `discourage` to just ask for user preference. 154 | | 155 | | See https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement 156 | | 157 | | Supported: `required`, `preferred`, `discouraged` 158 | */ 159 | 'user_verification' => env('WEBAUTHN_USER_VERIFICATION', \Rawilk\Webauthn\Enums\UserVerification::PREFERRED->value), 160 | 161 | /* 162 | |-------------------------------------------------------------------------- 163 | | Userless (One Touch, Typeless) Login 164 | |-------------------------------------------------------------------------- 165 | | 166 | | By default, users must input their email to receive a list of credential 167 | | IDs to use for authentication, but they can also log in without specifying 168 | | one if the device can remember them, allowing for true one-touch login. 169 | | 170 | | If required or preferred, login verification will always be required. 171 | | 172 | | See https://www.w3.org/TR/webauthn/#enum-residentKeyRequirement 173 | | 174 | | Supported: `null`, `required`, `preferred`, `discouraged` 175 | | 176 | */ 177 | 'userless' => env('WEBAUTHN_USERLESS'), 178 | 179 | /* 180 | |-------------------------------------------------------------------------- 181 | | Assets URL 182 | |-------------------------------------------------------------------------- 183 | | 184 | | This value sets the path to the WebAuthn JavaScript assets for cases 185 | | where your app's domain root is not the correct path. By default, 186 | | WebAuthn will load its JavaScript assets from the app's 187 | | "relative root". 188 | | 189 | | Examples: "/assets", "myapp.com/app", 190 | | 191 | */ 192 | 'asset_url' => null, 193 | ]; 194 | -------------------------------------------------------------------------------- /database/factories/WebauthnKeyFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word(), 21 | 'counter' => 0, 22 | 'type' => 'public-key', 23 | 'transports' => [], 24 | 'attestation_type' => 'none', 25 | 'trust_path' => new EmptyTrustPath, 26 | 'aaguid' => Uuid::fromString($this->faker->uuid()), 27 | 'credential_public_key' => 'oWNrZXlldmFsdWU=', 28 | ]; 29 | } 30 | 31 | public function configure() 32 | { 33 | return $this->afterMaking(function (WebauthnKey $webauthnKey) { 34 | $webauthnKey->credential_id = Base64UrlSafe::encode((string) $webauthnKey->user_id); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/create_webauthn_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->foreignId('user_id') 14 | ->constrained('users') 15 | ->cascadeOnDelete(); 16 | $table->string('name')->nullable(); 17 | $table->string('attachment_type', 20)->nullable()->index(); 18 | $table->string('credential_id')->index(); 19 | $table->string('type'); 20 | $table->text('transports'); 21 | $table->string('attestation_type'); 22 | $table->text('trust_path'); 23 | $table->text('aaguid'); 24 | $table->text('credential_public_key'); 25 | $table->unsignedBigInteger('counter'); 26 | $table->dateTime('last_used_at')->nullable(); 27 | $table->dateTime('created_at')->nullable(); 28 | $table->dateTime('updated_at')->nullable(); 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /dist/assets/webauthn.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var t=function(t){for(var e=t.replace(/-/g,"+").replace(/_/g,"/"),r=(4-e.length%4)%4,n=e.padEnd(e.length+r,"="),o=atob(n),i=new ArrayBuffer(o.length),a=new Uint8Array(i),c=0;c=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,c=!1;return{s:function(){r=r.call(t)},n:function(){var t=r.next();return a=t.done,t},e:function(t){c=!0,i=t},f:function(){try{a||null==r.return||r.return()}finally{if(c)throw i}}}}function u(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var c=r.call(i,"catchLoc"),u=r.call(i,"finallyLoc");if(c&&u){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),k(r),s}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,r){return this.delegate={iterator:S(t),resultName:e,nextLoc:r},"next"===this.method&&(this.arg=void 0),s}},t}function m(t,e,r,n,o,i,a){try{var c=t[i](a),u=c.value}catch(t){return void r(t)}c.done?e(u):Promise.resolve(u).then(n,o)}function b(t){return function(){var e=this,r=arguments;return new Promise((function(n,o){var i=t.apply(e,r);function a(t){m(i,n,o,a,c,"next",t)}function c(t){m(i,n,o,a,c,"throw",t)}a(void 0)}))}}function x(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function O(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:null;x(this,e),this.notifyCallback=t}var r,n,i,c,u;return r=e,n=[{key:"registerNotifyCallback",value:function(t){return this.notifyCallback=t,this}},{key:"register",value:(u=b(w().mark((function e(r,n){var i,c,u,l,f,h,p;return w().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(this.supported()){e.next=2;break}throw new Error("WebAuthn is not supported in this browser.");case 2:return(c=Object.assign({},r)).challenge=t(r.challenge),c.user.id=(g=r.user.id,(new TextEncoder).encode(g)),c.excludeCredentials=null===(i=r.excludeCredentials)||void 0===i?void 0:i.map(o),u={publicKey:c,signal:a.createNewAbortSignal()},e.prev=7,e.next=10,navigator.credentials.create(u);case 10:l=e.sent,e.next=18;break;case 13:return e.prev=13,e.t0=e.catch(7),f=y(e.t0,u),h=f.name,p=f.default,this._notify(h,p),e.abrupt("return");case 18:return e.prev=18,a.reset(),e.finish(18);case 21:if(l){e.next=24;break}return this._notify(d,"Authentication was not completed."),e.abrupt("return");case 24:n(s(l),v(l));case 25:case"end":return e.stop()}var g}),e,this,[[7,13,18,21]])}))),function(t,e){return u.apply(this,arguments)})},{key:"sign",value:(c=b(w().mark((function e(r,n){var i,c,u,l,f,h;return w().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(this.supported()){e.next=2;break}throw new Error("WebAuthn is not supported in this browser.");case 2:return(i=Object.assign({},r)).challenge=t(r.challenge),r.allowCredentials&&(i.allowCredentials=r.allowCredentials.map(o)),c={publicKey:i,signal:a.createNewAbortSignal()},e.prev=6,e.next=9,navigator.credentials.get(c);case 9:u=e.sent,e.next=17;break;case 12:return e.prev=12,e.t0=e.catch(6),l=y(e.t0,c),f=l.name,h=l.default,this._notify(f,h),e.abrupt("return");case 17:return e.prev=17,a.reset(),e.finish(17);case 20:if(u){e.next=23;break}return this._notify(d,"Authentication was not completed."),e.abrupt("return");case 23:n(s(u));case 24:case"end":return e.stop()}}),e,this,[[6,12,17,20]])}))),function(t,e){return c.apply(this,arguments)})},{key:"supported",value:function(){return void 0!==(null===(t=window)||void 0===t?void 0:t.PublicKeyCredential)&&"function"==typeof window.PublicKeyCredential;var t}},{key:"notSupportedType",value:function(){return window.isSecureContext||"localhost"===window.location.hostname||"127.0.0.1"===window.location.hostname?"notSupported":"notSecured"}},{key:"_notify",value:function(t,e){this.notifyCallback&&this.notifyCallback(t,e)}}],n&&O(r.prototype,n),i&&O(r,i),Object.defineProperty(r,"prototype",{writable:!1}),e}();window.WebAuthn=E})(); -------------------------------------------------------------------------------- /dist/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/assets/webauthn.js": "/assets/webauthn.js?id=9cf885f212ebec7c66815840f7fd7138" 3 | } 4 | -------------------------------------------------------------------------------- /resources/js/helpers/base64URLStringToBuffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | 7 | /** 8 | * Convert from a Base64URL-encoded string to an Array Buffer. Best used when converting a 9 | * credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or 10 | * excludeCredentials. 11 | * 12 | * Helper method to compliment: `bufferToBase64URLString` 13 | * 14 | * @param {string} base64URLString 15 | * @returns {ArrayBuffer} 16 | */ 17 | export const base64URLStringToBuffer = base64URLString => { 18 | // Convert from Base64URL to Base64. 19 | const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/'); 20 | 21 | /** 22 | * Pad with '=' until it's a multiple of four 23 | * (4 - (85 % 4 = 1) = 3) % 4 = 3 padding 24 | * (4 - (86 % 4 = 2) = 2) % 4 = 2 padding 25 | * (4 - (87 % 4 = 3) = 1) % 4 = 1 padding 26 | * (4 - (88 % 4 = 0) = 4) % 4 = 0 padding 27 | */ 28 | const padLength = (4 - (base64.length % 4)) % 4; 29 | const padded = base64.padEnd(base64.length + padLength, '='); 30 | 31 | // Convert to a binary string. 32 | const binary = atob(padded); 33 | 34 | // Convert binary string to buffer. 35 | const buffer = new ArrayBuffer(binary.length); 36 | const bytes = new Uint8Array(buffer); 37 | 38 | for (let i = 0; i < binary.length; i++) { 39 | bytes[i] = binary.charCodeAt(i); 40 | } 41 | 42 | return buffer; 43 | }; 44 | -------------------------------------------------------------------------------- /resources/js/helpers/browserSupportsWebAuthn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | 7 | /** 8 | * Determine if the browser is capable of Webauthn. 9 | * 10 | * @returns {boolean} 11 | */ 12 | export const browserSupportsWebAuthn = () => window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function'; 13 | -------------------------------------------------------------------------------- /resources/js/helpers/bufferToBase64URLString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | 7 | /** 8 | * Convert the given ArrayBuffer into a Base64URL-encoded string. Ideal for converting various 9 | * credential response ArrayBuffers to strings for sending back to the server as JSON. 10 | * 11 | * Helper method to compliment `base64URLStringToBuffer` 12 | * 13 | * @param {ArrayBuffer} buffer 14 | * @returns {string} 15 | */ 16 | export const bufferToBase64URLString = buffer => { 17 | const bytes = new Uint8Array(buffer); 18 | let str = ''; 19 | 20 | for (const charCode of bytes) { 21 | str += String.fromCharCode(charCode); 22 | } 23 | 24 | const base64String = btoa(str); 25 | 26 | return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 27 | }; 28 | -------------------------------------------------------------------------------- /resources/js/helpers/bufferToUTF8String.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | 7 | /** 8 | * A helper method to convert an arbitrary ArrayBuffer returned from an authenticator to a UTF-8 9 | * string. 10 | * 11 | * @param {ArrayBuffer} value 12 | * @returns {string} 13 | */ 14 | export const bufferToUTF8String = value => new TextDecoder('utf-8').decode(value); 15 | -------------------------------------------------------------------------------- /resources/js/helpers/guessDeviceName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attempt to guess the device name the user is using as a key. 3 | * 4 | * @param {PublicKeyCredential} publicKey 5 | * @returns {string} 6 | */ 7 | export const guessDeviceName = publicKey => { 8 | if (! publicKey.response.getTransports().includes('internal')) { 9 | return 'Security key'; 10 | } 11 | 12 | const userAgent = navigator.userAgent, 13 | platform = navigator.platform, 14 | macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], 15 | windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'], 16 | iosPlatforms = ['iPhone', 'iPad', 'iPod']; 17 | 18 | if (macosPlatforms.includes(platform)) { 19 | return 'macOS Computer'; 20 | } 21 | 22 | if (iosPlatforms.includes(platform)) { 23 | return 'iOS Phone'; 24 | } 25 | 26 | if (windowsPlatforms.includes(platform)) { 27 | return 'Windows Computer'; 28 | } 29 | 30 | if (/Android/.test(userAgent)) { 31 | return 'Android Phone'; 32 | } 33 | 34 | if (/Linux/.test(platform)) { 35 | return 'Linux Computer'; 36 | } 37 | 38 | return 'Unknown Device Type'; 39 | }; 40 | -------------------------------------------------------------------------------- /resources/js/helpers/identifyAuthenticationError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | import { isValidDomain } from './isValidDomain'; 7 | 8 | export const errorNames = { 9 | Abort: 'AbortError', 10 | Constraint: 'ConstraintError', 11 | InvalidState: 'InvalidStateError', 12 | NotAllowed: 'NotAllowedError', 13 | Security: 'SecurityError', 14 | Type: 'TypeError', 15 | Unknown: 'UnknownError', 16 | }; 17 | 18 | /** 19 | * Attempt to find an explanation on why an error was raised after calling `navigator.credentials.get()`. 20 | * 21 | * @param {Error} error 22 | * @param {CredentialRequestOptions} options 23 | * @return {object} 24 | */ 25 | export const identifyAuthenticationError = (error, options) => { 26 | const { publicKey } = options; 27 | 28 | if (! publicKey) { 29 | throw new Error('options param is missing the required publicKey property'); 30 | } 31 | 32 | if (error.name === errorNames.Abort) { 33 | if (options.signal === (new AbortController).signal) { 34 | return { 35 | name: errorNames.Abort, 36 | default: 'Authentication ceremony was sent an abort signal.', 37 | }; 38 | } 39 | } 40 | 41 | if (error.name === errorNames.NotAllowed) { 42 | if (publicKey.allowedCredentials?.length) { 43 | // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 17) 44 | // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 6) 45 | return { 46 | name: `${errorNames.NotAllowed}_none_registered`, 47 | default: 'No available authenticator recognized any of the allowed credentials.', 48 | }; 49 | } 50 | 51 | // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 18) 52 | // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 7) 53 | return { 54 | name: errorNames.NotAllowed, 55 | default: 'User clicked cancel, or the authentication ceremony timed out.', 56 | }; 57 | } 58 | 59 | if (error.name === errorNames.Security) { 60 | const effectiveDomain = window.location.hostname; 61 | if (! isValidDomain(effectiveDomain)) { 62 | // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 5) 63 | return { 64 | name: errorNames.Security, 65 | default: `${window.location.hostname} is an invalid domain.`, 66 | }; 67 | } 68 | 69 | if (publicKey.rpId !== effectiveDomain) { 70 | return { 71 | name: errorNames.Security, 72 | default: `The RP ID "${publicKey.rpId}" is invalid for this domain.`, 73 | }; 74 | } 75 | } 76 | 77 | return { 78 | name: errorNames.Unknown, 79 | default: 'The authenticator was unable to process your request.', 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /resources/js/helpers/isValidDomain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | 7 | /** 8 | * A simple test to determine if a hostname is a properly-formatted domain name. 9 | * 10 | * A "valid" domain is defined here: https://url.spec.whatwg.org/#valid-domain 11 | * 12 | * @param {string} hostname 13 | * @returns {boolean} 14 | */ 15 | export const isValidDomain = hostname => { 16 | return ( 17 | // Consider localhost valid as well since it's okay with Secure Contexts. 18 | hostname === 'localhost' || /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(hostname) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /resources/js/helpers/preparePublicKeyCredentials.js: -------------------------------------------------------------------------------- 1 | import { bufferToBase64URLString } from './bufferToBase64URLString'; 2 | import { bufferToUTF8String } from './bufferToUTF8String'; 3 | 4 | /** 5 | * Prepare the public key credentials object returned by the authenticator for our server. 6 | * 7 | * @param {PublicKeyCredential} credential 8 | * @returns {{authenticatorAttachment: *, response: {clientDataJSON: string}, rawId: string, id: string, type: string, clientExtensionResults: AuthenticationExtensionsClientOutputs}} 9 | */ 10 | export const preparePublicKeyCredentials = credential => { 11 | const { id, rawId, response, type } = credential; 12 | 13 | const publicKeyCredential = { 14 | id, 15 | rawId: bufferToBase64URLString(rawId), 16 | response: { 17 | clientDataJSON: bufferToBase64URLString(response.clientDataJSON), 18 | }, 19 | type, 20 | clientExtensionResults: credential.getClientExtensionResults(), 21 | authenticatorAttachment: credential.authenticatorAttachment, 22 | }; 23 | 24 | if (response.attestationObject !== undefined) { 25 | publicKeyCredential.response.attestationObject = bufferToBase64URLString(response.attestationObject); 26 | } 27 | 28 | if (response.authenticatorData !== undefined) { 29 | publicKeyCredential.response.authenticatorData = bufferToBase64URLString(response.authenticatorData); 30 | } 31 | 32 | if (response.signature !== undefined) { 33 | publicKeyCredential.response.signature = bufferToBase64URLString(response.signature); 34 | } 35 | 36 | if (response.userHandle) { 37 | publicKeyCredential.response.userHandle = bufferToUTF8String(response.userHandle); 38 | } 39 | 40 | return publicKeyCredential; 41 | }; 42 | -------------------------------------------------------------------------------- /resources/js/helpers/toPublicKeyCredentialDescriptor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | 7 | import { base64URLStringToBuffer } from './base64URLStringToBuffer'; 8 | 9 | export const toPublicKeyCredentialDescriptor = descriptor => { 10 | const { id } = descriptor; 11 | 12 | return { 13 | ...descriptor, 14 | id: base64URLStringToBuffer(id), 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /resources/js/helpers/utf8StringToBuffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | 7 | /** 8 | * A helper method to convert an arbitrary string sent from the server to an ArrayBuffer the authenticator 9 | * will expect. 10 | * 11 | * @param {string} value 12 | * @returns {Uint8Array} 13 | */ 14 | export const utf8StringToBuffer = value => new TextEncoder().encode(value); 15 | -------------------------------------------------------------------------------- /resources/js/helpers/webAuthnAbortService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2020 Matthew Miller 5 | */ 6 | 7 | /** 8 | * Provides a way to cancel an existing WebAuthn request, for example to cancel a 9 | * WebAuthn autofill authentication request for a manual authentication attempt. 10 | */ 11 | class WebAuthnAbortService { 12 | constructor() { 13 | this.reset(); 14 | } 15 | 16 | /** 17 | * Prepare an abort signal that will help support multiple auth attempts without needing to 18 | * reload the page. 19 | * 20 | * @returns {AbortSignal} 21 | */ 22 | createNewAbortSignal() { 23 | // Abort any existing calls to navigator.credentials.create() or navigator.credentials.get(). 24 | if (this.controller) { 25 | this.controller.abort(); 26 | } 27 | 28 | this.controller = new AbortController; 29 | 30 | return this.controller.signal; 31 | } 32 | 33 | reset() { 34 | this.controller = undefined; 35 | } 36 | } 37 | 38 | export const webauthnAbortService = new WebAuthnAbortService; 39 | -------------------------------------------------------------------------------- /resources/js/webauthn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { browserSupportsWebAuthn } from './helpers/browserSupportsWebAuthn'; 4 | import { base64URLStringToBuffer } from './helpers/base64URLStringToBuffer'; 5 | import { toPublicKeyCredentialDescriptor } from './helpers/toPublicKeyCredentialDescriptor'; 6 | import { webauthnAbortService } from './helpers/webAuthnAbortService'; 7 | import { preparePublicKeyCredentials } from './helpers/preparePublicKeyCredentials'; 8 | import { errorNames, identifyAuthenticationError } from './helpers/identifyAuthenticationError'; 9 | import { utf8StringToBuffer } from './helpers/utf8StringToBuffer'; 10 | import { guessDeviceName } from './helpers/guessDeviceName'; 11 | 12 | class WebAuthn { 13 | /** 14 | * @param {function(string, string)} notifyCallback 15 | */ 16 | constructor(notifyCallback = null) { 17 | this.notifyCallback = notifyCallback; 18 | } 19 | 20 | /** 21 | * Register a notification callback. 22 | * 23 | * @param {function(string, string)} callback 24 | * @returns {WebAuthn} 25 | */ 26 | registerNotifyCallback(callback) { 27 | this.notifyCallback = callback; 28 | 29 | return this; 30 | } 31 | 32 | /** 33 | * Register a new key. 34 | * 35 | * @param {PublicKeyCredentialCreationOptions} publicKey - see https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions 36 | * @param {function(PublicKeyCredential)} callback User callback 37 | */ 38 | async register(publicKey, callback) { 39 | if (! this.supported()) { 40 | throw new Error('WebAuthn is not supported in this browser.'); 41 | } 42 | 43 | const publicKeyCredential = Object.assign({}, publicKey); 44 | 45 | // We need to convert some values to Uint8Arrays before passing the credentials to the navigator. 46 | publicKeyCredential.challenge = base64URLStringToBuffer(publicKey.challenge); 47 | publicKeyCredential.user.id = utf8StringToBuffer(publicKey.user.id); 48 | publicKeyCredential.excludeCredentials = publicKey.excludeCredentials?.map(toPublicKeyCredentialDescriptor); 49 | 50 | /** @var {CredentialCreationOptions} options */ 51 | const options = { 52 | publicKey: publicKeyCredential, 53 | 54 | // Set up the ability to cancel this request if the user attempts another. 55 | signal: webauthnAbortService.createNewAbortSignal(), 56 | }; 57 | 58 | // Wait for the user to complete attestation. 59 | let credential; 60 | try { 61 | credential = await navigator.credentials.create(options); 62 | } catch (e) { 63 | const { name, default: defaultMessage } = identifyAuthenticationError(e, options); 64 | 65 | this._notify(name, defaultMessage); 66 | 67 | return; 68 | } finally { 69 | webauthnAbortService.reset(); 70 | } 71 | 72 | if (! credential) { 73 | this._notify(errorNames.Unknown, 'Authentication was not completed.'); 74 | 75 | return; 76 | } 77 | 78 | callback(preparePublicKeyCredentials(credential), guessDeviceName(credential)); 79 | } 80 | 81 | /** 82 | * Authenticate a user. 83 | * 84 | * @param {PublicKeyCredentialRequestOptions} publicKey 85 | * @param {function(PublicKeyCredential)} callback 86 | */ 87 | async sign(publicKey, callback) { 88 | if (! this.supported()) { 89 | throw new Error('WebAuthn is not supported in this browser.'); 90 | } 91 | 92 | const publicKeyCredential = Object.assign({}, publicKey); 93 | 94 | // We need to convert some values to Uint8Arrays before passing the credentials to the navigator. 95 | publicKeyCredential.challenge = base64URLStringToBuffer(publicKey.challenge); 96 | if (publicKey.allowCredentials) { 97 | publicKeyCredential.allowCredentials = publicKey.allowCredentials.map(toPublicKeyCredentialDescriptor); 98 | } 99 | 100 | /** @var {CredentialCreationOptions} options */ 101 | const options = { 102 | publicKey: publicKeyCredential, 103 | signal: webauthnAbortService.createNewAbortSignal(), 104 | }; 105 | 106 | // Wait for the user to complete the assertion. 107 | let credential; 108 | try { 109 | credential = await navigator.credentials.get(options); 110 | } catch (e) { 111 | const { name, default: defaultMessage } = identifyAuthenticationError(e, options); 112 | 113 | this._notify(name, defaultMessage); 114 | 115 | return; 116 | } finally { 117 | webauthnAbortService.reset(); 118 | } 119 | 120 | if (! credential) { 121 | this._notify(errorNames.Unknown, 'Authentication was not completed.'); 122 | 123 | return; 124 | } 125 | 126 | callback(preparePublicKeyCredentials(credential)); 127 | } 128 | 129 | /** 130 | * Test if WebAuthn is supported by this navigator. 131 | * 132 | * @returns {boolean} 133 | */ 134 | supported() { 135 | return browserSupportsWebAuthn(); 136 | } 137 | 138 | notSupportedType() { 139 | if (! window.isSecureContext && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') { 140 | return 'notSecured'; 141 | } 142 | 143 | return 'notSupported'; 144 | } 145 | 146 | /** 147 | * Notify end user of error if a callback is defined. 148 | * @param {string} name 149 | * @param {string} defaultMessage 150 | * @private 151 | */ 152 | _notify(name, defaultMessage) { 153 | if (this.notifyCallback) { 154 | this.notifyCallback(name, defaultMessage); 155 | } 156 | } 157 | } 158 | 159 | window.WebAuthn = WebAuthn; 160 | -------------------------------------------------------------------------------- /resources/lang/en/alerts.php: -------------------------------------------------------------------------------- 1 | 'WebAuthn only supports secure connections.', 5 | 'browser_not_supported' => 'Your browser does not support WebAuthn.', 6 | 'register_not_allowed' => 'You are not able to register any more keys for your account.', 7 | 'key_validation_error' => 'An error occurred during the validation of the key.', 8 | 'key_not_allowed_error' => 'The operation either timed out or was not allowed.', 9 | 'key_already_used' => "This key is already registered. It's not necessary to register it again.", 10 | 'login_not_allowed_error' => 'Please try again or use a different authentication method.', 11 | 12 | 'auth' => [ 13 | 'AbortError' => 'Authentication ceremony was sent an abort signal.', 14 | 'NotAllowedError' => 'User clicked cancel, or the authentication ceremony timed out.', 15 | 'NotAllowedError_none_registered' => 'No available authenticator recognized any of the allowed credentials.', 16 | 'UnknownError' => 'The authenticator was unable to process your request.', 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /resources/lang/en/labels.php: -------------------------------------------------------------------------------- 1 | 'Never', 5 | ]; 6 | -------------------------------------------------------------------------------- /src/Actions/PrepareAssertionData.php: -------------------------------------------------------------------------------- 1 | registerKey($user, $data, $keyName); 23 | } 24 | 25 | protected function registerKey(User $user, array $data, string $keyName): WebauthnKey 26 | { 27 | try { 28 | return Webauthn::registerAttestation($user, $data, $keyName); 29 | } catch (Exception $e) { 30 | WebauthnRegistrationFailed::dispatch($user, $e); 31 | 32 | throw WebauthnRegisterException::keyValidationError(Webauthn::username()); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Casts/Base64.php: -------------------------------------------------------------------------------- 1 | 18 | * HTML tag. 19 | */ 20 | public function createdAtHtml(string $timezone = 'UTC'): string; 21 | 22 | /** 23 | * Return the date the WebAuthn key was last used wrapped in 24 | * a 137 | HTML; 138 | } 139 | 140 | public function lastUsedAtHtml(string $timezone = 'UTC'): string 141 | { 142 | $date = $this->last_used_at?->clone()->tz($timezone); 143 | 144 | if (! $date) { 145 | return __('webauthn::labels.webauthn_key_never_used'); 146 | } 147 | 148 | return <<{$date->format('M d Y, g:i a')} 150 | HTML; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Services/Webauthn.php: -------------------------------------------------------------------------------- 1 | validateAttestation($user, $credentials); 50 | 51 | if (! $publicKey) { 52 | throw WebauthnRegisterException::keyValidationError($this->username()); 53 | } 54 | 55 | return tap( 56 | $this->create($user, $keyName, $publicKey, Arr::get($credentials, 'attachment_type')), 57 | function (WebauthnKey $webauthnKey) { 58 | WebauthnKeyWasRegistered::dispatch($webauthnKey); 59 | } 60 | ); 61 | } 62 | 63 | /** 64 | * Get publicKey data to prepare a WebAuthn login. 65 | */ 66 | public function prepareAssertion(User $user): PublicKeyCredentialRequestOptions 67 | { 68 | return tap(app(RequestOptionsFactory::class)($user), function (PublicKeyCredentialRequestOptions $publicKey) use ($user) { 69 | WebauthnLoginDataGenerated::dispatch($user, $publicKey); 70 | }); 71 | } 72 | 73 | /** 74 | * Validate a WebAuthn login request. 75 | */ 76 | public function validateAssertion(User $user, array $credentials): bool|PublicKeyCredentialSource 77 | { 78 | return app(CredentialAssertionValidator::class)($user, $credentials); 79 | } 80 | 81 | public function username(): string 82 | { 83 | return config('webauthn.username', 'email'); 84 | } 85 | 86 | /** 87 | * Check if a given user can register a new key. 88 | */ 89 | public function canRegister(User $user): bool 90 | { 91 | return $this->webauthnIsEnabled(); 92 | } 93 | 94 | /** 95 | * Check if both WebAuthn is enabled for the application and that 96 | * the given user has at least one key registered to them. 97 | */ 98 | public function enabledFor(User $user): bool 99 | { 100 | return $this->webauthnIsEnabled() && $this->hasKey($user); 101 | } 102 | 103 | /** 104 | * Check if webauthn is configured to be enabled for this application. 105 | */ 106 | public function webauthnIsEnabled(): bool 107 | { 108 | return (bool) config('webauthn.enabled', true); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Services/Webauthn/CreationOptionsFactory.php: -------------------------------------------------------------------------------- 1 | attestationConveyance = $config->get('webauthn.attestation_conveyance', AttestationConveyancePreference::NONE->value); 38 | } 39 | 40 | public function __invoke(User $user, ?string $attachmentType = null): PublicKeyCredentialCreationOptions 41 | { 42 | $publicKey = (new PublicKeyCredentialCreationOptions( 43 | $this->publicKeyCredentialRpEntity, 44 | $this->getUserEntity($user), 45 | $this->getChallenge(), 46 | $this->createCredentialParameters(), 47 | )) 48 | ->setTimeout($this->timeout) 49 | ->excludeCredentials(...$this->getExcludedCredentials($user)) 50 | ->setAuthenticatorSelection($this->authenticatorSelectionCriteria) 51 | ->setAttestation($this->attestationConveyance); 52 | 53 | $this->cache->put($this->cacheKey($user, $attachmentType), $publicKey, $this->timeout); 54 | 55 | return $publicKey; 56 | } 57 | 58 | private function getUserEntity(User $user): PublicKeyCredentialUserEntity 59 | { 60 | return new PublicKeyCredentialUserEntity( 61 | $user->{Webauthn::username()} ?? '', 62 | (string) $user->getAuthIdentifier(), 63 | $user->{Webauthn::username()} ?? '', 64 | null, 65 | ); 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | private function createCredentialParameters(): array 72 | { 73 | return collect($this->algorithmManager->list()) 74 | ->map(function ($algorithm): PublicKeyCredentialParameters { 75 | return new PublicKeyCredentialParameters( 76 | PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, 77 | $algorithm, 78 | ); 79 | }) 80 | ->toArray(); 81 | } 82 | 83 | private function getExcludedCredentials(User $user): array 84 | { 85 | return $this->repository->getRegisteredKeys($user); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Services/Webauthn/CredentialAssertionValidator.php: -------------------------------------------------------------------------------- 1 | pullPublicKey($user)) { 35 | return false; 36 | } 37 | 38 | try { 39 | $publicKeyCredential = $this->loader->loadArray($data); 40 | 41 | return $this->validator->check( 42 | $publicKeyCredential->getRawId(), 43 | $this->getResponse($publicKeyCredential), 44 | $publicKeyRequestOptions, 45 | $this->serverRequest, 46 | (string) $user->getAuthIdentifier(), 47 | ); 48 | } catch (InvalidArgumentException) { 49 | return false; 50 | } 51 | } 52 | 53 | protected function pullPublicKey(User $user): ?PublicKeyCredentialRequestOptions 54 | { 55 | $publicKeyCredentialRequestOptions = $this->cache->pull($this->cacheKey($user)); 56 | 57 | if (! $publicKeyCredentialRequestOptions instanceof PublicKeyCredentialRequestOptions) { 58 | return null; 59 | } 60 | 61 | return $publicKeyCredentialRequestOptions; 62 | } 63 | 64 | protected function getResponse(PublicKeyCredential $publicKeyCredential): AuthenticatorAssertionResponse 65 | { 66 | $response = $publicKeyCredential->getResponse(); 67 | 68 | if (! $response instanceof AuthenticatorAssertionResponse) { 69 | throw ResponseMismatchException::assertionMismatched(); 70 | } 71 | 72 | return $response; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Services/Webauthn/CredentialAttestationValidator.php: -------------------------------------------------------------------------------- 1 | pullPublicKey($user, Arr::get($data, 'attachment_type'))) { 35 | return null; 36 | } 37 | 38 | $publicKeyCredential = $this->loader->loadArray($data); 39 | 40 | return $this->validator->check( 41 | $this->getResponse($publicKeyCredential), 42 | $publicKeyCredentialCreationOptions, 43 | $this->serverRequest, 44 | ); 45 | } 46 | 47 | protected function pullPublicKey(User $user, ?string $attachmentType): ?PublicKeyCredentialCreationOptions 48 | { 49 | $publicKeyCredentialCreationOptions = $this->cache->pull($this->cacheKey($user, $attachmentType)); 50 | 51 | if (! $publicKeyCredentialCreationOptions instanceof PublicKeyCredentialCreationOptions) { 52 | return null; 53 | } 54 | 55 | return $publicKeyCredentialCreationOptions; 56 | } 57 | 58 | /** 59 | * Get the authenticator response. 60 | */ 61 | protected function getResponse(PublicKeyCredential $publicKeyCredential): AuthenticatorAttestationResponse 62 | { 63 | $response = $publicKeyCredential->getResponse(); 64 | 65 | if (! $response instanceof AuthenticatorAttestationResponse) { 66 | throw ResponseMismatchException::mismatched(); 67 | } 68 | 69 | return $response; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Services/Webauthn/CredentialRepository.php: -------------------------------------------------------------------------------- 1 | model($publicKeyCredentialId); 26 | 27 | return $webauthnKey->public_key_credential_source; 28 | } catch (ModelNotFoundException) { 29 | } 30 | 31 | return null; 32 | } 33 | 34 | public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array 35 | { 36 | return $this->getAllRegisteredKeys($publicKeyCredentialUserEntity->getId()) 37 | ->toArray(); 38 | } 39 | 40 | public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void 41 | { 42 | $webauthnKey = $this->model($publicKeyCredentialSource->getPublicKeyCredentialId()); 43 | 44 | $webauthnKey->public_key_credential_source = $publicKeyCredentialSource; 45 | $webauthnKey->last_used_at = now(); 46 | $webauthnKey->save(); 47 | 48 | WebauthnKeyWasUsed::dispatch($webauthnKey); 49 | } 50 | 51 | /** 52 | * List all PublicKeyCredentialSource associated with a user. 53 | * 54 | * @return \Illuminate\Support\Collection 55 | */ 56 | protected function getAllRegisteredKeys($userId): Collection 57 | { 58 | return app(WebauthnKey::class)::query() 59 | ->where('user_id', $userId) 60 | ->get() 61 | ->map 62 | ->public_key_credential_source; 63 | } 64 | 65 | /** 66 | * List all registered PublicKeyCredentialDescriptor associated with a user. 67 | * 68 | * @return array 69 | */ 70 | public function getRegisteredKeys(User $user): array 71 | { 72 | return $this->getAllRegisteredKeys($user->getAuthIdentifier()) 73 | ->map 74 | ->getPublicKeyCredentialDescriptor() 75 | ->toArray(); 76 | } 77 | 78 | private function model(string $credentialId) 79 | { 80 | return app(WebauthnKey::class)::query() 81 | ->when($this->guard()->check(), fn ($query) => $query->where('user_id', $this->guard()->id())) 82 | ->where(function ($query) use ($credentialId) { 83 | $query->where('credential_id', Base64UrlSafe::encode($credentialId)) 84 | ->orWhere('credential_id', Base64UrlSafe::encodeUnpadded($credentialId)); 85 | }) 86 | ->firstOrFail(); 87 | } 88 | 89 | private function guard(): \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard 90 | { 91 | return $this->auth->guard(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Services/Webauthn/CredentialValidator.php: -------------------------------------------------------------------------------- 1 | getAuthIdentifier(), 32 | sha1($this->request->getHost() . '|' . $this->request->ip()), 33 | $attachmentType, 34 | ]), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Services/Webauthn/OptionsFactory.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 32 | } 33 | 34 | $this->challengeLength = (int) $config->get('webauthn.challenge_length', 32); 35 | $this->timeout = (int) $config->get('webauthn.timeout', 60000); 36 | } 37 | 38 | protected function getChallenge(): string 39 | { 40 | return random_bytes($this->challengeLength); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Services/Webauthn/RequestOptionsFactory.php: -------------------------------------------------------------------------------- 1 | userVerification = $this->getUserVerification($config); 34 | } 35 | 36 | public function __invoke(User $user): PublicKeyCredentialRequestOptions 37 | { 38 | $publicKey = (new PublicKeyCredentialRequestOptions($this->getChallenge())) 39 | ->setTimeout($this->timeout) 40 | ->allowCredentials(...$this->getAllowedCredentials($user)) 41 | ->setRpId($this->getRpId()) 42 | ->setUserVerification($this->userVerification); 43 | 44 | $this->cache->put($this->cacheKey($user), $publicKey, $this->timeout); 45 | 46 | return $publicKey; 47 | } 48 | 49 | /** 50 | * Get the user verification preference. 51 | */ 52 | private function getUserVerification(Config $config): ?string 53 | { 54 | return Userless::requiresUserPresence($config->get('webauthn.userless')) 55 | ? UserVerification::REQUIRED->value 56 | : $config->get('webauthn.user_verification', UserVerification::PREFERRED->value); 57 | } 58 | 59 | private function getAllowedCredentials(User $user): array 60 | { 61 | return $this->repository->getRegisteredKeys($user); 62 | } 63 | 64 | private function getRpId(): ?string 65 | { 66 | return $this->publicKeyCredentialRpEntity->getId(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Services/WebauthnRepository.php: -------------------------------------------------------------------------------- 1 | where('user_id', $user->getAuthIdentifier()) 28 | ->count(); 29 | } 30 | 31 | public function keysFor(User $user): Collection 32 | { 33 | return app(WebauthnKey::class)::query() 34 | ->where('user_id', $user->getAuthIdentifier()) 35 | ->get(); 36 | } 37 | 38 | public function hasKey(User $user): bool 39 | { 40 | return app(WebauthnKey::class)::query() 41 | ->where('user_id', $user->getAuthIdentifier()) 42 | ->exists(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Support/WebauthnAssets.php: -------------------------------------------------------------------------------- 1 | '] : []; 14 | 15 | $html[] = $this->javaScriptAssets($options); 16 | 17 | return implode(PHP_EOL, $html); 18 | } 19 | 20 | private function javaScriptAssets(array $options = []): string 21 | { 22 | $assetsUrl = config('webauthn.asset_url') ?: rtrim($options['asset_url'] ?? '', '/'); 23 | $nonce = $this->getNonce($options); 24 | 25 | $manifest = json_decode(file_get_contents(__DIR__ . '/../../dist/mix-manifest.json'), true); 26 | $versionedFileName = ltrim($manifest['/assets/webauthn.js'], '/'); 27 | 28 | $fullAssetPath = "{$assetsUrl}/webauthn/{$versionedFileName}"; 29 | 30 | return << 32 | HTML; 33 | } 34 | 35 | private function getNonce(array $options): string 36 | { 37 | if (isset($options['nonce'])) { 38 | return "nonce=\"{$options['nonce']}\""; 39 | } 40 | 41 | // If there is a csp package installed, i.e. spatie/laravel-csp, we'll check for the existence of the helper function. 42 | if (function_exists('csp_nonce') && $nonce = csp_nonce()) { 43 | return "nonce=\"{$nonce}\""; 44 | } 45 | 46 | // Lastly, we'll check for the existence of a csp nonce from Vite. 47 | if (class_exists(Vite::class) && $nonce = Vite::cspNonce()) { 48 | return "nonce=\"{$nonce}\""; 49 | } 50 | 51 | return ''; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/WebauthnServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-webauthn') 50 | ->hasConfigFile() 51 | ->hasMigration('create_webauthn_table') 52 | ->hasTranslations(); 53 | } 54 | 55 | public function packageRegistered(): void 56 | { 57 | $this->app->singleton(WebauthnFacade::class, Webauthn::class); 58 | 59 | $this->registerBindings(); 60 | } 61 | 62 | public function packageBooted(): void 63 | { 64 | Blade::directive('webauthnScripts', function (string $expression) { 65 | return "javaScript({$expression}) ?>"; 66 | }); 67 | 68 | Route::get('/webauthn/assets/{asset}', [AssetsController::class, 'source']); 69 | } 70 | 71 | protected function registerBindings(): void 72 | { 73 | $this->app->bind(WebauthnKeyContract::class, config('webauthn.database.model', WebauthnKey::class)); 74 | 75 | $this->registerWebauthnBindings(); 76 | } 77 | 78 | protected function registerWebauthnBindings(): void 79 | { 80 | $this->app->bind(PublicKeyCredentialSourceRepository::class, Webauthn\CredentialRepository::class); 81 | $this->app->bind(TokenBindingHandler::class, IgnoreTokenBindingHandler::class); 82 | 83 | $this->app->bind( 84 | PackedAttestationStatementSupport::class, 85 | fn ($app) => new PackedAttestationStatementSupport( 86 | $app[CoseAlgorithmManager::class] 87 | ) 88 | ); 89 | 90 | if ($this->app['config']->get('webauthn.google_safetynet_api_key')) { 91 | $this->app->bind( 92 | AndroidKeyAttestationStatementSupport::class, 93 | fn ($app) => (new AndroidSafetyNetAttestationStatementSupport) 94 | ->enableApiVerification( 95 | $app[ClientInterface::class], 96 | $app['config']->get('webauthn.google_safetynet_api_key'), 97 | $app[RequestFactoryInterface::class], 98 | ) 99 | ); 100 | } 101 | 102 | $this->app->bind( 103 | AttestationStatementSupportManager::class, 104 | fn ($app) => tap(new AttestationStatementSupportManager, function ($manager) use ($app) { 105 | // https://www.w3.org/TR/webauthn/#sctn-none-attestation 106 | $manager->add($app[NoneAttestationStatementSupport::class]); 107 | 108 | // https://www.w3.org/TR/webauthn/#sctn-fido-u2f-attestation 109 | $manager->add($app[FidoU2FAttestationStatementSupport::class]); 110 | 111 | // https://www.w3.org/TR/webauthn/#sctn-android-key-attestation 112 | $manager->add($app[AndroidKeyAttestationStatementSupport::class]); 113 | 114 | // https://www.w3.org/TR/webauthn/#sctn-tpm-attestation 115 | $manager->add($app[TPMAttestationStatementSupport::class]); 116 | 117 | // https://www.w3.org/TR/webauthn/#sctn-packed-attestation 118 | $manager->add($app[PackedAttestationStatementSupport::class]); 119 | 120 | // https://www.w3.org/TR/webauthn/#sctn-android-safetynet-attestation 121 | if ($app['config']->get('webauthn.google_safetynet_api_key') !== null) { 122 | $manager->add($app[AndroidSafetyNetAttestationStatementSupport::class]); 123 | } 124 | 125 | // https://www.w3.org/TR/webauthn/#sctn-apple-anonymous-attestation 126 | // Note: for some reason, this never makes it through the validation and prevents us from adding 127 | // a hardware key from an Apple device when attestation conveyance is enabled. 128 | $manager->add($app[AppleAttestationStatementSupport::class]); 129 | }) 130 | ); 131 | 132 | $this->app->bind( 133 | AttestationObjectLoader::class, 134 | function ($app) { 135 | $attestationObjectLoader = new AttestationObjectLoader($app[AttestationStatementSupportManager::class]); 136 | $attestationObjectLoader->setLogger($app['log']); 137 | 138 | return $attestationObjectLoader; 139 | } 140 | ); 141 | 142 | $this->app->bind( 143 | CounterChecker::class, 144 | fn ($app) => new ThrowExceptionIfInvalid($app['log']) 145 | ); 146 | 147 | $this->app->bind( 148 | AuthenticatorAttestationResponseValidator::class, 149 | function ($app) { 150 | $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator( 151 | $app[AttestationStatementSupportManager::class], 152 | $app[PublicKeyCredentialSourceRepository::class], 153 | $app[TokenBindingHandler::class], 154 | $app[ExtensionOutputCheckerHandler::class], 155 | ); 156 | $authenticatorAttestationResponseValidator->setLogger($app['log']); 157 | 158 | return $authenticatorAttestationResponseValidator; 159 | } 160 | ); 161 | 162 | $this->app->bind(AuthenticatorAssertionResponseValidator::class, function ($app) { 163 | $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator( 164 | $app[PublicKeyCredentialSourceRepository::class], 165 | $app[TokenBindingHandler::class], 166 | $app[ExtensionOutputCheckerHandler::class], 167 | $app[CoseAlgorithmManager::class], 168 | ); 169 | $authenticatorAssertionResponseValidator->setCounterChecker($app[CounterChecker::class]); 170 | $authenticatorAssertionResponseValidator->setLogger($app['log']); 171 | 172 | return $authenticatorAssertionResponseValidator; 173 | }); 174 | 175 | $this->app->bind( 176 | AuthenticatorSelectionCriteria::class, 177 | fn ($app) => tap(new AuthenticatorSelectionCriteria, function ($authenticatorSelectionCriteria) use ($app) { 178 | $authenticatorSelectionCriteria->setAuthenticatorAttachment($app['config']->get('webauthn.attachment_mode', 'null')) 179 | ->setUserVerification($app['config']->get('webauthn.user_verification', 'preferred')); 180 | 181 | if (($userless = $app['config']->get('webauthn.userless')) !== null) { 182 | $authenticatorSelectionCriteria->setResidentKey($userless); 183 | } 184 | }) 185 | ); 186 | 187 | $this->app->bind( 188 | PublicKeyCredentialRpEntity::class, 189 | fn ($app) => new PublicKeyCredentialRpEntity( 190 | $app['config']->get('webauthn.relying_party.name') ?? 'Laravel', 191 | $app['config']->get('webauthn.relying_party.id') ?? $app->make('request')->getHost(), 192 | $app['config']->get('webauthn.relying_party.icon'), 193 | ) 194 | ); 195 | 196 | $this->app->bind(PublicKeyCredentialLoader::class, function ($app) { 197 | $publicKeyCredentialLoader = new PublicKeyCredentialLoader($app[AttestationObjectLoader::class]); 198 | $publicKeyCredentialLoader->setLogger($app['log']); 199 | 200 | return $publicKeyCredentialLoader; 201 | }); 202 | 203 | $this->app->bind( 204 | CoseAlgorithmManager::class, 205 | fn ($app) => $app[CoseAlgorithmManagerFactory::class] 206 | ->generate(...$app['config']->get('webauthn.public_key_credential_parameters')) 207 | ); 208 | 209 | $this->app->bind( 210 | CoseAlgorithmManagerFactory::class, 211 | fn () => tap(new CoseAlgorithmManagerFactory, function ($factory) { 212 | // list of existing algorithms 213 | $algorithms = [ 214 | RSA\RS1::class, 215 | RSA\RS256::class, 216 | RSA\RS384::class, 217 | RSA\RS512::class, 218 | RSA\PS256::class, 219 | RSA\PS384::class, 220 | RSA\PS512::class, 221 | ECDSA\ES256::class, 222 | ECDSA\ES256K::class, 223 | ECDSA\ES384::class, 224 | ECDSA\ES512::class, 225 | EdDSA\Ed256::class, 226 | EdDSA\Ed512::class, 227 | EdDSA\Ed25519::class, 228 | EdDSA\EdDSA::class, 229 | ]; 230 | 231 | foreach ($algorithms as $algorithm) { 232 | $factory->add((string) $algorithm::identifier(), new $algorithm); 233 | } 234 | }) 235 | ); 236 | } 237 | } 238 | --------------------------------------------------------------------------------