NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Auth for Laravel

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:

StepHTTPResponse
StatusGET two-factorenabled, pending, recovery_codes_remaining, mode
StartPOST two-factorsecret, provisioning_uri, qr_svg, recovery_codes, issuer
ConfirmPOST two-factor/confirmstatus enabled + tokens
Regenerate recovery codesPOST two-factor/recovery-codesrecovery_codes + tokens
Disable (mode optional only)DELETE two-factorstatus 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:

StepHTTPResponse
ListGET passkeysPasskeys, newest first
OptionsPOST passkeys/optionsceremonyId + publicKey
RegisterPOST passkeys201 passkey + tokens
RenamePATCH passkeys/{passkey}The passkey
RemoveDELETE 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

SettingEffect
two_factor.mode = requiredAccounts without TOTP must enrol during login (or satisfy it with a passkey when passkey_satisfies_required).
passkeys.mode = requiredAccounts without a passkey must enrol one during login.
passkeys.second_factorWhether a passkey may (allowed) or must (required_when_enrolled, required) be the second step after a password or email login.
passkeys.satisfies_mfaA 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 crypto

By 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.