laravel/passkeys
Add passwordless WebAuthn/passkey authentication to Laravel. Install migrations, add a trait/contract to your User model, and use the @laravel/passkeys JS client for registration and login. Includes built-in routes for login, confirmation, and passkey management.
Install the package:
composer require laravel/passkeys
php artisan vendor:publish --tag=passkeys-migrations
php artisan migrate
Add trait to your User model (e.g., app/Models/User.php):
use Laravel\Passkeys\PasskeyAuthenticatable;
class User extends Authenticatable implements PasskeyUser
{
use PasskeyAuthenticatable;
}
Frontend integration (e.g., in your login blade or SPA):
import { Passkeys } from '@laravel/passkeys';
await Passkeys.register({ name: 'My Device' }); // Triggers WebAuthn flow
Test registration:
/user/passkeys/options (returns WebAuthn options for registration)./user/passkeys to store the passkey./passkeys/login/options → Returns WebAuthn challenge./passkeys/login → Authenticates user./) or custom response (via PasskeyLoginResponse).config/passkeys.php: Adjust allowed_origins, guard, or middleware.app/Models/User.php: Override getPasskeyDisplayName() if needed.routes/web.php: Verify auto-registered routes aren’t conflicting.Frontend:
// 1. Fetch options
const options = await fetch('/passkeys/login/options').then(r => r.json());
// 2. Show authenticator prompt (e.g., Touch ID)
const credential = await navigator.credentials.get({ publicKey: options });
// 3. Submit assertion
await fetch('/passkeys/login', {
method: 'POST',
body: JSON.stringify({ credential }),
headers: { 'Content-Type': 'application/json' },
});
Backend:
VerifyPasskey action → triggers PasskeyVerified event.authorizeLoginUsing callback to block suspended accounts:
Passkeys::authorizeLoginUsing(function ($request, $user, $passkey) {
return !$user->is_banned; // Custom logic
});
Register a new passkey (authenticated user):
const options = await fetch('/user/passkeys/options').then(r => r.json());
const credential = await navigator.credentials.create({ publicKey: options });
await fetch('/user/passkeys', { method: 'POST', body: JSON.stringify({ credential }) });
Delete a passkey:
await fetch(`/user/passkeys/${passkeyId}`, { method: 'DELETE' });
// In PasskeyLoginResponse::toResponse()
auth()->login($user, true); // Persist session
GenerateVerificationOptions for reauth:
$options = app(GenerateVerificationOptions::class)($request->user());
// Frontend submits to `/passkeys/confirm` (no login).
AppServiceProvider:
Passkeys::useUserModel(CustomUser::class);
Passkeys::usePasskeyModel(CustomPasskey::class);
Origin Mismatch:
config('passkeys.allowed_origins') includes all frontend domains (e.g., app.example.com, staging.app.example.com).NotAllowedError if origins don’t match.Pessimistic Locking:
selectForUpdate() → avoid in read-heavy apps without transactions.DB::transaction() if needed.User Handle Secrets:
APP_KEY. Change PASSKEYS_USER_HANDLE_SECRET in .env for production.Middleware Conflicts:
management_middleware (e.g., password.confirm) may block API routes. Exclude with:
Passkeys::ignoreRoutes(); // Then register custom routes.
webauthn-json-serializer to inspect raw responses:
use Webauthn\Serializer\Json\JsonSerializer;
$serializer = new JsonSerializer();
$serializer->serialize($options); // Log to debug.
passkeys table for orphaned records if PasskeyUser contract isn’t implemented.php artisan passkeys:prune (if available) to clean up failed registrations.Custom Attestation:
StorePasskey to validate attestation statements:
class CustomStorePasskey extends StorePasskey
{
public function validateAttestation(AttestationStatement $statement): void
{
if (!$statement->isFidoU2f()) {
throw new \RuntimeException('Unsupported attestation.');
}
}
}
Bind in AppServiceProvider:
$this->app->bind(StorePasskey::class, CustomStorePasskey::class);
Rate Limiting:
throttle config or use middleware:
Passkeys::middleware(['throttle:10,1']); // 10 attempts/minute.
Passkey Metadata:
passkeys table (e.g., device_type, last_used_at) and extend Passkey model:
protected $casts = ['last_used_at' => 'datetime'];
vendor/webauthn/webauthn-lib/src/Attestation/AAGUIDCatalog.php.@laravel/passkeys npm package with webauthn-mock:
import { mockPasskeys } from '@laravel/passkeys/testing';
mockPasskeys(); // Simulates passkey flows in tests.
if (auth()->attempt(['email' => $email, 'password' => $password])) {
// Fallback to password login.
}
How can I help you explore Laravel packages today?