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

A session is a token family; its active row carries the device data. Row ids change on every rotation, so address a session by its family id — the stable session id, and a natural sid claim for your access token:

$sessions = RefreshTokens::sessions($user);

$sessions->all();                              // active rows, newest first
$sessions->find($familyId);                    // ?RefreshToken — active row of that family
$sessions->revoke($familyId);                  // bool — revokes every active row of it
$sessions->revokeAllExcept($currentFamilyId);  // "log out my other devices"; int
$sessions->revokeOthers($currentAccess->jti);  // same, keyed by access reference
$sessions->revokeAll();                        // revoke every session

// On the owner model via the trait:
$user->refreshTokens();  // MorphMany, all tokens
$user->sessions();       // MorphMany, active tokens only
MethodReturnsNotes
sessions($owner)->all()Collection<RefreshToken>Active rows, newest first.
sessions($owner)->find($familyId)?RefreshTokenThe active row of that family; null for a malformed id or another owner’s family.
sessions($owner)->revoke($familyId, $reason)boolRevokes every active row of the family (default Logout); false when nothing was active.
sessions($owner)->revokeAllExcept($keepFamilyId, $reason)intKeeps one session by family id (default LogoutAll); null or an unrecognisable id revokes everything.
sessions($owner)->revokeOthers($currentAccessReference, $reason)intKeeps the session holding that access reference (default LogoutAll); null revokes everything.
sessions($owner)->revokeAll($reason)intRevokes every session (default LogoutAll).
session($row)->enrich($device, $location)voidWrites host-supplied device and geo data onto one row (the model or its key).
session($row)->revoke($reason)boolRevokes one row you hold (default Manual).
  • The handle is owner-scoped: another owner’s family — including one of a same-id owner of another type — is unknown (find() returns null, revoke() false, nothing touched).
  • Ids are validated as UUIDs before any query — a malformed id returns null/false, never a database error.
  • revoke() revokes every active row of the family — a grace-window rotation can briefly leave two — denying each access reference once.
  • revokeAllExcept with null or an unrecognisable id revokes everything: failing closed is the safe reading of an unknown “current session”.
  • $row->sessionStartedAt() returns when the family began, inherited verbatim across rotation.

Verbs on the owner model

The HasRefreshTokens trait adds verbs that read as the owner acting on itself. Each one delegates to the manager — sugar over RefreshTokens::sessions($this) — so RefreshTokens::fake() records them too:

use RoundlyConsulting\RefreshTokens\DataTransferObjects\IssueContext;

$new = $user->issueRefreshToken(new IssueContext(accessReference: $access->jti));
$user->findSession($familyId);              // ?RefreshToken
$user->revokeSession($familyId);            // bool
$user->revokeAllSessions();                 // = RefreshTokens::sessions($user)->revokeAll(); returns count
$user->revokeOtherSessions($currentAccess->jti); // keep current, revoke the rest; returns count

A “your devices” endpoint

Build the list from the active rows; token_hash and access_reference are hidden from serialization, so returning whole rows is safe too:

return RefreshTokens::sessions($user)->all()->map(fn ($session) => [
    'id' => $session->family_id,               // stable session id
    'browser' => $session->browser,
    'os' => $session->os,
    'device' => $session->device_type,
    'city' => $session->city,
    'started_at' => $session->sessionStartedAt(),
    'current' => $session->family_id === $currentFamilyId,
]);

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.