---
title: "Tokens & sessions — Auth for Laravel | Roundly"
description: "RS256 access tokens and their claims, rotating refresh tokens, device sessions, logouts and the invalidation policy."
url: https://roundly-consulting.com/open-source/docs/auth-for-laravel/tokens-sessions
language: en
---

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

[Auth for Laravel](https://roundly-consulting.com/open-source/docs/auth-for-laravel.md)

# Tokens & sessions

Access tokens are jwt-for-laravel RS256 user tokens; refresh tokens are rotating refresh-tokens-for-laravel families, one family per device session.

## Access token claims

| Claim | Source |
| --- | --- |
| `iss, iat, nbf, exp, jti, scope` | jwt |
| `aud` | The guard’s JWT audience — the isolation boundary. |
| `sub` | Account key. |
| `tv` | Account::tokenVersion(). |
| `email, email_verified, permissions, extra` | The guard’s ResolvesAccessTokenClaims (default: email per tokens.include\_email, no permissions). |
| `sid` | The refresh-token family id = the session id. |
| `amr` | RFC 8176 methods accumulated through the login (omitted when empty). |
| `auth_time` | When the first factor succeeded (kept across refreshes). |
| `grd` | Guard name (diagnostics). |

## Refresh and sessions

```php
use RoundlyConsulting\Auth\Facades\Authentication;

$guard   = Authentication::guard('users');
$context = $guard->contextFrom($request);
$current = $guard->tokenFrom($request);                    // jti, sid, auth_time, amr

$pair = $guard->refresh($refreshToken, $context);          // rotates within the same session; the previous access token stops working

$guard->sessions($user, $current->sessionId);              // Collection<SessionData>, current flagged
$guard->logout($user, $current);                           // this device
$guard->logoutSession($user, $sessionId);                  // one device
$revoked = $guard->logoutOthers($user, $current);          // every other device
$revoked = $guard->logoutEverywhere($user);                // tv++ and every session
```

Refresh is throttled per IP. A successful refresh retires the session’s previous access token, so a session holds exactly one live access token and revoking the session kills every token it minted — a copied older one included. A token of another guard is unknown and not consumed; a disabled owner has its family revoked. Each session records the token version it was issued under: refreshing one the account has moved past — a login that raced a password reset, a disable or a logout everywhere — revokes it and answers refresh\_invalid (sessions issued by 1.1.0 or earlier carry no version and are not checked). Reusing a rotated token kills the family, fires RefreshTokenReuseReported and mails the owner a suspicious-session alert.

| Operation | Effect | Event |
| --- | --- | --- |
| logout | Denies the current jti until it expires and revokes the session. | LoggedOut(Current) |
| logoutSession | Revokes one session; unknown or foreign ids are 404. | LoggedOut(Session) |
| logoutOthers | Revokes every other session without bumping tv (that would log the caller out); each revoked session’s one live access token is denied. | LoggedOut(Others) |
| logoutEverywhere | tv++ and every session revoked. | LoggedOut(Everywhere) + AccountTokensInvalidated |
| sessions.max\_active | Oldest sessions revoked above the cap. | LoggedOut(Session) |

## Invalidation

Each credential change applies the guard’s scope for its reason: none does nothing; others bumps tv, revokes every session and re-issues a fresh pair for the device that made the change; all bumps tv and revokes every session. Any scope other than none also kills the account’s active challenges and login one-time tokens.

| Trigger | Reason | Default scope |
| --- | --- | --- |
| Change password | `password_changed` | others |
| Reset password | `password_reset` | all |
| Email change confirmed | `email_changed` | others |
| 2FA enabled / disabled / recovery codes regenerated | `two_factor_changed` | others |
| Passkey added / removed | `passkey_changed` | none |
| Account disabled | `account_disabled` | all (fixed) |
| Logout everywhere / incident | `logout / security` | all (fixed) |

Credential-change responses include tokens (or null). When non-null the client must swap to it immediately — its previous access token died with the change.

## Configuration

```php
'tokens' => [
    'access_ttl' => null,                // seconds; null → jwt.ttl
    'refresh_ttl' => 2_592_000,          // sliding, 30 days
    'refresh_absolute_ttl' => 7_776_000, // 90 days; 0 = no cap
    'claims_resolver' => DefaultClaimsResolver::class,
    'include_email' => true,
],

'sessions' => [
    'max_active' => null,                // int|null — oldest sessions revoked above the cap
],

'invalidation' => [                      // none|others|all  (account_disabled is always all)
    'password_changed' => 'others',
    'password_reset' => 'all',
    'email_changed' => 'others',
    'two_factor_changed' => 'others',
    'passkey_changed' => 'none',
],
```

The jti denylist lives in cache: a flush revives revoked access tokens until they expire. Keep access\_ttl short (15 minutes or less); the token version covers invalidations.

## Custom claims

Bind your own ResolvesAccessTokenClaims to put permissions or tenant data into the access token:

```php
use RoundlyConsulting\Auth\Contracts\Account;
use RoundlyConsulting\Auth\Contracts\ResolvesAccessTokenClaims;
use RoundlyConsulting\Auth\DataTransferObjects\AccessTokenClaims;
use RoundlyConsulting\Auth\Guards\GuardConfig;

// config: 'guards' => ['users' => ['tokens' => ['claims_resolver' => PermissionsClaims::class]]]

final class PermissionsClaims implements ResolvesAccessTokenClaims
{
    public function resolve(Account $account, GuardConfig $guard): AccessTokenClaims
    {
        return new AccessTokenClaims(
            email: $account->accountEmail(),
            emailVerified: $account->hasVerifiedEmail(),
            permissions: $account->permissions()->pluck('name')->all(),
            extra: ['tenant' => $account->tenant_id],
        );
    }
}
```

## 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/auth-for-laravel.md)
