spatie/laravel-passkeys
Add passkey (WebAuthn) login to your Laravel app. Provides a Livewire component to create and manage passkeys and a Blade component to authenticate users without passwords, using platform authenticators like iCloud Keychain or 1Password.
Installation:
composer require spatie/laravel-passkeys
Ensure Livewire is installed (composer require livewire/livewire).
Publish Config:
php artisan vendor:publish --provider="Spatie\Passkeys\PasskeysServiceProvider"
Configure config/passkeys.php (e.g., relying_party_name, allowed_origins).
Run Migrations:
php artisan migrate
Creates passkeys table for storing credentials.
First Use Case:
<x-passkeys::register />
<x-passkeys::authenticate />
<livewire:passkeys.register
:user="$user"
:options="['challenge' => $challenge, 'rp' => ['name' => config('passkeys.relying_party_name')]]"
/>
use Spatie\Passkeys\GeneratePasskeyRegisterOptions;
$options = GeneratePasskeyRegisterOptions::create()
->withAuthenticatorSelection([
'authenticatorAttachment' => 'platform',
'requireResidentKey' => true,
]);
<x-passkeys::authenticate
:user="$user"
:options="['challenge' => $challenge, 'rpId' => config('passkeys.relying_party_id')]"
/>
use Spatie\Passkeys\AuthenticateWithPasskey;
$authenticator = AuthenticateWithPasskey::create($user)
->withChallenge($challenge)
->withRemember($request->has('remember'))
->authenticate();
$passkeys = $user->passkeys()->get();
$user->passkeys()->where('id', $passkeyId)->delete();
use Spatie\Passkeys\Events\PasskeyRegistered;
PasskeyRegistered::subscribe(function (PasskeyRegistered $event) {
// Log or notify when a passkey is registered
});
passkeys array to your Inertia props:
return Inertia::render('Auth/Register', [
'passkeys' => $user->passkeys()->get()->toArray(),
]);
MustVerifyEmail or third-party MFA packages:
use Spatie\Passkeys\HasPasskeys;
class User extends Authenticatable
{
use HasPasskeys, MustVerifyEmail;
}
<x-passkeys::authenticate>
@slot('before')
<p>Use your passkey for secure login.</p>
@endslot
</x-passkeys::authenticate>
WebAuthn Library Version Conflicts:
web-auth/webauthn-lib is compatible (v5.3+ requires adjustments for CredentialRecord).Passkey model’s data accessor:
public function getDataAttribute(): array
{
return $this->attributes['data'] ?? [];
}
Relying Party ID (RP ID):
config('passkeys.relying_party_id').Challenge Persistence:
session(['passkey_challenge' => $challenge]);
Livewire Component State:
wire:ignore or persist state.Allowed Origins:
allowed_origins in config/passkeys.php can break passkey registration/authentication.http://localhost and your production domain during development.Enable WebAuthn Debugging:
WEB_AUTHN_DEBUG in .env to log WebAuthn errors:
WEB_AUTHN_DEBUG=true
webauthn-lib exceptions.Validate Credential Responses:
webauthn-lib validator directly:
use WebAuthn\Validation;
$validator = new Validation();
$result = $validator->validatePresence($credential, $options);
Inspect Passkey Data:
dd($user->passkeys()->first()->attributes);
Custom Passkey Model:
Spatie\Passkeys\Models\Passkey to add fields (e.g., device_name):
class CustomPasskey extends Passkey
{
protected $casts = [
'device_name' => 'string',
];
}
Override Default Options:
GeneratePasskeyRegisterOptions:
GeneratePasskeyRegisterOptions::macro('withCustomRP', function () {
return $this->withRelyingParty([
'name' => 'Custom App',
'id' => 'custom.app',
]);
});
Custom Events:
PasskeyRegistered or create new events (e.g., PasskeyDeleted):
event(new CustomPasskeyEvent($user, $passkey));
Fallback Authentication:
public function handle($request, Closure $next)
{
if ($request->has('passkey') && !$request->user()->hasPasskeys()) {
return redirect()->route('login');
}
return $next($request);
}
Relying Party Name:
config('passkeys.relying_party_name') should be YourApp for your-app.com).Allowed Origins:
localhost, app.yourdomain.com):
'allowed_origins' => [
'http://localhost:3000',
'https://app.yourdomain.com',
],
Challenge Generation:
Str::random(32) for challenges, but ensure uniqueness per session:
$challenge = Str::random(32);
session(['passkey_challenge' => $challenge]);
User Model Binding:
HasPasskeys trait is used in your User model:
use Spatie\Passkeys\HasPasskeys;
class User extends Authenticatable
{
use HasPasskeys;
}
Database Indexes:
passkeys table for user_id and credential_id:
Schema::table('passkeys', function (Blueprint $table) {
$table->index('user_id');
$table->index('credential_id');
});
Caching Challenges:
Cache::put("passkey_challenge_{$user->id}", $challenge, now()->addMinutes(5));
Batch Operations:
$passkeys = $user->passkeys()->paginate(10);
How can I help you explore Laravel packages today?