moontechs/filament-webauthn
moontechs/filament-webauthn brings WebAuthn passkey authentication to Filament panels. Users register and sign in with fingerprint, face recognition, or a hardware security key instead of passwords, adding phishing-resistant, passwordless login to your admin panel with minimal setup.
Install the package and its migration, then wire it into a Filament panel:
composer require moontechs/filament-webauthn
php artisan vendor:publish --tag="filament-webauthn-migrations"
php artisan migrate
Register the panel plugin in your PanelProvider (typically app/Providers/Filament/AdminPanelProvider.php):
use Moontechs\FilamentWebauthn\WebauthnPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(WebauthnPlugin::make())
// ...
}
Add the package's trait to your authenticatable model so it can own credentials:
use Moontechs\FilamentWebauthn\Concerns\HasWebauthnCredentials;
class User extends Authenticatable
{
use HasWebauthnCredentials;
}
Where to look first:
config/filament-webauthn.php — this is where relying-party (rp_id, rp_name, allowed origins) values live. Set these before testing.First use case: log in as a user with a password, navigate to wherever the package registers the "add passkey" action (profile/security page or the plugin's default page), register the device, log out, and sign back in with the passkey. If that loop works, the plumbing (HTTPS, RP ID, origin, JS component) is correct.
Passwordless alongside passwords. The common day-one setup is to keep the existing Filament login form and let the plugin append a passkey button. Users enroll a passkey once while authenticated, then can skip the password on subsequent logins. Do not remove password login until you've handled account recovery.
Enrollment as a profile action. Expose passkey registration as a Filament action on a "Security" page or the user resource. Treat it like password change UX:
Action::make('addPasskey')
->label('Add a passkey')
->modalContent(fn () => view('filament-webauthn::register'))
->modalSubmitAction(false);
The actual ceremony runs in the browser via the package's front-end component; the server-side handler validates the attestation and persists the credential against auth()->user().
Multiple credentials per user. Users will have a laptop, phone, and a hardware key. List stored credentials (name, created_at, last_used_at if tracked) and offer delete/rename. Enforce a policy: require at least one other auth method before allowing the last passkey to be deleted.
Requiring WebAuthn as a second factor. Because the plugin authenticates a user directly, the usual pattern is to gate sensitive panels or routes behind "has at least one verified credential" rather than trying to layer it on Filament's 2FA. Check $user->webauthnCredentials()->exists() in a middleware or panel ->authGuard()/access callback.
Reusing the server-side services outside Filament. The package is a thin Filament layer over WebAuthn server-side logic. For a JSON API or a non-Filament guard, call the underlying registration/assertion services directly and skip the Livewire components — but keep the same rp_id/origin config so credentials stay valid across entry points.
Testing. WebAuthn cannot be exercised with plain HTTP feature tests. Use virtual authenticators (Chrome DevTools protocol / @simplewebauthn test helpers) or stub the verification service in PHPUnit. At minimum, cover the persistence path (credential stored, linked to user, rejected when signature invalid).
localhost. WebAuthn will silently fail on http:// staging hosts. This is the #1 "it doesn't work" cause.staging.example.com to app.example.com invalidates every existing credential. Set rp_id to the registrable domain (e.g. example.com) and include all valid origins in config before enrollment.php artisan config:cache during deploy will freeze RP values; if you change config and see stale behavior, clear it. Same for view caches if you publish and override the JS/components.users table or a non-User model, update the migration's foreign key and the trait's relation target. Running the migration before your users table exists will fail.WebauthnException) with the challenge and credential ID; inspect the browser console for NotAllowedError/SecurityError which usually map to origin/RP mismatch or user cancellation.none attestation. If you want to verify hardware (e.g. only allow YubiKeys), you must configure attestation conveyance and trust anchors — this is a meaningful lift, not a config toggle.How can I help you explore Laravel packages today?