skip to content

How do you enable passkeys with Laravel Fortify 1.40, and what does the application still have to provide itself?

level: middleimportance: nice to knowfreq 15%

answer

  1. added in Fortify 1.37
  2. Features::passkeys with confirmPassword
  3. PasskeyUser contract plus PasskeyAuthenticatable
  4. relying_party_id and allowed_origins
  5. @laravel/passkeys runs the browser ceremony

basics

~10 s

Add 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 s

Passkeys (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
<?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

for a junior

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.

for a middle

Explain the relying_party_id and allowed_origins settings, the login, confirm and manage endpoints, and what confirmPassword guards.

for a senior

Anticipate environment problems from APP_URL, the passkeys limiter behind proxies, and the fallback paths users need when they change devices.

for a principal

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.