NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Auth for Laravel

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

ClaimSource
iss, iat, nbf, exp, jti, scopejwt
audThe guard’s JWT audience — the isolation boundary.
subAccount key.
tvAccount::tokenVersion().
email, email_verified, permissions, extraThe guard’s ResolvesAccessTokenClaims (default: email per tokens.include_email, no permissions).
sidThe refresh-token family id = the session id.
amrRFC 8176 methods accumulated through the login (omitted when empty).
auth_timeWhen the first factor succeeded (kept across refreshes).
grdGuard name (diagnostics).

Refresh and sessions

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. Reusing a rotated token kills the family, fires RefreshTokenReuseReported and mails the owner a suspicious-session alert.

OperationEffectEvent
logoutDenies the current jti until it expires and revokes the session.LoggedOut(Current)
logoutSessionRevokes one session; unknown or foreign ids are 404.LoggedOut(Session)
logoutOthersRevokes every other session without bumping tv (that would log the caller out); each revoked session’s one live access token is denied.LoggedOut(Others)
logoutEverywheretv++ and every session revoked.LoggedOut(Everywhere) + AccountTokensInvalidated
sessions.max_activeOldest 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.

TriggerReasonDefault scope
Change passwordpassword_changedothers
Reset passwordpassword_resetall
Email change confirmedemail_changedothers
2FA enabled / disabled / recovery codes regeneratedtwo_factor_changedothers
Passkey added / removedpasskey_changednone
Account disabledaccount_disabledall (fixed)
Logout everywhere / incidentlogout / securityall (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

'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:

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

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