roundly-consulting/passkeys-for-laravel
Add passkey (WebAuthn) authentication to Laravel apps. Register and verify passkeys so users can sign in with Face ID, Touch ID, or a security key instead of a password.
composer require roundly-consulting/passkeys-for-laravel. The service provider auto-discovers; if you use dont-discover or register providers by hand, add it to bootstrap/providers.php (Laravel 11+) or config/app.php.php artisan vendor:publish — check the README for the exact tags). The config is the map: relying-party ID/origins, route registration, and where the user handle comes from.Schema::table('users', function (Blueprint $table) {
$table->passkeyUserHandle();
});
php artisan migrate.Treat the ceremony as two endpoints, not one. The registration/assertion options endpoint mints the challenge and returns PublicKeyCredentialCreationOptions / PublicKeyCredentialRequestOptions; the browser calls navigator.credentials.create() or .get(); your verify endpoint posts the result back. Keep both as thin JSON controllers — put your own policy (who may enrol, how many credentials) in a form request or action class, not in the ceremony.
Layer it onto existing auth rather than replacing it. The pragmatic rollout is: password login stays, and after a successful login you offer "add a passkey" from the account settings page. Offer passkey login on the login screen only when PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() is true, and keep a recovery path (password reset, magic link, recovery codes) from day one — users lose devices.
Support multiple credentials per user. Enrolment is per-device, so your settings screen needs a list, human-readable labels, last_used_at, and a delete action. Refuse to delete the last credential if passkeys are the user's only login method.
Testing. Feature-test the options endpoint for shape (challenge present and non-trivial, rp.id matches config) and feature-test verify with a deliberately malformed/failed assertion — asserting the user is not authenticated. Real ceremonies belong in a browser test; drive Chrome via CDP WebAuthn.addVirtualAuthenticator (Dusk/Playwright) rather than hand-crafting attestations.
Keep static analysis in CI. Put larastan and phpstan/extension-installer in require-dev so migrations using the macro type-check without extra wiring (see below).
passkeyUserHandle() macro only exists once the provider boots. Larastan usually boots an auto-discovered host app and finds it, but not under Testbench package development, dont-discover, or a manually registered provider — there migrations fail PHPStan with "Call to an undefined method …Blueprint::passkeyUserHandle()". 1.1.3 ships an extension loaded automatically by phpstan/extension-installer via extra.phpstan.includes.vendor/roundly-consulting/passkeys-for-laravel/extension.neon to the includes: of your phpstan.neon. If analyses pass locally but fail in CI, suspect composer install --no-plugins / --no-scripts silencing extension-installer.testbench.yaml so the macro is registered before your package's own migrations are analysed.php artisan migrate is fine.localhost and 127.0.0.1 are different origins; staging and production each need their own RP ID and allowed origins or credentials silently fail to validate. php artisan config:clear after changing them.How can I help you explore Laravel packages today?