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

Replay protection

Once a TOTP code is accepted, its timestep is claimed in a single check-and-set. Any code with a timestep at or before the last successful one is rejected and TwoFactorReplayDetected fires with that timestep — so two concurrent submissions of the same code cannot both succeed. The code used to confirm enrolment is claimed too.

Choosing a guard

// config/two-factor.php
'replay_guard' => 'cache',   // 'column' | 'cache' | 'none' (null → none)
'cache' => [
    'store' => env('TWO_FACTOR_CACHE_STORE'),
    'ttl' => 60 * 60 * 24,
],
ModeGuardHow it claims
columnColumnReplayGuardThe default — also what a blank replay_guard resolves to. One conditional UPDATE on the last-used-timestep column; zero affected rows means a replay. Persists with the account row.
cacheCacheReplayGuardClaims under the store’s atomic lock (Redis, Memcached, database, file and array all provide one), with a best-effort path for lock-less stores.
none / nullNullReplayGuardEvery claim succeeds — replay protection is off.

If you choose the cache guard on a multi-node host, back it with an atomic store such as Redis or the database.

Cache guard caveat

A cache flush or eviction (cache:clear, a deploy, LRU pressure) drops the last-used timestep and momentarily reopens the replay window. Where that risk is unacceptable, use the default column guard, which persists to the account table.

Custom guard

Bind your own implementation of the ReplayGuard contract to store timesteps elsewhere:

// The contract — RoundlyConsulting\TwoFactor\Contracts\ReplayGuard
interface ReplayGuard
{
    // The latest recorded timestep (null when none yet).
    public function latestTimestep(TwoFactorAuthenticatable&Model $user): ?int;

    // Atomic check-and-set: true when newly claimed, false on a replay.
    public function claim(TwoFactorAuthenticatable&Model $user, int $timestep): bool;
}

// In a service provider's register():
$this->app->singleton(ReplayGuard::class, fn () => new MyReplayGuard);

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.