Re-authentication
A signed-in account re-proves itself before sensitive actions. Afterwards its session counts as recently authenticated for reauthentication.timeout seconds (default 900). The marker lives in cache, keyed by guard and session, and records how the session re-proved itself — not just when.
'reauthentication' => [
'timeout' => 900,
'methods' => ['password', 'totp', 'recovery_code', 'passkey', 'email_otp'],
'require_second_factor_when_enrolled' => true, // 2FA/passkey accounts must reauth with totp|recovery_code|passkey
'fresh_login_counts' => true, // auth_time within timeout satisfies the check
'required_for' => ['enable_two_factor', 'disable_two_factor', 'regenerate_recovery_codes',
'register_passkey', 'remove_passkey', 'change_email', 'set_password', 'logout_everywhere'],
],Available methods
| Method | Offered when |
|---|---|
password | The account has a password — and, with require_second_factor_when_enrolled, no TOTP or passkey. |
totp, recovery_code | 2FA mode is not off and TOTP is enabled on the account. |
passkey | Passkeys mode is not off and the account has a passkey. |
email_otp | Passwordless accounts with no TOTP or passkey, and an address. |
A stolen session plus a leaked password therefore cannot switch off the factor that protects the account.
Re-authenticating
From PHP, the reauthentication() sub-context lists an account’s methods, confirms a proof and gates your own code — ensureRecent() and ensureFor() throw ReauthenticationRequired. sendCode() mails a code to a passwordless account and passkeyOptions() starts a passkey ceremony:
use RoundlyConsulting\Auth\DataTransferObjects\ReauthenticationData;
use RoundlyConsulting\Auth\Enums\ReauthenticationMethod;
use RoundlyConsulting\Auth\Enums\SensitiveAction;
use RoundlyConsulting\Auth\Facades\Authentication;
$guard = Authentication::guard('users');
$reauth = $guard->reauthentication();
$current = $guard->tokenFrom($request);
$reauth->methods($user); // what this account may re-authenticate with
$until = $reauth->confirm($user, new ReauthenticationData(
method: ReauthenticationMethod::Totp,
current: $current,
context: $guard->contextFrom($request),
code: $request->input('code'),
));
// Gate your own sensitive code:
$reauth->ensureRecent($user, $current, seconds: 300);
$reauth->ensureFor(SensitiveAction::ChangeEmail, $user, $current);Re-authentication is throttled per session; a locked account answers account_locked (423). Over HTTP: POST reauthenticate with method plus password, code or credential answers confirmed with confirmed_until; POST reauthenticate/passkey/options starts a passkey ceremony; POST reauthenticate/otp mails a code to passwordless accounts.
reauthentication.methods is checked against the factor that actually matched: a recovery code sent as totp while recovery_code is not allowed is refused with factor_not_allowed (422), counted like a wrong proof and leaves no marker — the code is spent, so RecoveryCodeUsed still fires. A re-authentication never makes its device known to new-device detection: it needs only a (possibly stolen) access token.
Protecting your routes
Route::delete('/account', DeleteAccount::class)
->middleware(['auth:users', 'authentication.guard:users', 'authentication.reauthenticated:300']);The check passes when a fresh login (auth_time within the window, with fresh_login_counts) or a recent re-authentication meets the account’s requirement at check time. An account that has TOTP or a passkey now needs a second-factor proof: a totp, recovery_code or passkey re-authentication, or a login whose amr has mfa or hwk. Otherwise it answers 403 reauthentication_required with the available methods.
Sensitive actions
| SensitiveAction | Endpoint gated |
|---|---|
enable_two_factor | POST two-factor |
disable_two_factor | DELETE two-factor |
regenerate_recovery_codes | POST two-factor/recovery-codes |
register_passkey | POST passkeys/options, POST passkeys |
remove_passkey | DELETE passkeys/{passkey} |
change_email | POST email/change — only while email_change.require_reauthentication is true (ensureFor() follows the same switch) |
logout_everywhere | POST logout/everywhere |
set_password | PUT password for an account without a password |
Remove an entry to drop its gate — per guard or for every guard. It is a list, so a guard’s list replaces the default one wholesale:
// config/authentication.php — this guard lets a passwordless account set a first password
// without re-authenticating; everything else stays gated.
'guards' => [
'users' => [
'reauthentication' => [
'required_for' => ['enable_two_factor', 'disable_two_factor', 'regenerate_recovery_codes',
'register_passkey', 'remove_passkey', 'change_email', 'logout_everywhere'],
],
],
],Limits
- The window is time-based: any proof counts for timeout seconds on the session that made it. Keep it short where that matters.
- With require_second_factor_when_enrolled = false, any recorded proof satisfies the gate, whatever factors the account has.
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.