spatie/laravel-passkeys
Add passkey (WebAuthn) login to Laravel without passwords. Includes Livewire components to register/generate passkeys and a Blade component to authenticate users using device-stored credentials (1Password, macOS Keychain, etc.).
Installation
composer require spatie/laravel-passkeys
Ensure Livewire is installed (composer require livewire/livewire).
Publish Config
php artisan vendor:publish --provider="Spatie\Passkeys\PasskeysServiceProvider" --tag="passkeys-config"
Update config/passkeys.php with your app’s relying party details (e.g., rp_name, rp_id).
Run Migrations
php artisan migrate
Creates a passkeys table to store credentials.
First Use Case: Registration Add the Livewire component to your registration form:
<x-passkeys::register />
This renders a passkey registration UI (e.g., "Add a Passkey" button).
Passkey Model: Stores passkey credentials (extends Spatie\Passkeys\Models\Passkey).PasskeyManager: Core logic for generating/verifying passkeys (injected via Laravel’s service container).RegisterPasskey: Handles passkey creation.AuthenticateWithPasskey: Handles authentication.<x-passkeys::authenticate />
use Spatie\Passkeys\Facades\Passkey;
if (Passkey::authenticate($request)) {
// User authenticated via passkey
return redirect()->intended('/dashboard');
}
<x-passkeys::register /> in your registration form.register component’s props (e.g., user, redirectTo):
<x-passkeys::register
:user="$user"
redirectTo="/dashboard"
/>
PasskeyRegistered events to trigger post-registration actions (e.g., send welcome email):
use Spatie\Passkeys\Events\PasskeyRegistered;
PasskeyRegistered::listen(function (Passkey $passkey) {
// Send email or log activity
});
Override the GeneratePasskeyRegisterOptions class to modify passkey behavior (e.g., enforce authenticator attachment):
use Spatie\Passkeys\PasskeyRegisterOptions;
class CustomPasskeyRegisterOptions extends PasskeyRegisterOptions
{
public function __construct()
{
parent::__construct();
$this->authenticatorSelection->requireResidentKey();
$this->authenticatorSelection->requireUserVerification();
}
}
Register it in config/passkeys.php:
'register_options' => \App\CustomPasskeyRegisterOptions::class,
Embed <x-passkeys::authenticate /> in your login form. Supports:
remember: Boolean to enable "Remember Me" (default: false).redirectTo: Post-authentication redirect URL.Verify passkey authentication in your login controller:
public function login(Request $request)
{
if (Passkey::authenticate($request)) {
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
// Fallback to password auth
// ...
}
Fetch a user’s passkeys:
$user->passkeys; // Collection of Passkey models
Delete a passkey:
$user->passkeys()->where('id', $passkeyId)->delete();
Pass passkey data to Inertia:
return Inertia::render('Auth/Login', [
'passkeys' => auth()->user()->passkeys->map(fn ($passkey) => [
'id' => $passkey->id,
'name' => $passkey->name,
'type' => $passkey->type,
]),
]);
Use the webauthn library in Vue to handle passkey operations client-side.
Use the Passkey facade in tests:
use Spatie\Passkeys\Facades\Passkey;
public function test_passkey_authentication()
{
$user = User::factory()->create();
$passkey = Passkey::createForUser($user);
$response = Passkey::authenticate(
new Request([
'passkey' => $passkey->toArray(),
])
);
$this->assertTrue($response->authenticated());
}
Relying Party ID (rp_id):
Must match your domain exactly (e.g., https://your-app.com). Use spatie/laravel-passkeys:generate-rp-id Artisan command to generate it:
php artisan passkeys:generate-rp-id
Gotcha: If rp_id mismatches, browsers will reject passkey operations.
Allowed Origins:
Configure allowed_origins in config/passkeys.php to restrict passkey usage to specific domains:
'allowed_origins' => ['https://your-app.com', 'https://staging.your-app.com'],
Tip: Use null to allow all origins (not recommended for production).
| Issue | Solution |
|---|---|
| Passkey registration fails | Check browser console for NotAllowedError (likely rp_id mismatch). |
| Authentication timeouts | Ensure authenticateWithPasskey middleware is not interfering. |
| Livewire component not rendering | Verify Livewire is installed and the component is properly namespaced. |
| "No passkeys found" | Check if the passkeys table has records for the user. |
Debugging Tip:
Enable WebAuthn logging in config/passkeys.php:
'debug' => env('PASSKEYS_DEBUG', false),
Logs will appear in Laravel’s log files.
Extend the Passkey model to add custom fields:
use Spatie\Passkeys\Models\Passkey as BasePasskey;
class Passkey extends BasePasskey
{
protected $casts = [
'metadata' => 'json',
'created_at' => 'datetime:Y-m-d H:i:s',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
Update config/passkeys.php:
'model' => \App\Models\Passkey::class,
Extend the PasskeyRegistered event to add custom logic:
use Spatie\Passkeys\Events\PasskeyRegistered;
PasskeyRegistered::listen(function (Passkey $passkey) {
// Example: Log passkey creation
\Log::info("Passkey registered for user {$passkey->user->email}");
});
Route::middleware(['throttle:10,1'])->group(function () {
Route::post('/passkey/authenticate', [PasskeyController::class, 'authenticate']);
});
requireUserVerification() in PasskeyRegisterOptions to prevent silent authentications.$passkey = Cache::remember("passkey_{$user->id}", now()->addHours(1), function () use ($user) {
return $user->passkeys()->latest()->first();
});
with() to eager-load passkeys in queries:
$user = User::with('passkeys')->find($id);
User::where('email_verified_at', null)->chunk(100, function ($users) {
foreach ($users as $user) {
Passkey::createForUser($user);
}
});
passkeys table, run:
php artisan migrate --path=/vendor/spatie/laravel-passkeys/database/migrations
Then add your custom migrations.How can I help you explore Laravel packages today?