Two-factor & passkeys
Setting up factors during login is covered in Login challenges. This page covers a signed-in account managing its own factors. The ceremonies belong to two-factor-for-laravel and passkeys-for-laravel; this package adds guard modes, re-authentication, invalidation, events and notifications.
Two-factor (TOTP + recovery codes)
Available when the guard’s two_factor.mode is not off:
| Step | HTTP | Response |
|---|---|---|
| Status | GET two-factor | enabled, pending, recovery_codes_remaining, mode |
| Start | POST two-factor | secret, provisioning_uri, qr_svg, recovery_codes, issuer |
| Confirm | POST two-factor/confirm | status enabled + tokens |
| Regenerate recovery codes | POST two-factor/recovery-codes | recovery_codes + tokens |
| Disable (mode optional only) | DELETE two-factor | status disabled + tokens |
From PHP, the twoFactor() sub-context runs the same steps — pass the caller’s $current token to keep its device and get the re-issued pair:
use RoundlyConsulting\Auth\Facades\Authentication;
$twoFactor = Authentication::guard('users')->twoFactor();
$status = $twoFactor->status($user); // enabled, pending, recoveryCodesRemaining, mode
$setup = $twoFactor->start($user); // show $setup->qrSvg / ->secret, store ->recoveryCodes
$tokens = $twoFactor->confirm($user, $code, $current, $context);
$codes = $twoFactor->regenerateRecoveryCodes($user, $current, $context); // codes, tokens
$tokens = $twoFactor->disable($user, $current, $context);- Starting while already enabled answers two_factor_already_enabled (409); qr_svg is null when two_factor.qr.enabled is off or rendering failed.
- A wrong or stale confirmation code answers invalid_code (422); disabling under required answers two_factor_required (409).
- Confirm, regenerate and disable apply invalidation.two_factor_changed (default others): every other session ends and the caller gets a fresh pair — swap immediately.
- Events TwoFactorEnabled, TwoFactorDisabled and RecoveryCodesRegenerated; using a recovery code at login fires RecoveryCodeUsed and mails the remaining count.
Passkeys
Available when the guard’s passkeys.mode is not off:
| Step | HTTP | Response |
|---|---|---|
| List | GET passkeys | Passkeys, newest first |
| Options | POST passkeys/options | ceremonyId + publicKey |
| Register | POST passkeys | 201 passkey + tokens |
| Rename | PATCH passkeys/{passkey} | The passkey |
| Remove | DELETE passkeys/{passkey} | status removed + tokens |
From PHP, through the passkeys() sub-context — rename() and remove() take a Passkey or its id, and another account’s passkey is PasskeyNotFound:
use RoundlyConsulting\Auth\Facades\Authentication;
$passkeys = Authentication::guard('users')->passkeys();
$options = $passkeys->registrationOptions($user); // ceremonyId + publicKey
$added = $passkeys->register($user, $response, 'MacBook', $current, $context); // passkey, tokens
$passkeys->all($user); // newest first
$passkeys->rename($user, $passkeyId, 'Work laptop'); // a Passkey or its id
$tokens = $passkeys->remove($user, $passkeyId, $current, $context);- A failed attestation answers passkey_registration_failed (422); names are sanitised.
- Removing the last passkey is refused with last_credential (409) when the account would have no other way in, or the guard requires passkeys.
- Register and remove apply invalidation.passkey_changed (default none, so tokens is null) and fire PasskeyAdded / PasskeyRemoved.
How the modes combine at login
| Setting | Effect |
|---|---|
two_factor.mode = required | Accounts without TOTP must enrol during login (or satisfy it with a passkey when passkey_satisfies_required). |
passkeys.mode = required | Accounts without a passkey must enrol one during login. |
passkeys.second_factor | Whether a passkey may (allowed) or must (required_when_enrolled, required) be the second step after a password or email login. |
passkeys.satisfies_mfa | A user-verified passkey login counts as multi-factor on its own — also under a risk step-up; with false a step-up demands TOTP. |
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.