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,
],| Mode | Guard | How it claims |
|---|---|---|
column | ColumnReplayGuard | The 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. |
cache | CacheReplayGuard | Claims 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 / null | NullReplayGuard | Every 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 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.