Skip to content

Add opt-in passkey sign in, manageable from the admin panel - #384

Merged
Cannonb4ll merged 1 commit into
ploi:mainfrom
sandervanhooft:claude/passkey-support-roadmap-fe0e4b
Oct 15, 2026
Merged

Cannonb4ll merged 1 commit into
ploi:mainfrom
sandervanhooft:claude/passkey-support-roadmap-fe0e4b

Conversation

@sandervanhooft

Copy link
Copy Markdown
Contributor

Summary

Adds passkey (WebAuthn) sign in as an opt-in feature. Like MCP and 2FA, it's off by default and an admin turns it on in the panel.

  • Admin setting: Settings → General → "Enable passkeys" (general.enable_passkeys, default false).
  • Profile → Security tab: users can add a passkey (name + password confirmation) and remove one (password confirmation). The browser runs the WebAuthn ceremony and hands the credential back to Livewire for verification.
  • Login page: a "Sign in with a passkey" button, shown only when the setting is on and the browser supports WebAuthn. Because the admin panel uses /login, this covers the panel too. A passkey sign in skips the 2FA code, since passkeys require user verification (biometric/PIN).
  • Admin → Users → Edit: a Passkeys relation manager lists a user's passkeys so an admin can revoke one (for example after a lost device). It stays visible after the feature is disabled while the user still has passkeys.

Implementation notes

  • Built on laravel/passkeys, which laravel/fortify 1.40 already pulls in, so there are no new dependencies.
  • Fortify stays headless (Fortify::ignoreRoutes()). Only the two guest sign in routes (passkeys/login/options, passkeys/login) are registered in routes/web.php, next to the two-factor challenge. EnsurePasskeysAreEnabled returns 404 while the setting is off, and the routes are throttled by a new passkeys limiter (10/min per IP).
  • laravel/passkeys only turns an unknown passkey into a validation error. A bad signature, wrong origin or challenge, or a cloned authenticator throws a raw WebauthnException, which would render a 500. Both the sign in controller and the profile registration catch these, log a warning, and return a normal error.
  • Registration options live in the session only after the password-confirmed addPasskey action and are pulled (single use) when the registration finishes, so finishing a registration always comes after a password confirmation.
  • Small dependency-free WebAuthn helper in resources/js/passkeys.js, bundled in app.js.
  • New migrations: passkeys table (the package's own schema) and the settings migration.
  • English translations only, the same as the 2FA and MCP features. Other locales fall back to English.

Known limitation

Adding or removing a passkey asks for the current password, the same as enabling or disabling 2FA. Users who signed up through SSO and never set a password can't manage passkeys yet.

Test plan

  • tests/Feature/Auth/PasskeyLoginTest.php: button shown/hidden by the setting, endpoints 404 when disabled, discoverable options with required user verification, verified passkey signs in (and bypasses 2FA), unknown passkey and WebAuthn failures return 422 and leave the visitor a guest
  • tests/Feature/Auth/PasskeyManagementTest.php: list, hidden when disabled, add requires correct password, creation options dispatched, finish stores the passkey, finish without a confirmed password rejected, WebAuthn failure not stored, remove own passkey (with password), can't remove someone else's
  • tests/Feature/Filament/PasskeyAdminTest.php: disabled by default, admin can enable it, admin can revoke a user's passkey, relation manager visibility
  • Full suite: 1136 passed (run with memory_limit=1G)
  • Manual check in a browser over HTTPS: register a passkey, sign in with it, revoke it from the admin panel. The relying party ID comes from APP_URL, so check it on a real domain.

Admins can enable passkeys under Settings > General. Once enabled, users
can register passkeys (Face ID, Touch ID, Windows Hello, security keys) on
the Security tab of their profile and sign in with one from /login, which
also covers the Filament admin panel through the shared web session.

Built on laravel/passkeys, which Fortify 1.40 already pulls in. Fortify
stays headless, so the two sign in routes are registered in routes/web.php
next to the two-factor challenge and are hidden (404) while the setting is
off.

- Profile: add (name + password confirmation) and remove passkeys; the
  browser runs the WebAuthn ceremony and hands the credential back to
  Livewire for verification
- Login page: "Sign in with a passkey" button, shown only when enabled and
  supported by the browser; passkey sign in skips the 2FA code
- Admin: passkeys relation manager on the user edit page so admins can
  revoke a lost device; it stays visible after disabling while the user
  still has passkeys
- WebAuthn verification failures become validation errors instead of 500s
  and are logged
- Adds the passkeys table, the general.enable_passkeys setting and a
  passkeys rate limiter
@Cannonb4ll
Cannonb4ll merged commit e2d87f9 into ploi:main Oct 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants