---
title: "Revocation & logout — Refresh Tokens for Laravel | Roundly"
description: "Log out one token or every session, record why with RevocationReason, and revoke everything on a password change."
url: https://roundly-consulting.com/open-source/docs/refresh-tokens-for-laravel/revocation
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)

# Revocation & logout

```php
use RoundlyConsulting\RefreshTokens\Enums\RevocationReason;

RefreshTokens::revoke($plainFromClient);            // logout with token in hand; bool, idempotent
RefreshTokens::sessions($user)->revokeAll();         // global logout — returns count revoked
RefreshTokens::sessions($user)->revokeAll(RevocationReason::CredentialsChanged);
RefreshTokens::session($row)->revoke();              // one row you hold (reason defaults to Manual)
```

RefreshTokens::revoke() takes the plaintext (reason defaults to Logout). It returns true when it ended a live session and false for an unknown token or one whose session had already ended — so calling it twice is safe. A token that expired unused counts as ended: it is left as it is, with no SessionRevoked and no AccessTokenRevoker call. To revoke a row you already hold, use session($row)->revoke() (reason defaults to Manual):

```php
RefreshTokens::session($row)->revoke();                           // the model or its key — reason Manual
RefreshTokens::session($row)->revoke(RevocationReason::Security);  // every verb takes an explicit reason
RefreshTokens::revoke($plainFromClient, RevocationReason::Security);
```

## Logging out with an already-rotated token

A spent token still ends its session. It no longer holds it — the session lives on in the row it was rotated into — so revoking the spent row alone would let whoever rotated it keep refreshing. Instead:

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

// The client logs out with the token it held before the rotation:
RefreshTokens::revoke($a->plainText);   // true — outside rotation.grace the whole family is revoked
                                        // as ReuseDetected and RefreshTokenReuseDetected fires
```

- Outside rotation.grace the spent token is a theft signal, handled exactly like a replay at redeem(): the whole family is revoked as ReuseDetected, every live access reference is denied, RefreshTokenReuseDetected fires and revoke() returns true. The reason you passed is not used — the rows record why they really died.
- Inside rotation.grace (your client’s own refresh racing its logout) the family’s live rows are revoked with your reason, SessionRevoked fires for each, and no reuse event fires.
- A session caught mid-rotation is sealed either way, so the in-flight replacement is refused.

Every revoke persists revoked\_reason, calls the bound AccessTokenRevoker once per live access reference, and dispatches SessionRevoked.

## Revocation reasons

Every revoke verb takes an optional RevocationReason, persisted in revoked\_reason and carried on SessionRevoked:

| Case | Value | When |
| --- | --- | --- |
| `Rotated` | `rotated` | Superseded by a rotation. |
| `Logout` | `logout` | Explicit single-session logout. |
| `LogoutAll` | `logout_all` | Global logout / revoke-all. |
| `ReuseDetected` | `reuse_detected` | Family revoke on a theft signal. |
| `Expired` | `expired` | Swept past expiry. |
| `Manual` | `manual` | Admin / host revocation. |
| `CredentialsChanged` | `credentials_changed` | Password / 2FA / passkey change. |
| `AccountDisabled` | `account_disabled` | Owner disabled or deleted. |
| `SessionLimit` | `session_limit` | Evicted by a per-owner session cap. |
| `Security` | `security` | Any other security response. |

## Log out everywhere on a password change

The canonical reaction to a credential change is to revoke every session. Wire it from your change-password flow or from a model observer — the package ships the verb but never hooks your User model for you:

```php
// In a change-password action:
$user->update(['password' => Hash::make($newPassword)]);
$user->revokeAllSessions(RevocationReason::CredentialsChanged);

// …or via a saved observer:
User::saved(function (User $user): void {
    if ($user->wasChanged('password')) {
        $user->revokeAllSessions(RevocationReason::CredentialsChanged);
    }
});
```

## Logout can’t be outrun by a refresh

A session revoked mid-rotation — its token already redeemed, the replacement not yet issued — has no active row for that instant. The session verbs (sessions()->revokeAll(), revokeOthers(), revokeAllExcept(), revoke()) seal such a session too: they relabel its redeemed row with the reason, deny its access reference and fire SessionRevoked. The in-flight replacement is then refused — rotate() returns null, an explicit issue() throws InvalidTokenFamilyException or comes back already revoked.

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