The Passkeys facade
The Passkeys facade is the single entry point. Everything scoped to one account goes through Passkeys::for($user), which returns a UserPasskeys handle; the discoverable (usernameless) login stays flat because there is no account yet. Each ceremony is two calls — generate options for the browser, then verify the browser’s response:
use RoundlyConsulting\Passkeys\Facades\Passkeys;
// Everything scoped to one account goes through its handle.
$keys = Passkeys::for($user); // UserPasskeys
$keys->registrationOptions($overrides); // CreationOptionsData ($overrides optional)
$keys->register($response, name: 'Laptop'); // Passkey
$keys->authenticationOptions($overrides); // RequestOptionsData bound to this account
$keys->authenticate($response); // Passkey — only this account's credential
$keys->all(); // Collection<Passkey>, newest first
$keys->find($id); // ?Passkey — null for another account's id
$keys->count(); // int
$keys->exists(); // bool
$keys->rename($passkeyOrId, 'Work laptop'); // Passkey — refuses another account's passkey
$keys->revoke($passkeyOrId); // void — refuses another account's passkey
// No account yet: the discoverable (usernameless) login stays flat.
Passkeys::authenticationOptions($overrides); // RequestOptionsData ($overrides optional)
Passkeys::authenticate($response, $expect); // Passkey — $expect optionally holds it to an owner (type)
Passkeys::attestationFormats(); // ['none', 'packed', 'apple']Every method
| Method | Returns | What it does |
|---|---|---|
for($user) | UserPasskeys | The account handle — every call on it is scoped to $user. |
for($user)->registrationOptions(?$overrides) | CreationOptionsData | Creation options for navigator.credentials.create(); stores a single-use challenge. |
for($user)->register($response, ?$name) | Passkey | Verify the attestation response and store the credential, optionally named. |
for($user)->authenticationOptions(?$overrides) | RequestOptionsData | Request options bound to this account’s credentials. |
for($user)->authenticate($response) | Passkey | Verify an assertion held to this account. |
for($user)->all() | Collection<int, Passkey> | The account’s active passkeys, newest first. |
for($user)->find($id) | ?Passkey | One active passkey by int or string id — null for a revoked one, another account’s id or a non-numeric string. |
for($user)->count() / exists() | int / bool | How many active passkeys, and whether there is at least one. |
for($user)->rename($passkeyOrId, $name) | Passkey | Rename one of this account’s passkeys; fires PasskeyRenamed. |
for($user)->revoke($passkeyOrId) | void | Soft-delete one of this account’s passkeys; fires PasskeyRevoked. |
authenticationOptions(?$overrides) | RequestOptionsData | Usernameless request options — any account’s passkey can answer. |
authenticate($response, ?$expect) | Passkey | Verify a usernameless assertion, optionally held to an owner or owner type. |
attestationFormats() | list<string> | The attestation formats this relying party can verify. |
fake() | PasskeysFake | Swap in the recording, no-crypto test double (see Testing). |
Scoping is a security boundary
- for($user)->authenticate() accepts only this account’s credential — it holds the result to AuthenticationExpectation::owner($user).
- find(), rename() and revoke() take a Passkey or its id — an int, or the string a route parameter arrives as — and rename() and revoke() refuse a passkey of any other account, or an already revoked one, with the uniform CredentialNotFound. A route parameter can be passed straight through; a string that is not a canonical positive integer is simply not found.
- find() returns null for another account’s id instead of leaking that it exists.
Every call resolves its action from the container, so a host override of an action applies to the facade, the injected service and the model verbs alike. The facade is the recommended default; the next section shows the two equivalent entry points without it.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.