robertboes/filament-passkeys
Filament Passkeys adds passwordless WebAuthn passkey login to Filament panels. Users register and sign in with biometrics or hardware security keys, and manage their own passkeys. Simple setup, configurable, and fully integrated with Filament's authentication flow.
Assuming the package wraps laravel/passkeys (WebAuthn) and exposes a Filament login button plus a credential-management UI:
composer require robertboes/filament-passkeys-plugin
php artisan vendor:publish --tag="filament-passkeys-migrations" # check exact tag in README
php artisan migrate
Register the plugin on the panel(s) that should accept passkeys:
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(PasskeysPlugin::make());
}
Then set the relying-party values — these are the ones that actually make or break the feature:
// config/filament-passkeys.php (names approximate — verify against src/)
'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST), // e.g. "example.com"
'origin' => config('app.url'), // full scheme+host+port
'user_verification' => 'preferred',
First use case to validate end-to-end: log in as a user, go to the security/profile page the plugin registers, click Add passkey, complete the OS prompt (Touch ID / Windows Hello / security key), log out, and sign back in from the Filament login screen using the passkey button. If that loop works on a real device over HTTPS, the integration is sound.
Where to look first, in order:
README.md — installation and the exact plugin/config class names.config/filament-passkeys.php — relying party, origin, user verification, and any toggle for auto-registration on the login page.src/ — the Filament page/plugin classes and the controllers handling the navigator.credentials.create/get round-trip. This is a small package; read it all before you ship it.database/migrations/ — the passkeys table shape (credential id, public key, counter, transports, last_used_at).Per-panel rollout. Don't enable it everywhere by default. Register the plugin only on the panel where it's earned its keep, and keep the password form on every panel as a fallback:
->plugin(PasskeysPlugin::make())
->login() // keep the password form
Treat passkeys as an additive second factor, then promote. The safest sequence for an existing app is: users enroll a passkey while still logging in with a password, run it for a release cycle, and only then consider passwordless-first. Because the package sits on top of Filament's auth guard, you can gate enrollment behind a policy or a simple check in a custom page:
public static function canAccess(): bool
{
return auth()->user()?->hasRole('admin')
&& auth()->user()->passkeys()->count() < 5;
}
Require recent password confirmation before enrolling. A passkey added on a hijacked session is a permanent backdoor. Wrap the enrollment action in Filament's password confirmation (or your own password.confirm middleware) so the user re-enters their password immediately before the WebAuthn prompt.
Give users a management surface. Even if the plugin ships a page, most teams end up with a small Filament page or relation manager listing credentials by name, created_at, last_used_at, with a Revoke action. Nudge users to name devices ("MacBook – Chrome") at registration; an unnamed list of four credentials is unusable for support.
Always keep a recovery path. Never let a user delete their last credential if password login is disabled for them, and never disable password login for an account with zero passkeys. For admins, provide an artisan command or Filament action to purge a user's passkeys — otherwise "I lost my phone" becomes a manual DB edit at 2am.
Pair with email-based recovery. For passwordless flows, pair passkeys with a signed magic link or Fortify's password reset. The passkey is the fast path, not the only path.
Testing. You cannot exercise real WebAuthn in a plain PHPUnit feature test — there's no authenticator. Split it:
WebAuthn.enable + WebAuthn.addVirtualAuthenticator) for the actual create/get ceremony.HTTPS is non-negotiable. navigator.credentials only exists in a secure context. localhost is exempt, http://staging.example.com is not. If the button renders but nothing happens, check for a secure-context error in the console before blaming the package.
RP ID and origin must match reality, not APP_URL. The browser validates the origin against the RP ID. A mismatch between your proxy/load-balancer host, APP_URL, and config('filament-passkeys.origin') produces confusing SecurityErrors. Behind a proxy, make sure TrustProxies is configured so Laravel sees the real scheme and host. Subdomains matter: rp_id = example.com covers admin.example.com; rp_id = admin.example.com does not cover the apex.
Credentials are bound to the origin. Everything enrolled on staging is useless in production and vice versa. Plan for re-enrollment between environments, and consider disabling the plugin entirely outside production to avoid confusing testers.
Synced vs device-bound passkeys. iCloud Keychain and Google Password Manager sync credentials; hardware keys don't. Users will assume a passkey "is on their phone" and be surprised on a new device. Set user verification to preferred rather than required unless you have a compliance reason — required breaks some security keys and older platform authenticators.
The assertion endpoint is a POST from JS, not a Livewire call. If you see 419s, it's CSRF/session — verify the endpoint is inside the right middleware group (web) and that the fetch includes credentials. Filament panel middleware stacks differ per panel; a route that works on admin may 419 on a second panel.
This package is very young — 3 stars, 0 dependents, single release. That is the dominant risk here, not the WebAuthn code. Mitigations:
composer.json; don't use ^ on a 0.x/1.x with one release.src/ tree (~an afternoon) so you can debug and fork it. Budget for owning it.web-auth/webauthn-lib / laravel/passkeys version it requires and whether that is maintained; the real security surface is there.laravel/passkeys + Fortify directly if your auth flow is simple and you only need a login button — you keep the dependency count down and Filament gives you the views anyway.Extension points worth knowing. Publish the views and translations before you edit anything in vendor/; use a custom Passkey model via config if you need extra columns (device label, IP, last-used tracking); listen for whatever registration/authentication events the package dispatches to feed an audit log. If the package doesn't expose an event you need, that's a signal to wrap its controllers in your own rather than patch it.
Debugging checklist, in order: console error in the browser → chrome://webauthn-internals (or Safari's WebAuthn pane) → check the RP ID/origin the browser reports vs your config → confirm the challenge row was persisted and consumed → check storage/logs for the underlying WebAuthn library's exception, which is far more specific than anything Filament shows you.
How can I help you explore Laravel packages today?