jeffersongoncalves/filament-multifactor-passkeys
Add passkey (WebAuthn) multi-factor authentication to Filament panels. Users register passkeys on their devices and confirm sign-in with biometrics or a PIN instead of one-time codes, with simple setup, recovery, and per-user passkey management.
Pre-flight (do this before composer require). This package is a hard pin on Filament 5, Laravel 11/12 and PHP 8.2+. The single most common install failure is a version mismatch — check composer show filament/filament first. If you're on Filament 3 or 4, stop; there is no backport.
composer require jeffersongoncalves/filament-multifactor-passkeys
This transitively pulls spatie/laravel-passkeys, which is where the actual WebAuthn ceremony, credential storage and challenge validation live. Treat that package as your dependency of record for security review and CVE watching; this repo is the Filament glue.
Publish and migrate.
php artisan vendor:publish # pick the plugin's config / views / lang tags
php artisan vendor:publish --provider="Spatie\LaravelPasskeys\LaravelPasskeysServiceProvider"
php artisan migrate
Confirm the Spatie migration table name against your existing schema before running migrate in an established app. It's easier to rename now than to untangle later.
Register the plugin on the panel.
use JeffersonGoncalves\FilamentMultifactorPasskeys\FilamentMultifactorPasskeysPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->plugin(FilamentMultifactorPasskeysPlugin::make());
}
The exact class namespace and fluent options aren't documented anywhere public — spend ten minutes reading src/ (the package is small enough to audit end-to-end in an afternoon) and note the real names before you build anything on top. That read is your best investment; eight stars and zero dependents means there is no blog-post trail to lean on.
Configure the Relying Party. Set the RP ID and origin to match your deployed host exactly. In production this means HTTPS with APP_URL matching the browser origin character-for-character. localhost is fine for dev; 127.0.0.1 may not be.
First use case — the two-day spike. In a disposable Filament 5 app: log in as a super-admin, enroll a passkey from the profile page, log out, and log back in with password + passkey assertion. Test on macOS/iOS (Touch ID / Face ID), Windows Hello, Android, and one hardware security key. If enrollment fails silently on any of those, you've found the thing you needed to know before committing.
Enrollment-first rollout. Never flip the panel to passkey-required in one deploy. Sequence it:
This ordering eliminates the entire lockout class of incident, which is the dominant operational risk with passkeys.
Profile management surface. Use Filament's built-in ->profile() page and mount the plugin's passkey list component there, or build a small custom page. Either way, resolve credentials strictly through auth()->user()->passkeys() — never accept a user or passkey ID from the request. The package gives you registration and deletion; the multi-passkey UX (naming credentials "MacBook Pro", "YubiKey 5C") is worth adding yourself so users can tell them apart at recovery time.
Per-panel registration. The plugin is registered per panel, not globally. If you run admin and app panels, decide deliberately whether both get it. A shared RP ID across admin.example.com and app.example.com works only if the RP ID is set to the registrable domain (example.com) — otherwise credentials enrolled in one panel won't assert in the other.
Role-based enforcement. Gate with middleware or canAccessPanel(), and redirect unenrolled privileged users to an enrollment gate rather than denying login outright:
// pseudo-flow
if ($user->hasRole('super_admin') && $user->passkeys()->doesntExist()) {
return redirect(PasskeyEnrollment::getUrl());
}
Keep the bypass path explicit and logged so you can find it in an incident review.
Fallback factor stays yours. This package does not ship TOTP, email OTP or SMS. If you need a fallback for users without a compatible authenticator (locked-down corporate VMs and older Windows machines are the usual culprits), wire that separately and make the factor choice visible in the UI.
Admin revocation and audit — build it. There is no advertised admin-side "revoke all passkeys for user X" action, and no audit trail. Build a small Filament resource or table action over the Spatie Passkey model, behind a policy, writing to your existing activity log. Your helpdesk runbook depends on this existing.
Testing strategy. Split it:
WebAuthn.addVirtualAuthenticator) in a browser test. Don't burn a day trying to sign an assertion with openssl in a unit test — you'd be testing your test harness, not the package.Localization. 1.1.0 added az, fa, hi, pt and uz. Publish the lang files if you need to override copy or add a locale — useful for the "why can't I enroll on this laptop" error strings, which are the ones users actually read.
Deployment hygiene. Drive RP ID/origin from env (APP_URL, session domain), then php artisan config:cache as part of deploy. A stale cached config after a domain change produces a failure that looks exactly like a browser bug.
RP ID / origin mismatch is the failure mode. WebAuthn fails in the browser with a SecurityError and writes nothing to storage/logs. When a user says "it just does nothing," open the browser console first — Chrome DevTools, Safari Web Inspector, chrome://webauthn on Chrome. Add this to your onboarding docs; it will save every new developer half a day.
localhost works, 127.0.0.1 may not, *.test needs care. Production must be HTTPS with an exact origin match. Subdomains complicate everything: admin.example.com enrolling a credential scoped to example.com is fine, but scoping to admin.example.com and then serving the panel from admin-staging.example.com is not.
Enabling required-MFA before anyone has enrolled locks out every admin. This is the one mistake that turns a security improvement into an outage. Ship the enrollment window first, and keep one break-glass super-admin account with a documented recovery path (password + TOTP, or a console command that clears the MFA requirement) that is tested, not just written down.
Pin exact versions and assume you own the fork. Bus factor 1, zero dependents, single maintainer. Enable Dependabot, pin the Filament 5 minor, and subscribe to security advisories for spatie/laravel-passkeys and its WebAuthn dependency — not for this repo. The 1.1.0 release is CI standardization, Dependabot bumps and translations: healthy hygiene, no evidence of deep maintenance depth. Budget 2–4 hours/month plus a real spike whenever Filament ships a major.
Asset and view caching bites after publishing. After publishing views, editing lang files or upgrading: php artisan view:clear and php artisan filament:assets. A surprising number of "my custom view isn't loading" reports are stale compiled Blade.
Config changes need a cache clear locally. php artisan config:clear after touching RP settings, or you'll spend twenty minutes debugging a change that never took effect.
Watch for collisions with existing auth customization. If you run OIDC/SSO, LDAP, Fortify with custom views, or you've overridden the Filament login page, verify the plugin's login challenge hook still fires. Filament's auth pipeline is ordered, and a competing login page override can quietly bypass the multi-factor step — which is worse than not having it, because you'll believe you're protected.
Rate limiting on the assertion endpoint is likely your responsibility. Whoever owns the route owns the throttle. Confirm this explicitly; it's the kind of gap that only surfaces in a pen test.
Platform authenticator availability is not universal. Windows Hello needs TPM/PIN configured; locked-down corporate builds, some VMs and remote-desktop sessions can't do it at all. Publish a browser/OS support matrix and offer a hardware security key as the documented alternative. "Buy a $25 key" is a much better answer than "we'll look into it."
Don't test in production with your only admin account. Use a pilot group of 10–20 users, instrument enrollment success rate and login failure rate, and only then roll out by role. Passkey bugs almost never throw — they manifest as silent enrollment failures and lockouts, which is precisely why the pilot matters.
How can I help you explore Laravel packages today?