---
title: "Reuse detection — Refresh Tokens for Laravel | Roundly"
description: "Replaying a rotated token revokes the whole family, denies its access tokens and fires RefreshTokenReuseDetected — always on."
url: https://roundly-consulting.com/open-source/docs/refresh-tokens-for-laravel/reuse-detection
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Refresh Tokens for Laravel](https://roundly-consulting.com/open-source/docs/refresh-tokens-for-laravel.md)

# Reuse detection

Presenting an already-rotated token — to redeem() or rotate(), or to revoke() (see Revocation & logout) — is a theft signal. The entire token family is revoked, the bound AccessTokenRevoker is called for every live member’s access token, and RefreshTokenReuseDetected fires — all automatically, with no config switch:

```php
$a = RefreshTokens::issue($user, new IssueContext(accessReference: 'acc-a'));
$rotation = RefreshTokens::rotate($a->plainText, new RotationContext(accessReference: 'acc-b'));

// The already-rotated token A is presented again — a theft signal:
RefreshTokens::redeem($a->plainText); // null — and the whole family is revoked

$rotation->newRefreshToken->token->fresh()->revoked_reason; // RevocationReason::ReuseDetected
// AccessTokenRevoker::revoke('acc-b') was called; RefreshTokenReuseDetected fired once
```

## Race-hardened

- The reuse verdict is written on the family before the revoke scans it, and the scan repeats until a pass revokes nothing.
- A replacement issued into a family around the moment it is killed revokes itself, so a freshly rotated token can never survive the theft response, in any race ordering.
- If the replay lands while the family is mid-rotation — its newest row already claimed, the replacement not yet inserted — that session is closed too: the claimed row is relabelled ReuseDetected, its access token is denied and SessionRevoked fires. The in-flight replacement is refused (rotate() returns null; an explicit issue() throws InvalidTokenFamilyException).
- With the default strict rotation.grace = 0 this includes the loser of two concurrent redemptions of the same token.
- The response is scoped to the presented token’s owner, so even a family id that spans two owners never lets one owner’s replay log the other out.

## Grace window

If your frontend legitimately re-presents a token within a short single-flight window (two tabs refreshing at once, a retried request), give it a few seconds of grace. Inside the window a re-presented token returns null without revoking anything (a logout with it still ends the session — see Revocation & logout):

```bash
REFRESH_TOKENS_ROTATION_GRACE=30
```

## Alerting

RefreshTokenReuseDetected fires whenever the response ends something — a live member revoked or a session caught mid-rotation sealed, both counted in revokedCount (≥ 1) — so whether a theft raises an alert never depends on timing. A replay into a family that is already over fires nothing, so it can’t spam your alerting with empty events:

```php
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\RefreshTokens\Events\RefreshTokenReuseDetected;

Event::listen(function (RefreshTokenReuseDetected $event): void {
    // $event->familyId, $event->ownerType, $event->ownerId, $event->revokedCount
    // alert your security channel, notify the account owner, …
});
```

Request throttling stays host-owned — the package does not rate-limit token presentation. Wrap your refresh endpoint in Laravel’s throttle middleware to blunt replay floods.

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/refresh-tokens-for-laravel.md)
