> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sinemacula.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Refresh Token Rotation and Replay Detection

> Exchange a refresh token for a new access and refresh token pair using the JWT guard's refresh() method with atomic rotation and replay detection.

Refresh tokens in this package are single-use. Every successful exchange burns the presented token, issues a replacement pair, and atomically updates the rotation digest stored on the device row. The server owns this state, which means replay attacks and concurrent rotation attempts can be detected and acted on immediately — something that self-verifying access tokens alone cannot provide.

<Warning>
  The old refresh token is immediately invalidated the moment rotation succeeds. Clients must replace their stored refresh token with the new value returned in the response. Presenting the previous token again will be treated as a replay attack, the device will be revoked, and a `RefreshFailed` event will be dispatched.
</Warning>

## How refresh works

Call `refresh()` on the guard instance — not on the `Auth` facade's JWT service — and pass the raw refresh token string from the client. The method returns a `RefreshResult` on success, or `null` on any failure.

```php theme={null}
$guard   = auth()->guard('api');
$rotated = $guard->refresh($refreshToken);
```

On success, `RefreshResult` contains two public string properties:

| Property        | Description                                           |
| --------------- | ----------------------------------------------------- |
| `$accessToken`  | Newly issued access JWT for the authenticated context |
| `$refreshToken` | Newly issued refresh JWT — the old one is now burned  |

On failure, `refresh()` returns `null`. A `RefreshFailed` event has already been dispatched by the time `null` is returned — you do not need to dispatch it yourself.

## The full exchange pattern

```php theme={null}
use SineMacula\Laravel\Authentication\Facades\Auth;

$guard   = auth()->guard('api');
$rotated = $guard->refresh($refreshToken);

if ($rotated === null) {
    // RefreshFailed event already dispatched with a machine-readable reason
    abort(401);
}

return [
    'access_token'  => $rotated->accessToken,
    'refresh_token' => $rotated->refreshToken,
];
```

After a successful exchange, the guard also binds the resolved identity, principal, and device — the same event sequence as a full login fires, followed by a `Refreshed` event so your listeners can distinguish a rotation from an initial authentication.

## Failure handling and the RefreshFailed event

When `refresh()` returns `null`, a `RefreshFailed` event has been dispatched carrying a `RefreshFailureReason` backed enum. Listen to this event in your SIEM pipeline or for rate-limiting logic:

```php theme={null}
use SineMacula\Laravel\Authentication\Events\RefreshFailed;

Event::listen(RefreshFailed::class, function (RefreshFailed $event) {
    Log::warning('Refresh failed', [
        'reason' => $event->reason->value,
        'guard'  => $event->guard,
    ]);
});
```

The full set of reason codes is documented on the [events reference](/events) page.

## Security properties

**Atomic per-device rotation.** The rotation digest update and the new token pair are produced in a single operation scoped to the device row. Two concurrent exchange attempts on the same device cannot both succeed.

**Replay detection.** If the package detects that a rotation id has already been consumed — which happens when a client presents a token that was valid in a previous rotation cycle — it treats this as evidence of a compromised or leaked token. The device is immediately revoked and a `RefreshFailed` event is dispatched with reason `rotation_reuse`. Any future refresh attempts for that device will fail with `device_revoked`.

**Constant-time digest verification.** The stored rotation digest is compared against the presented token using `hash_equals()` to prevent timing side-channel attacks.

**`pid` hint validation.** When the refresh token carries a `pid` claim, the guard resolves the principal and confirms it matches the hint before completing the exchange. A mismatch produces `principal_mismatch` and the exchange fails closed.

## Revoking devices

Revoking a device row prevents all future refresh exchanges for that device. Access tokens already issued to that device remain valid until their `exp` claim is reached — the guard does not consult access-token `jti` values on the bearer path. For immediate access revocation, shorten your access-token TTL or implement `CanBeActive` on your identity model so the guard rejects inactive identities on every bearer request.

```php theme={null}
// Revoke a specific device — blocks refresh, does not invalidate in-flight access tokens
$device->update(['revoked_at' => now()]);
```
