Build1 publisher2 min readPublished
Laravel's passkeys packages handle WebAuthn on the server and in the browser
Laravel's two first-party passkeys packages, laravel/passkeys and @laravel/passkeys, run WebAuthn registration and login, according to a dev.to tutorial. The tutorial's own config leaves password reset switched on, so adopting the stack adds passkeys next to passwords.
The Engineer · Build desk

What happened
- Laravel has released laravel/passkeys for the server and @laravel/passkeys for the browser, according to a dev.to tutorial that calls them the official passkeys stack.
- Server setup is a Composer install, a published migration and a migrate run that creates a passkeys table holding the public credentials tied to each user.
- The User model must implement a PasskeyUser contract and use a PasskeyAuthenticatable trait.
- Apps on Fortify or Jetstream turn passkeys on by adding Features::passkeys() to the features array in config/fortify.php.
- The npm package exposes registerPasskey and authenticatePasskey helpers that run the browser half of the WebAuthn registration and login ceremonies.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost Teams drop their own ceremony code but keep maintaining the request layer around it: routes, CSRF headers and error handling spread across four fetch calls.
- exposure A stolen copy of the passkeys table yields public keys only, and none of them can sign a login challenge for a passkey account.
- contradiction The tutorial's enclave description and its sync description fit different credentials, so a threat model that assumes keys never leave one device is wrong for synced passkeys.
In the dev.to tutorial's registration code, the browser fetches options from /passkeys/register-options, passes them to registerPasskey, and posts the resulting credential to /passkeys/register [7]. Login follows the same shape. A POST to /passkeys/login-options returns a challenge, authenticatePasskey runs the device prompt, and the assertion goes to /passkeys/login [8]. Underneath, the device creates a key pair at registration and sends only the public key to the app [9]. At login it signs the server's challenge after biometric verification, and the server checks the signature against the stored public key [9]. According to the tutorial, the server never stores or receives private keys, biometrics or passwords [10].
The server footprint is a passkeys table plus a contract and a trait on the User model [2][3]. It asks little of an existing app, and I count that in its favour.
The browser side leaves plumbing with the team. The two flows make four hand-written fetch calls [12]. Each POST reads the CSRF token from a meta tag and sets the X-CSRF-TOKEN header itself [7][8]. The tutorial does not say whether laravel/passkeys registers those /passkeys routes or leaves them for the app to define. Its success handler calls alert(), a line best left in the tutorial [7].
Login is email-first. The code posts the typed address to /passkeys/login-options and fetches a challenge for that account before any device prompt appears [8]. The login form keeps its email field [8].
Passwords stay switched on. The Fortify example lists Features::registration() and Features::resetPasswords() beside Features::passkeys() [5]. The same tutorial opens by naming phishing and credential stuffing as primary attack vectors [15]. An account configured this way keeps a password path for those attacks to target [16]. Phasing passwords out is a second project on top of this setup.
One passage needs correcting before it reaches a threat model. The tutorial says the private key "remains securely stored on the device's hardware enclave" [18]. It later says operating systems sync passkeys through Apple iCloud Keychain or Google Password Manager [11]. A synced passkey sits on more than one device [13]. The two descriptions fit different authenticators, and the tutorial lists physical security keys alongside biometric logins such as TouchID and FaceID [14].
For a Fortify app, I'd adopt this stack over a hand-maintained ceremony layer on this evidence. The case gets weaker if the four /passkeys endpoints turn out to be the app's to write [12].
What to watch
- Laravel's own laravel/passkeys documentation, to settle whether the package registers the four /passkeys routes or the app must define them.
- A Fortify option that disables password registration and reset for accounts that hold a passkey.
- Whether @laravel/passkeys supports a login that starts without an email address.