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

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

MethodOffered when
passwordThe account has a password — and, with require_second_factor_when_enrolled, no TOTP or passkey.
totp, recovery_code2FA mode is not off and TOTP is enabled on the account.
passkeyPasskeys mode is not off and the account has a passkey.
email_otpPasswordless 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

SensitiveActionEndpoint gated
enable_two_factorPOST two-factor
disable_two_factorDELETE two-factor
regenerate_recovery_codesPOST two-factor/recovery-codes
register_passkeyPOST passkeys/options, POST passkeys
remove_passkeyDELETE passkeys/{passkey}
change_emailPOST email/change — only while email_change.require_reauthentication is true (ensureFor() follows the same switch)
logout_everywherePOST logout/everywhere
set_passwordPUT 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 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.