├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── passkeys.php ├── database ├── factories │ └── PasskeyFactory.php └── migrations │ └── create_passkeys_table.php.stub ├── resources ├── lang │ ├── cz │ │ └── passkeys.php │ ├── de │ │ └── passkeys.php │ ├── en │ │ └── passkeys.php │ ├── es │ │ └── passkeys.php │ ├── fr │ │ └── passkeys.php │ ├── nl │ │ └── passkeys.php │ ├── pt_BR │ │ └── passkeys.php │ ├── pt_PT │ │ └── passkeys.php │ ├── sk │ │ └── passkeys.php │ └── tr │ │ └── passkeys.php └── views │ ├── components │ ├── authenticate.blade.php │ └── partials │ │ └── authenticateScript.blade.php │ └── livewire │ ├── partials │ └── createScript.blade.php │ └── passkeys.blade.php └── src ├── Actions ├── FindPasskeyToAuthenticateAction.php ├── GeneratePasskeyAuthenticationOptionsAction.php ├── GeneratePasskeyRegisterOptionsAction.php └── StorePasskeyAction.php ├── Events └── PasskeyUsedToAuthenticateEvent.php ├── Exceptions ├── InvalidActionClass.php ├── InvalidAuthenticatableModel.php ├── InvalidPasskey.php ├── InvalidPasskeyModel.php └── InvalidPasskeyOptions.php ├── Http ├── Components │ └── AuthenticatePasskeyComponent.php ├── Controllers │ ├── AuthenticateUsingPasskeyController.php │ └── GeneratePasskeyAuthenticationOptionsController.php └── Requests │ └── AuthenticateUsingPasskeysRequest.php ├── LaravelPasskeysServiceProvider.php ├── Livewire └── PasskeysComponent.php ├── Models ├── Concerns │ ├── HasPasskeys.php │ └── InteractsWithPasskeys.php └── Passkey.php └── Support ├── Config.php └── Serializer.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-passkeys` will be documented in this file. 4 | 5 | ## 1.0.9 - 2025-06-10 6 | 7 | ### What's Changed 8 | 9 | * Turkish translation of passkeys.php by @altayevrim in https://github.com/spatie/laravel-passkeys/pull/53 10 | 11 | ### New Contributors 12 | 13 | * @altayevrim made their first contribution in https://github.com/spatie/laravel-passkeys/pull/53 14 | 15 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.8...1.0.9 16 | 17 | ## 1.0.8 - 2025-05-20 18 | 19 | ### What's Changed 20 | 21 | * Redirect to intended URL by @mralston in https://github.com/spatie/laravel-passkeys/pull/50 22 | 23 | ### New Contributors 24 | 25 | * @mralston made their first contribution in https://github.com/spatie/laravel-passkeys/pull/50 26 | 27 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.7...1.0.8 28 | 29 | ## 1.0.7 - 2025-05-20 30 | 31 | ### What's Changed 32 | 33 | * Czech & Slovak translation by @hamrak in https://github.com/spatie/laravel-passkeys/pull/49 34 | 35 | ### New Contributors 36 | 37 | * @hamrak made their first contribution in https://github.com/spatie/laravel-passkeys/pull/49 38 | 39 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.6...1.0.7 40 | 41 | ## 1.0.6 - 2025-05-19 42 | 43 | ### What's Changed 44 | 45 | * Better french translations by @sdebacker in https://github.com/spatie/laravel-passkeys/pull/48 46 | 47 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.5...1.0.6 48 | 49 | ## 1.0.5 - 2025-05-19 50 | 51 | ### What's Changed 52 | 53 | * Add French and Dutch translations for passkeys by @Mantix in https://github.com/spatie/laravel-passkeys/pull/47 54 | 55 | ### New Contributors 56 | 57 | * @Mantix made their first contribution in https://github.com/spatie/laravel-passkeys/pull/47 58 | 59 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.4...1.0.5 60 | 61 | ## 1.0.4 - 2025-05-18 62 | 63 | ### What's Changed 64 | 65 | * add german translation by @NastyOOF in https://github.com/spatie/laravel-passkeys/pull/45 66 | 67 | ### New Contributors 68 | 69 | * @NastyOOF made their first contribution in https://github.com/spatie/laravel-passkeys/pull/45 70 | 71 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.3...1.0.4 72 | 73 | ## 1.0.3 - 2025-05-12 74 | 75 | ### What's Changed 76 | 77 | * Create passkeys.php by @nessimabadi in https://github.com/spatie/laravel-passkeys/pull/40 78 | 79 | ### New Contributors 80 | 81 | * @nessimabadi made their first contribution in https://github.com/spatie/laravel-passkeys/pull/40 82 | 83 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.2...1.0.3 84 | 85 | ## 1.0.2 - 2025-05-12 86 | 87 | ### What's Changed 88 | 89 | * startAuthentication refactor to expected call structure by @sdebacker in https://github.com/spatie/laravel-passkeys/pull/39 90 | 91 | ### New Contributors 92 | 93 | * @sdebacker made their first contribution in https://github.com/spatie/laravel-passkeys/pull/39 94 | 95 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.1...1.0.2 96 | 97 | ## 1.0.1 - 2025-05-12 98 | 99 | ### What's Changed 100 | 101 | * remove duplicated fragment by @xHeaven in https://github.com/spatie/laravel-passkeys/pull/22 102 | * Add config tests by @lrljoe in https://github.com/spatie/laravel-passkeys/pull/31 103 | * Standardise on src/Support/Config rather than config() by @lrljoe in https://github.com/spatie/laravel-passkeys/pull/27 104 | * Restore Laravel 11 Support by @lrljoe in https://github.com/spatie/laravel-passkeys/pull/28 105 | * Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot in https://github.com/spatie/laravel-passkeys/pull/38 106 | * Add Portuguese translations for passkey-related messages by @kidiatoliny in https://github.com/spatie/laravel-passkeys/pull/37 107 | * [JS] startAuthentication refactor to expected call structure by @lrljoe in https://github.com/spatie/laravel-passkeys/pull/33 108 | 109 | ### New Contributors 110 | 111 | * @xHeaven made their first contribution in https://github.com/spatie/laravel-passkeys/pull/22 112 | * @kidiatoliny made their first contribution in https://github.com/spatie/laravel-passkeys/pull/37 113 | 114 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/1.0.0...1.0.1 115 | 116 | ## 1.0.0 - 2025-05-06 117 | 118 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/0.0.3...1.0.0 119 | 120 | ## 0.0.3 - 2025-05-06 121 | 122 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/0.0.2...0.0.3 123 | 124 | ## 0.0.2 - 2025-05-06 125 | 126 | **Full Changelog**: https://github.com/spatie/laravel-passkeys/compare/0.0.1...0.0.2 127 | 128 | ## 0.0.1 - 2025-05-05 129 | 130 | - test release 131 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Logo for laravel-pdf 6 | 7 | 8 | 9 |

Use passkeys in your Laravel app

10 | 11 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-passkeys.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-passkeys) 12 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-passkeys/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/spatie/laravel-passkeys/actions?query=workflow%3Arun-tests+branch%3Amain) 13 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-passkeys/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/spatie/laravel-passkeys/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-passkeys.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-passkeys) 15 | 16 |
17 | 18 | Passkeys let you log in without needing a password. Instead of a password, you can generate a passkey which will be stored in 1Pass, MacOS' password app, or alternative app on your favourite OS. 19 | 20 | This package provides a simple way to generate passkeys using a Livewire component. It also contains a Blade component that can authenticate your users using passkeys. 21 | 22 | ## Support us 23 | 24 | [](https://spatie.be/github-ad-click/laravel-passkeys) 25 | 26 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 27 | 28 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 29 | 30 | ## Requirements 31 | 32 | This package contains a Livewire component to generate passkeys. Make sure you have Livewire installed in your Laravel app. 33 | 34 | ## Documentation 35 | 36 | All documentation is available [on our documentation site](https://spatie.be/docs/laravel-passkeys). 37 | 38 | 39 | ## Testing 40 | 41 | ```bash 42 | composer test 43 | ``` 44 | 45 | ## Changelog 46 | 47 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 48 | 49 | ## Contributing 50 | 51 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 52 | 53 | ## Security Vulnerabilities 54 | 55 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 56 | 57 | ## Credits 58 | 59 | This code is based on the [Laracast course on passkeys](https://laracasts.com/series/add-passkeys-to-a-laravel-app) by the amazing [Luke Downing](https://github.com/lukeraymonddowning). 60 | 61 | - [Freek Van der Herten](https://github.com/freekmurze) 62 | - [All Contributors](../../contributors) 63 | 64 | ## License 65 | 66 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 67 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-passkeys", 3 | "description": "Use passkeys in your Laravel app", 4 | "keywords": [ 5 | "Spatie", 6 | "laravel", 7 | "laravel-passkeys" 8 | ], 9 | "homepage": "https://github.com/spatie/laravel-passkeys", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Freek Van der Herten", 14 | "email": "freek@spatie.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2|^8.3|^8.4", 20 | "illuminate/contracts": "^11.0|^12.0", 21 | "spatie/laravel-package-tools": "^1.16", 22 | "web-auth/webauthn-lib": "^5.0" 23 | }, 24 | "require-dev": { 25 | "larastan/larastan": "^3.4", 26 | "laravel/pint": "^1.14", 27 | "livewire/livewire": "^3.5", 28 | "nunomaduro/collision": "^8.1.1", 29 | "orchestra/testbench": "^10.0", 30 | "pestphp/pest": "^3.0", 31 | "pestphp/pest-plugin-arch": "^3.0", 32 | "pestphp/pest-plugin-laravel": "^3.0", 33 | "phpstan/extension-installer": "^1.3", 34 | "phpstan/phpstan-deprecation-rules": "^2.0", 35 | "phpstan/phpstan-phpunit": "^2.0", 36 | "spatie/laravel-ray": "^1.35" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Spatie\\LaravelPasskeys\\": "src/", 41 | "Spatie\\LaravelPasskeys\\Database\\Factories\\": "database/factories/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Spatie\\LaravelPasskeys\\Tests\\": "tests/", 47 | "Workbench\\App\\": "workbench/app/" 48 | } 49 | }, 50 | "scripts": { 51 | "post-autoload-dump": "@composer run prepare", 52 | "clear": "@php vendor/bin/testbench package:purge-laravel-passkeys --ansi", 53 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 54 | "build": [ 55 | "@composer run prepare", 56 | "@php vendor/bin/testbench workbench:build --ansi" 57 | ], 58 | "start": [ 59 | "Composer\\Config::disableProcessTimeout", 60 | "@composer run build", 61 | "@php vendor/bin/testbench serve" 62 | ], 63 | "analyse": "vendor/bin/phpstan analyse", 64 | "test": "vendor/bin/pest", 65 | "test-coverage": "vendor/bin/pest --coverage", 66 | "format": "vendor/bin/pint" 67 | }, 68 | "config": { 69 | "sort-packages": true, 70 | "allow-plugins": { 71 | "pestphp/pest-plugin": true, 72 | "phpstan/extension-installer": true 73 | } 74 | }, 75 | "extra": { 76 | "laravel": { 77 | "providers": [ 78 | "Spatie\\LaravelPasskeys\\LaravelPasskeysServiceProvider" 79 | ] 80 | } 81 | }, 82 | "minimum-stability": "dev", 83 | "prefer-stable": true 84 | } 85 | -------------------------------------------------------------------------------- /config/passkeys.php: -------------------------------------------------------------------------------- 1 | '/dashboard', 9 | 10 | /* 11 | * These class are responsible for performing core tasks regarding passkeys. 12 | * You can customize them by creating a class that extends the default, and 13 | * by specifying your custom class name here. 14 | */ 15 | 'actions' => [ 16 | 'generate_passkey_register_options' => Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction::class, 17 | 'store_passkey' => Spatie\LaravelPasskeys\Actions\StorePasskeyAction::class, 18 | 'generate_passkey_authentication_options' => \Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction::class, 19 | 'find_passkey' => Spatie\LaravelPasskeys\Actions\FindPasskeyToAuthenticateAction::class, 20 | ], 21 | 22 | /* 23 | * These properties will be used to generate the passkey. 24 | */ 25 | 'relying_party' => [ 26 | 'name' => config('app.name'), 27 | 'id' => parse_url(config('app.url'), PHP_URL_HOST), 28 | 'icon' => null, 29 | ], 30 | 31 | /* 32 | * The models used by the package. 33 | * 34 | * You can override this by specifying your own models 35 | */ 36 | 'models' => [ 37 | 'passkey' => Spatie\LaravelPasskeys\Models\Passkey::class, 38 | 'authenticatable' => env('AUTH_MODEL', App\Models\User::class), 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /database/factories/PasskeyFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word, 23 | 'authenticatable_id' => $authModel::factory(), 24 | 'credential_id' => $this->faker->sentence, 25 | 'data' => $this->dummyPublicKeyCredentialSource(), 26 | ]; 27 | } 28 | 29 | protected function dummyPublicKeyCredentialSource(): PublicKeyCredentialSource 30 | { 31 | return PublicKeyCredentialSource::create( 32 | base64_decode( 33 | 'eHouz/Zi7+BmByHjJ/tx9h4a1WZsK4IzUmgGjkhyOodPGAyUqUp/B9yUkflXY3yHWsNtsrgCXQ3HjAIFUeZB+w==', 34 | true 35 | ), 36 | PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, 37 | [], 38 | 'none', 39 | $trustPath ?? EmptyTrustPath::create(), 40 | Uuid::fromString('00000000-0000-0000-0000-000000000000'), 41 | base64_decode( 42 | 'pQECAyYgASFYIJV56vRrFusoDf9hm3iDmllcxxXzzKyO9WruKw4kWx7zIlgg/nq63l8IMJcIdKDJcXRh9hoz0L+nVwP1Oxil3/oNQYs=', 43 | true 44 | ), 45 | 'foo', 46 | 100, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /database/migrations/create_passkeys_table.php.stub: -------------------------------------------------------------------------------- 1 | getTable(); 15 | 16 | Schema::create('passkeys', function (Blueprint $table) use ($authenticatableTableName,$authenticatableClass) { 17 | $table->id(); 18 | 19 | $table 20 | ->foreignIdFor($authenticatableClass, 'authenticatable_id') 21 | ->constrained(table: $authenticatableTableName, indexName: 'passkeys_authenticatable_fk') 22 | ->cascadeOnDelete(); 23 | 24 | $table->text('name'); 25 | $table->text('credential_id'); 26 | $table->json('data'); 27 | 28 | $table->timestamp('last_used_at')->nullable(); 29 | $table->timestamps(); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /resources/lang/cz/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Ověřit pomocí přístupového klíče', 5 | 'create' => 'Vytvořit', 6 | 'delete' => 'Smazat', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Při generování přístupového klíče došlo k chybě.', 8 | 'invalid' => 'Nelze se přihlásit pomocí zadaného přístupového klíče', 9 | 'last_used' => 'Naposledy použito', 10 | 'name' => 'Název', 11 | 'name_placeholder' => 'Zadejte název přístupového klíče', 12 | 'no_passkeys_registered' => 'Nejsou registrovány žádné přístupové klíče', 13 | 'not_used_yet' => 'Zatím nepoužito', 14 | 'passkeys' => 'Přístupové klíče', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/de/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Mit Passkey authentifizieren', 5 | 'create' => 'Erstellen', 6 | 'delete' => 'Löschen', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Beim Erzeugen des Passkeys ist ein Fehler aufgetreten.', 8 | 'invalid' => 'Anmeldung mit dem angegebenen Passkey nicht möglich.', 9 | 'last_used' => 'Zuletzt verwendet', 10 | 'name' => 'Name', 11 | 'name_placeholder' => 'Passkey-Namen eingeben', 12 | 'no_passkeys_registered' => 'Keine Passkeys registriert', 13 | 'not_used_yet' => 'Noch nicht verwendet', 14 | 'passkeys' => 'Passkeys', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/en/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Authenticate using Passkey', 5 | 'create' => 'Create', 6 | 'delete' => 'Delete', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Something went wrong generating the passkey.', 8 | 'invalid' => 'Could not login using the given passkey', 9 | 'last_used' => 'Last used', 10 | 'name' => 'Name', 11 | 'name_placeholder' => 'Enter Passkey Name', 12 | 'no_passkeys_registered' => 'No passkeys registered', 13 | 'not_used_yet' => 'Not used yet', 14 | 'passkeys' => 'Passkeys', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/es/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Autenticar utilizando Clave de Acceso', 5 | 'create' => 'Crear', 6 | 'delete' => 'Eliminar', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Algo salió mal al generar la clave de acceso.', 8 | 'invalid' => 'No se pudo iniciar sesión con la clave de acceso proporcionada', 9 | 'last_used' => 'Utilizada por última vez', 10 | 'name' => 'Nombre', 11 | 'name_placeholder' => 'Ingrese el nombre de la clave de acceso', 12 | 'no_passkeys_registered' => 'No hay claves de acceso registradas', 13 | 'not_used_yet' => 'No utilizado todavía', 14 | 'passkeys' => 'Claves de Acceso', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/fr/passkeys.php: -------------------------------------------------------------------------------- 1 | 'S’authentifier avec la clé d’accès', 5 | 'create' => 'Créer', 6 | 'delete' => 'Supprimer', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Une erreur s’est produite lors de la génération de la clé d’accès.', 8 | 'invalid' => 'Impossible de se connecter avec la clé d’accès fournie', 9 | 'last_used' => 'Dernière utilisation', 10 | 'name' => 'Nom', 11 | 'name_placeholder' => 'Entrez le nom de la clé d’accès', 12 | 'no_passkeys_registered' => 'Aucune clé d’accès enregistrée', 13 | 'not_used_yet' => 'Pas encore utilisée', 14 | 'passkeys' => 'Clés d’accès', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/nl/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Authenticeren met passkey', 5 | 'create' => 'Aanmaken', 6 | 'delete' => 'Verwijderen', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Er is iets misgegaan bij het genereren van de passkey.', 8 | 'invalid' => 'Kon niet inloggen met de opgegeven passkey', 9 | 'last_used' => 'Laatst gebruikt', 10 | 'name' => 'Naam', 11 | 'name_placeholder' => 'Voer passkey naam in', 12 | 'no_passkeys_registered' => 'Geen passkeys geregistreerd', 13 | 'not_used_yet' => 'Nog niet gebruikt', 14 | 'passkeys' => 'Passkeys', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/pt_BR/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Autenticar com chave de acesso', 5 | 'create' => 'Criar', 6 | 'delete' => 'Excluir', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Algo deu errado ao gerar a chave de acesso.', 8 | 'invalid' => 'Não foi possível fazer login com a chave de acesso fornecida', 9 | 'last_used' => 'Último uso', 10 | 'name' => 'Nome', 11 | 'name_placeholder' => 'Digite o nome da chave de acesso', 12 | 'no_passkeys_registered' => 'Nenhuma chave de acesso registrada', 13 | 'not_used_yet' => 'Ainda não utilizada', 14 | 'passkeys' => 'Chaves de acesso', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/pt_PT/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Autenticar com chave de acesso', 5 | 'create' => 'Criar', 6 | 'delete' => 'Eliminar', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Ocorreu um erro ao gerar a chave de acesso.', 8 | 'invalid' => 'Não foi possível iniciar sessão com a chave de acesso fornecida', 9 | 'last_used' => 'Última utilização', 10 | 'name' => 'Nome', 11 | 'name_placeholder' => 'Introduza o nome da chave de acesso', 12 | 'no_passkeys_registered' => 'Nenhuma chave de acesso registada', 13 | 'not_used_yet' => 'Ainda não utilizada', 14 | 'passkeys' => 'Chaves de acesso', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/sk/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Autentifikovať pomocou prístupového kľúča', 5 | 'create' => 'Vytvoriť', 6 | 'delete' => 'Odstrániť', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Pri generovaní prístupového kľúča sa vyskytla chyba.', 8 | 'invalid' => 'Prihlásenie pomocou zadaného prístupového kľúča nebolo možné', 9 | 'last_used' => 'Naposledy použité', 10 | 'name' => 'Názov', 11 | 'name_placeholder' => 'Zadajte názov prístupového kľúča', 12 | 'no_passkeys_registered' => 'Žiadne prístupové kľúče nie sú registrované', 13 | 'not_used_yet' => 'Ešte nepoužité', 14 | 'passkeys' => 'Prístupové kľúče', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/tr/passkeys.php: -------------------------------------------------------------------------------- 1 | 'Geçiş Anahtarı Kullanarak Oturum Aç', 5 | 'create' => 'Oluştur', 6 | 'delete' => 'Sil', 7 | 'error_something_went_wrong_generating_the_passkey' => 'Geçiş anahtarı oluşturulurken bir şeyler ters gitti.', 8 | 'invalid' => 'Seçilen geçiş anahtarı ile giriş yapılırken bir hata oluştu.', 9 | 'last_used' => 'Son Kullanım', 10 | 'name' => 'İsim', 11 | 'name_placeholder' => 'Geçiş Anahtarına Bir İsim Verin', 12 | 'no_passkeys_registered' => 'Sistemde kayıtlı bir geçiş anahtarınız yok', 13 | 'not_used_yet' => 'Hiç Kullanılmadı', 14 | 'passkeys' => 'Geçiş Anahtarları', 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/views/components/authenticate.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @include('passkeys::components.partials.authenticateScript') 3 | 4 |
5 | @csrf 6 |
7 | 8 | @if($message = session()->get('authenticatePasskey::message')) 9 |
10 | {{ $message }} 11 |
12 | @endif 13 | 14 |
15 | @if ($slot->isEmpty()) 16 |
17 | {{ __('passkeys::passkeys.authenticate_using_passkey') }} 18 |
19 | @else 20 | {{ $slot }} 21 | @endif 22 |
23 |
24 | -------------------------------------------------------------------------------- /resources/views/components/partials/authenticateScript.blade.php: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /resources/views/livewire/partials/createScript.blade.php: -------------------------------------------------------------------------------- 1 | @script 2 | 11 | @endscript 12 | -------------------------------------------------------------------------------- /resources/views/livewire/passkeys.blade.php: -------------------------------------------------------------------------------- 1 |
2 |

{{ __('passkeys::passkeys.passkeys') }}

3 |
4 |
5 |
6 | 7 | 8 | @error('name') 9 | {{ $message }} 10 | @enderror 11 |
12 | 13 | 16 |
17 |
18 | 19 |
20 |
    21 | @foreach($passkeys as $passkey) 22 |
  • 23 |
    24 | {{ $passkey->name }} 25 |
    26 |
    27 | {{ __('passkeys::passkeys.last_used') }}: {{ $passkey->last_used_at?->diffForHumans() ?? __('passkeys::passkeys.not_used_yet') }} 28 |
    29 | 30 | 31 |
    32 | 35 |
    36 |
  • 37 | @endforeach 38 |
39 |
40 |
41 | 42 | @include('passkeys::livewire.partials.createScript') 43 | -------------------------------------------------------------------------------- /src/Actions/FindPasskeyToAuthenticateAction.php: -------------------------------------------------------------------------------- 1 | determinePublicKeyCredential($publicKeyCredentialJson); 23 | 24 | if (! $publicKeyCredential) { 25 | return null; 26 | } 27 | 28 | $passkey = $this->findPasskey($publicKeyCredential); 29 | 30 | if (! $passkey) { 31 | return null; 32 | } 33 | 34 | /** @var PublicKeyCredentialRequestOptions $passkeyOptions */ 35 | $passkeyOptions = Serializer::make()->fromJson( 36 | $passkeyOptionsJson, 37 | PublicKeyCredentialRequestOptions::class, 38 | ); 39 | 40 | $publicKeyCredentialSource = $this->determinePublicKeyCredentialSource( 41 | $publicKeyCredential, 42 | $passkeyOptions, 43 | $passkey, 44 | ); 45 | 46 | if (! $publicKeyCredentialSource) { 47 | return null; 48 | } 49 | 50 | $this->updatePasskey($passkey, $publicKeyCredentialSource); 51 | 52 | return $passkey; 53 | } 54 | 55 | public function determinePublicKeyCredential( 56 | string $publicKeyCredentialJson, 57 | ): ?PublicKeyCredential { 58 | $publicKeyCredential = Serializer::make()->fromJson( 59 | $publicKeyCredentialJson, 60 | PublicKeyCredential::class, 61 | ); 62 | 63 | if (! $publicKeyCredential->response instanceof AuthenticatorAssertionResponse) { 64 | return null; 65 | } 66 | 67 | return $publicKeyCredential; 68 | } 69 | 70 | protected function findPasskey(PublicKeyCredential $publicKeyCredential): ?Passkey 71 | { 72 | $passkeyModel = Config::getPassKeyModel(); 73 | 74 | return $passkeyModel::firstWhere('credential_id', mb_convert_encoding($publicKeyCredential->rawId, 'UTF-8')); 75 | } 76 | 77 | protected function determinePublicKeyCredentialSource( 78 | PublicKeyCredential $publicKeyCredential, 79 | PublicKeyCredentialRequestOptions $passkeyOptions, 80 | Passkey $passkey, 81 | ): ?PublicKeyCredentialSource { 82 | $csmFactory = new CeremonyStepManagerFactory; 83 | $requestCsm = $csmFactory->requestCeremony(); 84 | 85 | try { 86 | $validator = AuthenticatorAssertionResponseValidator::create($requestCsm); 87 | 88 | $publicKeyCredentialSource = $validator->check( 89 | publicKeyCredentialSource: $passkey->data, 90 | authenticatorAssertionResponse: $publicKeyCredential->response, 91 | publicKeyCredentialRequestOptions: $passkeyOptions, 92 | host: parse_url(config('app.url'), PHP_URL_HOST), 93 | userHandle: null, 94 | ); 95 | } catch (Throwable) { 96 | return null; 97 | } 98 | 99 | return $publicKeyCredentialSource; 100 | } 101 | 102 | protected function updatePasskey( 103 | Passkey $passkey, 104 | PublicKeyCredentialSource $publicKeyCredentialSource 105 | ): self { 106 | $passkey->update([ 107 | 'data' => $publicKeyCredentialSource, 108 | 'last_used_at' => now(), 109 | ]); 110 | 111 | return $this; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Actions/GeneratePasskeyAuthenticationOptionsAction.php: -------------------------------------------------------------------------------- 1 | toJson($options); 22 | 23 | Session::flash('passkey-authentication-options', $options); 24 | 25 | return $options; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Actions/GeneratePasskeyRegisterOptionsAction.php: -------------------------------------------------------------------------------- 1 | relatedPartyEntity(), 21 | user: $this->generateUserEntity($authenticatable), 22 | challenge: $this->challenge(), 23 | ); 24 | 25 | if ($asJson) { 26 | $options = Serializer::make()->toJson($options); 27 | } 28 | 29 | return $options; 30 | } 31 | 32 | protected function relatedPartyEntity(): PublicKeyCredentialRpEntity 33 | { 34 | return new PublicKeyCredentialRpEntity( 35 | name: Config::getRelyingPartyName(), 36 | id: Config::getRelyingPartyId(), 37 | icon: Config::getRelyingPartyIcon(), 38 | ); 39 | } 40 | 41 | public function generateUserEntity(HasPasskeys $authenticatable): PublicKeyCredentialUserEntity 42 | { 43 | return new PublicKeyCredentialUserEntity( 44 | name: $authenticatable->getPassKeyName(), 45 | id: $authenticatable->getPassKeyId(), 46 | displayName: $authenticatable->getPassKeyDisplayName(), 47 | ); 48 | } 49 | 50 | protected function challenge(): string 51 | { 52 | return Str::random(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Actions/StorePasskeyAction.php: -------------------------------------------------------------------------------- 1 | determinePublicKeyCredentialSource( 28 | $passkeyJson, 29 | $passkeyOptionsJson, 30 | $hostName 31 | ); 32 | 33 | /** @var Passkey $passkey */ 34 | $passkey = $authenticatable->passkeys()->create([ 35 | ...$additionalProperties, 36 | 'data' => $publicKeyCredentialSource, 37 | ]); 38 | 39 | return $passkey; 40 | } 41 | 42 | protected function determinePublicKeyCredentialSource( 43 | string $passkeyJson, 44 | string $passkeyOptionsJson, 45 | string $hostName, 46 | ): PublicKeyCredentialSource { 47 | $passkeyOptions = $this->getPasskeyOptions($passkeyOptionsJson); 48 | 49 | $publicKeyCredential = $this->getPasskey($passkeyJson); 50 | 51 | if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) { 52 | throw InvalidPasskey::invalidPublicKeyCredential(); 53 | } 54 | 55 | $csmFactory = new CeremonyStepManagerFactory; 56 | $creationCsm = $csmFactory->creationCeremony(); 57 | 58 | try { 59 | $publicKeyCredentialSource = AuthenticatorAttestationResponseValidator::create($creationCsm)->check( 60 | authenticatorAttestationResponse: $publicKeyCredential->response, 61 | publicKeyCredentialCreationOptions: $passkeyOptions, 62 | host: $hostName, 63 | ); 64 | } catch (Throwable $exception) { 65 | throw InvalidPasskey::invalidAuthenticatorAttestationResponse($exception); 66 | } 67 | 68 | return $publicKeyCredentialSource; 69 | } 70 | 71 | protected function getPasskeyOptions(string $passkeyOptionsJson): PublicKeyCredentialCreationOptions 72 | { 73 | if (! json_validate($passkeyOptionsJson)) { 74 | throw InvalidPasskeyOptions::invalidJson(); 75 | } 76 | 77 | /** @var PublicKeyCredentialCreationOptions $passkeyOptions */ 78 | $passkeyOptions = Serializer::make()->fromJson( 79 | $passkeyOptionsJson, 80 | PublicKeyCredentialCreationOptions::class 81 | ); 82 | 83 | return $passkeyOptions; 84 | } 85 | 86 | protected function getPasskey(string $passkeyJson): PublicKeyCredential 87 | { 88 | if (! json_validate($passkeyJson)) { 89 | throw InvalidPasskey::invalidJson(); 90 | } 91 | 92 | /** @var PublicKeyCredential $publicKeyCredential */ 93 | $publicKeyCredential = Serializer::make()->fromJson( 94 | $passkeyJson, 95 | PublicKeyCredential::class 96 | ); 97 | 98 | return $publicKeyCredential; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Events/PasskeyUsedToAuthenticateEvent.php: -------------------------------------------------------------------------------- 1 | redirect) { 16 | Session::put('passkeys.redirect', $this->redirect); 17 | } 18 | 19 | return view('passkeys::components.authenticate'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Controllers/AuthenticateUsingPasskeyController.php: -------------------------------------------------------------------------------- 1 | execute( 25 | $request->get('start_authentication_response'), 26 | Session::get('passkey-authentication-options'), 27 | ); 28 | 29 | if (! $passkey) { 30 | return $this->invalidPasskeyResponse(); 31 | } 32 | 33 | /** @var Authenticatable $authenticatable */ 34 | $authenticatable = $passkey->authenticatable; 35 | 36 | if (! $authenticatable) { 37 | return $this->invalidPasskeyResponse(); 38 | } 39 | 40 | $this->logInAuthenticatable($authenticatable); 41 | 42 | $this->firePasskeyEvent($passkey, $request); 43 | 44 | return $this->validPasskeyResponse($request); 45 | } 46 | 47 | protected function logInAuthenticatable(Authenticatable $authenticatable): self 48 | { 49 | auth()->login($authenticatable); 50 | 51 | Session::regenerate(); 52 | 53 | return $this; 54 | } 55 | 56 | protected function validPasskeyResponse(Request $request): RedirectResponse 57 | { 58 | $url = Session::has('passkeys.redirect') 59 | ? Session::pull('passkeys.redirect') 60 | : Config::getRedirectAfterLogin(); 61 | 62 | return redirect($url); 63 | } 64 | 65 | protected function invalidPasskeyResponse(): RedirectResponse 66 | { 67 | session()->flash('authenticatePasskey::message', __('passkeys::passkeys.invalid')); 68 | 69 | return back(); 70 | } 71 | 72 | protected function firePasskeyEvent(Passkey $passkey, AuthenticateUsingPasskeysRequest $request): self 73 | { 74 | event(new PasskeyUsedToAuthenticateEvent($passkey, $request)); 75 | 76 | return $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Http/Controllers/GeneratePasskeyAuthenticationOptionsController.php: -------------------------------------------------------------------------------- 1 | execute(); 16 | 17 | Session::flash('passkey-registration-options', $options); 18 | 19 | return $options; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Requests/AuthenticateUsingPasskeysRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'json'], 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/LaravelPasskeysServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-passkeys') 21 | ->hasConfigFile() 22 | ->hasMigration('create_passkeys_table') 23 | ->hasViews() 24 | ->hasTranslations(); 25 | 26 | $this 27 | ->registerPasskeyRouteMacro() 28 | ->registerComponents(); 29 | } 30 | 31 | protected function registerPasskeyRouteMacro(): self 32 | { 33 | Route::macro('passkeys', function (string $prefix = 'passkeys') { 34 | Route::prefix($prefix)->group(function () { 35 | Route::get( 36 | 'authentication-options', 37 | GeneratePasskeyAuthenticationOptionsController::class 38 | )->name('passkeys.authentication_options'); 39 | 40 | Route::post( 41 | 'authenticate', 42 | AuthenticateUsingPasskeyController::class 43 | )->name('passkeys.login'); 44 | }); 45 | }); 46 | 47 | return $this; 48 | } 49 | 50 | public function registerComponents(): self 51 | { 52 | Blade::component('authenticate-passkey', AuthenticatePasskeyComponent::class); 53 | 54 | if (class_exists(Livewire::class)) { 55 | Livewire::component('passkeys', PasskeysComponent::class); 56 | } 57 | 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Livewire/PasskeysComponent.php: -------------------------------------------------------------------------------- 1 | $this->currentUser()->passkeys, 25 | ]); 26 | } 27 | 28 | public function validatePasskeyProperties(): void 29 | { 30 | $this->validate(); 31 | 32 | $this->dispatch('passkeyPropertiesValidated', [ 33 | 'passkeyOptions' => json_decode($this->generatePasskeyOptions()), 34 | ]); 35 | } 36 | 37 | public function storePasskey(string $passkey): void 38 | { 39 | $storePasskeyAction = Config::getAction('store_passkey', StorePasskeyAction::class); 40 | 41 | try { 42 | $storePasskeyAction->execute( 43 | $this->currentUser(), 44 | $passkey, $this->previouslyGeneratedPasskeyOptions(), 45 | request()->getHost(), 46 | ['name' => $this->name] 47 | ); 48 | } catch (Throwable $e) { 49 | throw ValidationException::withMessages([ 50 | 'name' => __('passkeys::passkeys.error_something_went_wrong_generating_the_passkey'), 51 | ])->errorBag('passkeyForm'); 52 | } 53 | 54 | $this->clearForm(); 55 | } 56 | 57 | public function deletePasskey(int $passkeyId): void 58 | { 59 | $this->currentUser()->passkeys()->where('id', $passkeyId)->delete(); 60 | } 61 | 62 | public function currentUser(): Authenticatable&HasPasskeys 63 | { 64 | /** @var Authenticatable&HasPasskeys $user */ 65 | $user = auth()->user(); 66 | 67 | return $user; 68 | } 69 | 70 | protected function clearForm(): void 71 | { 72 | $this->name = ''; 73 | } 74 | 75 | protected function generatePasskeyOptions(): string 76 | { 77 | $generatePassKeyOptionsAction = Config::getAction('generate_passkey_register_options', GeneratePasskeyRegisterOptionsAction::class); 78 | 79 | $options = $generatePassKeyOptionsAction->execute($this->currentUser()); 80 | 81 | session()->put('passkey-registration-options', $options); 82 | 83 | return $options; 84 | } 85 | 86 | protected function previouslyGeneratedPasskeyOptions(): ?string 87 | { 88 | return session()->pull('passkey-registration-options'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Models/Concerns/HasPasskeys.php: -------------------------------------------------------------------------------- 1 | $passkeys 11 | */ 12 | interface HasPasskeys 13 | { 14 | public function passkeys(): HasMany; 15 | 16 | public function getPassKeyName(): string; 17 | 18 | public function getPassKeyId(): string; 19 | 20 | public function getPassKeyDisplayName(): string; 21 | } 22 | -------------------------------------------------------------------------------- /src/Models/Concerns/InteractsWithPasskeys.php: -------------------------------------------------------------------------------- 1 | hasMany($passkeyModel, 'authenticatable_id'); 15 | } 16 | 17 | public function getPasskeyName(): string 18 | { 19 | return $this->email; 20 | } 21 | 22 | public function getPasskeyId(): string 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function getPasskeyDisplayName(): string 28 | { 29 | return $this->name; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Models/Passkey.php: -------------------------------------------------------------------------------- 1 | 'datetime', 28 | ]; 29 | } 30 | 31 | public function data(): Attribute 32 | { 33 | $serializer = Serializer::make(); 34 | 35 | return new Attribute( 36 | get: fn (string $value) => $serializer->fromJson( 37 | $value, 38 | PublicKeyCredentialSource::class 39 | ), 40 | set: fn (PublicKeyCredentialSource $value) => [ 41 | 'credential_id' => mb_convert_encoding($value->publicKeyCredentialId, 'UTF-8'), 42 | 'data' => $serializer->toJson($value), 43 | ], 44 | ); 45 | } 46 | 47 | public function authenticatable(): BelongsTo 48 | { 49 | $authenticatableModel = Config::getAuthenticatableModel(); 50 | 51 | return $this->belongsTo($authenticatableModel); 52 | } 53 | 54 | protected static function newFactory(): Factory 55 | { 56 | return PasskeyFactory::new(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Support/Config.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function getPassKeyModel(): string 18 | { 19 | $passkeyModel = config('passkeys.models.passkey'); 20 | 21 | if (! is_a($passkeyModel, Passkey::class, true)) { 22 | throw InvalidPasskeyModel::make($passkeyModel); 23 | } 24 | 25 | return config('passkeys.models.passkey'); 26 | } 27 | 28 | /** @return class-string */ 29 | public static function getAuthenticatableModel(): string 30 | { 31 | /** @var class-string $authenticatableModel */ 32 | $authenticatableModel = config('passkeys.models.authenticatable'); 33 | 34 | foreach ([Authenticatable::class, HasPasskeys::class] as $interface) { 35 | if (! is_a($authenticatableModel, $interface, true)) { 36 | throw InvalidAuthenticatableModel::missingInterface($authenticatableModel, $interface); 37 | } 38 | } 39 | 40 | return $authenticatableModel; 41 | } 42 | 43 | public static function getRelyingPartyName(): string 44 | { 45 | return config('passkeys.relying_party.name'); 46 | } 47 | 48 | public static function getRelyingPartyId(): string 49 | { 50 | return config('passkeys.relying_party.id'); 51 | } 52 | 53 | public static function getRelyingPartyIcon(): ?string 54 | { 55 | return config('passkeys.relying_party.icon'); 56 | } 57 | 58 | /** 59 | * @template T 60 | * 61 | * @param class-string $actionBaseClass 62 | * @return class-string 63 | */ 64 | public static function getActionClass(string $actionName, string $actionBaseClass): string 65 | { 66 | $actionClass = config("passkeys.actions.{$actionName}"); 67 | 68 | self::ensureValidActionClass($actionName, $actionBaseClass, $actionClass); 69 | 70 | return config("passkeys.actions.{$actionName}"); 71 | } 72 | 73 | /** 74 | * @template T 75 | * 76 | * @param class-string $actionBaseClass 77 | * @return T 78 | */ 79 | public static function getAction(string $actionName, string $actionBaseClass) 80 | { 81 | $actionClass = self::getActionClass($actionName, $actionBaseClass); 82 | 83 | return new $actionClass; 84 | } 85 | 86 | protected static function ensureValidActionClass(string $actionName, string $actionBaseClass, string $actionClass): void 87 | { 88 | if (! is_a($actionClass, $actionBaseClass, true)) { 89 | throw InvalidActionClass::make($actionName, $actionBaseClass, $actionClass); 90 | } 91 | } 92 | 93 | public static function getRedirectAfterLogin(): ?string 94 | { 95 | return redirect() 96 | ->intended(config('passkeys.redirect_to_after_login')) 97 | ->getTargetUrl(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Support/Serializer.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | return new self($serializer); 19 | } 20 | 21 | public function __construct( 22 | protected SymfonySerializer $serializer, 23 | ) {} 24 | 25 | public function toJson(mixed $value): string 26 | { 27 | return $this->serializer->serialize($value, 'json'); 28 | } 29 | 30 | /** 31 | * @param class-string $desiredClass 32 | */ 33 | public function fromJson(string $value, string $desiredClass): mixed 34 | { 35 | return $this 36 | ->serializer 37 | ->deserialize($value, $desiredClass, 'json'); 38 | } 39 | } 40 | --------------------------------------------------------------------------------