How do you enable passkeys with Laravel Fortify 1.40, and what does the application still have to provide itself?
answer
- added in Fortify 1.37
- Features::passkeys with confirmPassword
- PasskeyUser contract plus PasskeyAuthenticatable
- relying_party_id and allowed_origins
- @laravel/passkeys runs the browser ceremony
basics
~10 sAdd Features::passkeys() to config/fortify.php, make User implement Laravel\Fortify\Contracts\PasskeyUser and use PasskeyAuthenticatable, run the migrations, and set relying_party_id and allowed_origins. The app still provides the screens and the browser WebAuthn calls, usually through @laravel/passkeys.
solid answer
~40 sPasskeys (WebAuthn) arrived in Fortify 1.37 and are in the 1.40 stub as `Features::passkeys(['confirmPassword' => true])`. Fortify wraps the `laravel/passkeys` package: the `User` model implements `Laravel\Fortify\Contracts\PasskeyUser` and uses `Laravel\Fortify\PasskeyAuthenticatable`, `fortify:install` publishes the passkeys migration alongside Fortify's own, and the `passkeys` array in `config/fortify.php` sets `relying_party_id` (defaulting to the host of `app.url`), `allowed_origins` and `timeout`; values in a published `laravel/passkeys` config are overridden. Fortify registers the endpoints: `/passkeys/login/options` and `/passkeys/login` for guests, `/passkeys/confirm/options` and `/passkeys/confirm` to satisfy password confirmation, and `/user/passkeys` routes to register and delete, all throttled by the `passkeys` limiter. With `confirmPassword` on, registering and deleting need a recent password confirmation. The app supplies the UI and the browser calls, typically with the `@laravel/passkeys` JavaScript client.
code
php · 13 lines<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;
class User extends Authenticatable implements PasskeyUser
{
use Notifiable, PasskeyAuthenticatable;
}go deeper
Recall the setup pieces: the passkeys feature, the PasskeyUser contract with the PasskeyAuthenticatable trait, the migration, and a front end that calls Fortify's endpoints.
Explain the relying_party_id and allowed_origins settings, the login, confirm and manage endpoints, and what confirmPassword guards.
Anticipate environment problems from APP_URL, the passkeys limiter behind proxies, and the fallback paths users need when they change devices.
Decide whether passkeys replace, supplement or sit beside two-factor for the product, weighing support load against phishing resistance.
## What a passkey is, briefly A **passkey** is a WebAuthn credential: a key pair created by the user's device (Face ID, Touch ID, Windows Hello or a hardware key), with the private key kept on the device and the public key stored by the site. Signing in means the browser proves possession of the private key for **this site's domain**, so there is no password to phish or reuse. The cryptography and the browser API are outside Laravel; what matters here is how Fortify wires them in. ## Fortify's support and its version Fortify added passkeys in **1.37.0** and the Fortify 1.40 stub enables them. Fortify depends on `laravel/passkeys` and configures it, so you do not install or publish that package separately. The current starter kits also enable `Features::passkeys` in their own Fortify config. ## Enabling it 1. In `config/fortify.php`, keep or add `Features::passkeys(['confirmPassword' => true])` in `features`. 2. Make `App\Models\User` implement `Laravel\Fortify\Contracts\PasskeyUser` and use the `Laravel\Fortify\PasskeyAuthenticatable` trait. 3. Run the migrations; `fortify:install` publishes the `laravel/passkeys` migration alongside Fortify's. 4. Check the `passkeys` configuration array. 5. Build the screens and call the endpoints from the browser. ## The configuration keys | Key | Stub value | Why it matters | |---|---|---| | `relying_party_id` | host of `config('app.url')` | passkeys are bound to this domain; a wrong `APP_URL` breaks every ceremony | | `allowed_origins` | `[config('app.url')]` | browser origins allowed to complete registration and login | | `timeout` | `60000` (milliseconds) | how long a ceremony may stay open | | `user_handle_secret` | not in the stub; falls back to `app.key` | derives opaque user identifiers | The docs note that values in a published `laravel/passkeys` config file are overridden by Fortify's, so configure passkeys in one place. ## The endpoints Fortify registers - **Sign in (guest):** `GET /passkeys/login/options` returns the challenge; `POST /passkeys/login` submits the credential, optionally with `remember`. - **Confirm (signed in):** `GET /passkeys/confirm/options` and `POST /passkeys/confirm` satisfy Laravel's password-confirmation requirement using a passkey instead of the password. - **Manage (signed in):** `GET /user/passkeys/options` and `POST /user/passkeys` (with `name` and `credential`) register one; `DELETE /user/passkeys/{passkey}` removes one. All of these except deletion are throttled by the `passkeys` limiter, which the published provider defines as ten requests per minute keyed by the credential ID, or the session ID, plus the IP. When `confirmPassword` is on (the routes treat it as on unless set to `false`), the management routes also require `password.confirm`. ## What the application still provides - **The screens:** a "sign in with a passkey" button, a list of registered passkeys with names, and delete buttons. - **The browser ceremony:** calling `navigator.credentials.get()` or `create()` with the options Fortify returns and posting the result back. The official `@laravel/passkeys` npm package does this and has React, Vue and Svelte helpers. - **A correct `APP_URL`** in each environment, because the relying party ID is derived from it; a staging host that differs from production needs its own value. ## In a tax-filing app Passkeys suit a product used once or twice a year, where users forget passwords: returning filers sign in with the device they used last spring. Keeping two-factor and password reset available covers users who changed devices.
- Why do passkeys registered on staging fail to work on production for the same Fortify app?A passkey is bound to the relying party ID, which Fortify derives from the host in `APP_URL`. Staging and production have different hosts, so a credential created for one is not offered or accepted by the other. That is by design, not a bug.
- Can a passkey stand in for password confirmation in Fortify?Yes. Fortify registers `/passkeys/confirm/options` and `POST /passkeys/confirm`; a successful ceremony marks the current session as password-confirmed, so routes behind `password.confirm` accept it.
saying these in an interview costs you the question
- Passkeys need their own separate package config published and edited.
- Fortify renders the passkey prompt for you on the login page.
- The relying party ID can be any string, independent of APP_URL.
- Passkeys have been part of Fortify since its first release.
- Enabling passkeys removes password login from Fortify.