# Laravel Passkeys
Laravel Passkeys is a comprehensive package by Spatie that enables passwordless authentication in Laravel applications using WebAuthn passkeys. This package eliminates the need for traditional passwords by allowing users to authenticate using biometric data, security keys, or device-based credentials stored in password managers like 1Password, macOS Keychain, or browser password managers.
The package provides a seamless integration with Laravel through Livewire components for passkey registration and Blade components for authentication. It leverages the web-auth/webauthn-lib library to handle the WebAuthn protocol, offering a production-ready solution for implementing modern, secure authentication flows. The package includes customizable actions for all core operations, comprehensive localization support for 14 languages, and full event-driven architecture for tracking authentication events.
## Installation and Configuration
Setup the package and configure relying party settings.
```bash
# Install via Composer
composer require spatie/laravel-passkeys
# Publish configuration file
php artisan vendor:publish --tag="passkeys-config"
# Publish and run migrations
php artisan vendor:publish --tag="passkeys-migrations"
php artisan migrate
# Register routes in routes/web.php
Route::passkeys();
# Or with custom prefix
Route::passkeys('auth/passkeys');
```
```php
// config/passkeys.php - Configure relying party and models
return [
'redirect_to_after_login' => '/dashboard',
'actions' => [
'generate_passkey_register_options' => Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction::class,
'store_passkey' => Spatie\LaravelPasskeys\Actions\StorePasskeyAction::class,
'generate_passkey_authentication_options' => Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction::class,
'find_passkey' => Spatie\LaravelPasskeys\Actions\FindPasskeyToAuthenticateAction::class,
],
'relying_party' => [
'name' => config('app.name'),
'id' => parse_url(config('app.url'), PHP_URL_HOST),
'icon' => null,
],
'models' => [
'passkey' => Spatie\LaravelPasskeys\Models\Passkey::class,
'authenticatable' => App\Models\User::class,
],
];
```
## User Model Setup
Add passkey functionality to authenticatable models.
```php
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\LaravelPasskeys\Models\Concerns\InteractsWithPasskeys;
use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys;
class User extends Authenticatable implements HasPasskeys
{
use InteractsWithPasskeys;
protected $fillable = ['name', 'email', 'password'];
// The InteractsWithPasskeys trait provides default implementations:
// - passkeys(): HasMany relationship
// - getPasskeyName(): returns $this->email
// - getPasskeyId(): returns $this->id
// - getPasskeyDisplayName(): returns $this->name
// Optional: Override to customize passkey user entity
public function getPasskeyName(): string
{
return $this->email;
}
public function getPasskeyDisplayName(): string
{
return $this->name ?? $this->email;
}
public function getPasskeyId(): string
{
return (string) $this->id;
}
}
```
## Passkey Registration Component
Display and manage user passkeys with Livewire.
```blade
{{-- resources/views/profile/passkeys.blade.php --}}
Manage Passkeys
{{-- Livewire component for passkey management --}}
```
```php
// The PasskeysComponent handles:
// 1. Displaying existing passkeys with last used timestamps
// 2. Creating new passkeys with custom names
// 3. Deleting passkeys
// 4. Validating and storing passkey credentials
// app/Livewire/CustomPasskeysComponent.php - Optional customization
namespace App\Livewire;
use Spatie\LaravelPasskeys\Livewire\PasskeysComponent;
class CustomPasskeysComponent extends PasskeysComponent
{
// Override to add custom validation rules
protected function rules(): array
{
return [
'name' => 'required|string|max:255|unique:passkeys,name,' . auth()->id() . ',authenticatable_id',
];
}
}
```
## Passkey Authentication Component
Add passkey login to authentication forms.
```blade
{{-- resources/views/auth/login.blade.php --}}
Login
{{-- Traditional email/password form --}}
Or
{{-- Passkey authentication component with custom redirect --}}
```
## Generate Passkey Registration Options
Create WebAuthn options for passkey registration.
```php
// app/Actions/CustomGeneratePasskeyRegisterOptionsAction.php
namespace App\Actions;
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction;
use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\PublicKeyCredentialCreationOptions;
class CustomGeneratePasskeyRegisterOptionsAction extends GeneratePasskeyRegisterOptionsAction
{
public function execute(HasPasskeys $authenticatable, bool $asJson = true): string|PublicKeyCredentialCreationOptions
{
// Customize authenticator selection criteria
$options = parent::execute($authenticatable, false);
return $asJson ? Serializer::make()->toJson($options) : $options;
}
// Override to require user verification
public function authenticatorSelection(): AuthenticatorSelectionCriteria
{
return new AuthenticatorSelectionCriteria(
authenticatorAttachment: null, // null allows both platform and cross-platform
userVerification: AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED,
residentKey: AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED,
);
}
}
```
```php
// Register custom action in config/passkeys.php
'actions' => [
'generate_passkey_register_options' => App\Actions\CustomGeneratePasskeyRegisterOptionsAction::class,
// ... other actions
],
```
## Store Passkey Action
Validate and persist passkey credentials.
```php
// app/Actions/CustomStorePasskeyAction.php
namespace App\Actions;
use Spatie\LaravelPasskeys\Actions\StorePasskeyAction;
use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys;
use Spatie\LaravelPasskeys\Models\Passkey;
use Illuminate\Support\Facades\Log;
class CustomStorePasskeyAction extends StorePasskeyAction
{
public function execute(
HasPasskeys $authenticatable,
string $passkeyJson,
string $passkeyOptionsJson,
string $hostName,
array $additionalProperties = [],
): Passkey {
// Add custom metadata
$additionalProperties['user_agent'] = request()->userAgent();
$additionalProperties['ip_address'] = request()->ip();
$additionalProperties['created_from'] = 'web';
$passkey = parent::execute(
$authenticatable,
$passkeyJson,
$passkeyOptionsJson,
$hostName,
$additionalProperties
);
// Log passkey creation
Log::info('Passkey created', [
'user_id' => $authenticatable->getKey(),
'passkey_id' => $passkey->id,
'name' => $additionalProperties['name'] ?? 'Unnamed',
]);
// Send notification
$authenticatable->notify(new PasskeyCreatedNotification($passkey));
return $passkey;
}
}
```
## Authenticate Using Passkey
Handle passkey-based authentication requests.
```php
// The package automatically registers these routes via Route::passkeys():
// GET /passkeys/authentication-options - Generate authentication challenge
// POST /passkeys/authenticate - Verify passkey and log in user
// Example JavaScript integration (typically in your Blade view)
```
```javascript
// resources/js/passkey-auth.js
async function authenticateWithPasskey() {
try {
// 1. Get authentication options from server
const optionsResponse = await fetch('/passkeys/authentication-options', {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'include'
});
const options = await optionsResponse.json();
// 2. Convert base64 strings to ArrayBuffers
options.challenge = base64ToArrayBuffer(options.challenge);
// 3. Request credential from authenticator
const credential = await navigator.credentials.get({
publicKey: options
});
// 4. Prepare credential data for server
const credentialData = {
id: credential.id,
rawId: arrayBufferToBase64(credential.rawId),
type: credential.type,
response: {
authenticatorData: arrayBufferToBase64(credential.response.authenticatorData),
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
signature: arrayBufferToBase64(credential.response.signature),
userHandle: credential.response.userHandle ? arrayBufferToBase64(credential.response.userHandle) : null
}
};
// 5. Send to server for verification
const form = document.getElementById('passkey-login-form');
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'start_authentication_response';
input.value = JSON.stringify(credentialData);
form.appendChild(input);
form.submit();
} catch (error) {
console.error('Passkey authentication failed:', error);
alert('Authentication failed. Please try again or use password.');
}
}
// Helper functions for base64 encoding/decoding
function base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
```
## Passkey Model and Relationships
Access and manage stored passkey credentials.
```php
// Working with the Passkey model
use Spatie\LaravelPasskeys\Models\Passkey;
use App\Models\User;
// Get all passkeys for a user
$user = User::find(1);
$passkeys = $user->passkeys;
// Get recently used passkeys
$recentPasskeys = $user->passkeys()
->whereNotNull('last_used_at')
->orderByDesc('last_used_at')
->take(5)
->get();
// Find specific passkey
$passkey = Passkey::find(1);
$owner = $passkey->authenticatable; // Returns the User model
// Access passkey data
$credentialId = $passkey->credential_id;
$publicKeyCredentialSource = $passkey->data; // Returns PublicKeyCredentialSource object
$lastUsed = $passkey->last_used_at; // Carbon instance
$name = $passkey->name; // User-defined name like "MacBook Pro" or "iPhone"
// Delete passkey
$passkey->delete();
// Update last used timestamp
$passkey->update(['last_used_at' => now()]);
```
```php
// database/migrations/xxxx_create_passkeys_table.php
Schema::create('passkeys', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class, 'authenticatable_id')
->constrained('users')
->cascadeOnDelete();
$table->text('name'); // User-friendly name
$table->text('credential_id'); // Base64 encoded credential ID
$table->json('data'); // Serialized PublicKeyCredentialSource
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
```
## Listen to Passkey Authentication Events
Track when passkeys are used for authentication.
```php
// app/Listeners/LogPasskeyAuthentication.php
namespace App\Listeners;
use Spatie\LaravelPasskeys\Events\PasskeyUsedToAuthenticateEvent;
use Illuminate\Support\Facades\Log;
use App\Models\LoginHistory;
class LogPasskeyAuthentication
{
public function handle(PasskeyUsedToAuthenticateEvent $event): void
{
$passkey = $event->passkey;
$request = $event->request;
$user = $passkey->authenticatable;
// Update last used timestamp
$passkey->update(['last_used_at' => now()]);
// Log authentication event
Log::info('User authenticated with passkey', [
'user_id' => $user->id,
'passkey_id' => $passkey->id,
'passkey_name' => $passkey->name,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// Store in database for audit trail
LoginHistory::create([
'user_id' => $user->id,
'method' => 'passkey',
'passkey_id' => $passkey->id,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'successful' => true,
'logged_in_at' => now(),
]);
// Send notification for security monitoring
if ($this->isUnusualLogin($user, $request)) {
$user->notify(new UnusualLoginDetectedNotification($passkey, $request));
}
}
private function isUnusualLogin($user, $request): bool
{
// Check if login from new location or device
$recentLogins = LoginHistory::where('user_id', $user->id)
->where('created_at', '>', now()->subDays(30))
->get();
return !$recentLogins->contains('ip_address', $request->ip());
}
}
```
```php
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Spatie\LaravelPasskeys\Events\PasskeyUsedToAuthenticateEvent;
use App\Listeners\LogPasskeyAuthentication;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
PasskeyUsedToAuthenticateEvent::class => [
LogPasskeyAuthentication::class,
],
];
}
```
## Custom Passkey Configuration and Actions
Extend and customize core package behavior.
```php
// app/Actions/FindPasskeyToAuthenticateAction.php
namespace App\Actions;
use Spatie\LaravelPasskeys\Actions\FindPasskeyToAuthenticateAction as BaseAction;
use Spatie\LaravelPasskeys\Models\Passkey;
class CustomFindPasskeyToAuthenticateAction extends BaseAction
{
public function execute(string $credentialJson, string $optionsJson): ?Passkey
{
$passkey = parent::execute($credentialJson, $optionsJson);
if (!$passkey) {
return null;
}
// Add custom validation
if ($passkey->authenticatable->is_suspended) {
\Log::warning('Suspended user attempted passkey login', [
'user_id' => $passkey->authenticatable_id
]);
return null;
}
// Check if passkey is expired (custom business logic)
if ($passkey->created_at->addMonths(6)->isPast()) {
\Log::warning('Expired passkey used', ['passkey_id' => $passkey->id]);
$passkey->delete();
return null;
}
return $passkey;
}
}
```
```php
// config/passkeys.php - Register all custom actions
return [
'redirect_to_after_login' => '/dashboard',
'actions' => [
'generate_passkey_register_options' => App\Actions\CustomGeneratePasskeyRegisterOptionsAction::class,
'store_passkey' => App\Actions\CustomStorePasskeyAction::class,
'generate_passkey_authentication_options' => Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction::class,
'find_passkey' => App\Actions\CustomFindPasskeyToAuthenticateAction::class,
'configure_ceremony_step_manager_factory' => Spatie\LaravelPasskeys\Actions\ConfigureCeremonyStepManagerFactoryAction::class,
],
'relying_party' => [
'name' => config('app.name'),
'id' => env('PASSKEY_RELYING_PARTY_ID', parse_url(config('app.url'), PHP_URL_HOST)),
'icon' => env('PASSKEY_RELYING_PARTY_ICON'),
],
'models' => [
'passkey' => Spatie\LaravelPasskeys\Models\Passkey::class,
'authenticatable' => App\Models\User::class,
],
];
```
## Summary
Laravel Passkeys provides a complete, production-ready solution for implementing passwordless authentication in Laravel applications. The package handles the complex WebAuthn protocol through an intuitive API, allowing users to register and authenticate using biometric sensors, security keys, or platform authenticators. This modern authentication method improves both security and user experience by eliminating password-related vulnerabilities like phishing, credential stuffing, and password reuse.
The package is designed with flexibility and extensibility in mind. All core operations are implemented as customizable action classes, allowing developers to extend functionality for specific business requirements such as rate limiting, audit logging, or multi-factor authentication flows. The package integrates seamlessly with Laravel's authentication system, supports multiple authenticatable models, provides comprehensive event hooks for tracking usage, and includes localization support for international applications. Whether you're building a new application or adding passkey support to an existing one, Laravel Passkeys provides the foundation for secure, modern authentication.