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 the trait to your User model:
use Laravel\Passkeys\PasskeyAuthenticatable;
class User extends Authenticatable
{
use PasskeyAuthenticatable;
}
Frontend integration (using @laravel/passkeys npm package):
import { Passkeys } from '@laravel/passkeys';
// Login flow
const options = await Passkeys.login();
const credential = await Passkeys.verify(options);
Test the login route:
/passkeys/login/options (returns WebAuthn options for the client)./passkeys/login (handles authentication).Registration (Authenticated User):
/user/passkeys/options → Client generates a new credential → Submit to /user/passkeys → Passkey stored in DB.Passkeys::register() in a controller or directly call the POST /user/passkeys route.Login (Guest):
/passkeys/login/options → Client shows available credentials → Submit assertion to /passkeys/login → User authenticated.PasskeyVerified event to trigger post-login actions (e.g., redirect, send welcome email).Reauthentication (2FA-like Confirmation):
GenerateVerificationOptions with an authenticated user to scope credentials to that user:
$options = app(GenerateVerificationOptions::class)($request->user());
VerifyPasskey with the user object to enforce ownership:
$passkey = app(VerifyPasskey::class)(
$request->credential(),
$options,
$request->user()
);
Passkey Management:
passkeys table (or your custom model).DELETE /user/passkeys/{id} (triggers PasskeyDeleted event).password.confirm to protect management routes.Route::middleware(['guest'])->group(function () {
Route::get('/login', function () {
return redirect()->route('passkeys.login.options');
});
});
guard in config to use a custom auth guard (e.g., api):
'guard' => 'api',
@laravel/passkeys npm package to manage WebAuthn state (e.g., store challenges in localStorage).Passkeys::authorizeLoginUsing(function ($request, $user, $passkey) {
if (!$passkey) {
return redirect()->route('login.email');
}
return true;
});
| Pattern | Implementation | Use Case |
|---|---|---|
| Passkey Registration | POST /user/passkeys with WebAuthn attestation response. |
Onboarding new users. |
| Passkey Login | POST /passkeys/login with WebAuthn assertion response. |
Guest authentication. |
| Reauthentication | VerifyPasskey with an authenticated user to scope credentials. |
Admin dashboards, sensitive actions. |
| Passkey Deletion | DELETE /user/passkeys/{id} with WebAuthn assertion for confirmation. |
User account cleanup. |
| Event Listening | Listen to PasskeyVerified to trigger post-login logic. |
Analytics, notifications. |
| Custom Middleware | Add auth.passkey middleware to protect routes. |
Role-based access control. |
Opaque User Handles:
getPasskeyAuthenticatorIdentifier(), ensure the handle is stable (e.g., hashed user_id + secret).Database Transactions:
'transactional_verification' => false,
Allowed Origins:
allowed_origins config must include all domains where passkeys are registered/verified (e.g., app.example.com, staging.example.com).env() to dynamically set them.Middleware Misconfiguration:
management_middleware config defaults to ['password.confirm']. If you set it to [], passkey management routes will be unprotected.['auth', 'verified']).Custom Model Route Bindings:
User or Passkey models, ensure their route keys match the package’s expectations (e.g., id for Passkey).getRouteKeyName() in your custom models if needed.WebAuthn Timeout:
NotAllowedError.'timeout' => 120000, // 2 minutes
Resident Key Requirement:
authenticatorSelection in a custom GenerateRegistrationOptions action.Passkey Deletion:
DELETE /user/passkeys/{id} route with WebAuthn assertion.WebAuthn Errors:
error field in the response for WebAuthn-specific errors (e.g., NotAllowedError, BadCredentialError).Database Locking:
Lock wait timeout, increase your database’s innodb_lock_wait_timeout (MySQL) or lock_timeout (PostgreSQL).Event Debugging:
AppServiceProvider to log passkey operations:
public function boot()
{
PasskeyVerified::listen(function ($user, $passkey) {
Log::info("Passkey verified for user {$user->id}", ['passkey_id' => $passkey->id]);
});
}
Custom Actions:
$this->app->bind(GenerateRegistrationOptions::class, function () {
return new class extends GenerateRegistrationOptions {
public function authenticatorSelection()
{
$criteria = parent::authenticatorSelection();
Log::debug('Authenticator selection:', $criteria->toArray());
return $criteria;
}
};
});
Passkey model to add custom fields (e.g., device_type, last_used_at):
class Passkey extends \Laravel\Passkeys\Passkey
How can I help you explore Laravel packages today?