kirchdev / laravel-device-sessions
Device-bound login sessions for Laravel: per-device remember-me tokens, a "where am I signed in" device list, and revoke/rename β privacy-respecting and Fortify-agnostic.
Package info
github.com/kirchDev/laravel-device-sessions
pkg:composer/kirchdev/laravel-device-sessions
Requires
- php: ^8.4
- illuminate/auth: ^13.0
- illuminate/cache: ^13.0
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/cookie: ^13.0
- illuminate/database: ^13.0
- illuminate/events: ^13.0
- illuminate/http: ^13.0
- illuminate/support: ^13.0
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/fortify: ^1.0
- laravel/pint: ^1.29
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.7
- pestphp/pest-plugin-laravel: ^4.1
Suggests
- laravel/fortify: Auto-wires the two-factor challenge device-cookie bridge when present.
This package is auto-updated.
Last update: 2026-05-30 21:37:19 UTC
README
π± laravel-device-sessions
Device-bound login sessions for Laravel β per-device "remember me" tokens, a "where am I signed in" list, and revoke/rename. Privacy-respecting and Fortify-agnostic.
$user->devices; // every browser signed in, with masked IP, OS and last-seen β GitHub-style
That's it. Concurrent device-bound remember-me tokens, a "where am I signed in" list, and revoke/rename β without touching your login controllers.
π¦ Install & run
composer require kirchdev/laravel-device-sessions php artisan vendor:publish --tag=device-sessions-migrations php artisan migrate
Important
Publish the config first (--tag=device-sessions-config) and set device-sessions.keys.* + table_names.* before migrating β the migrations read config at run time, and keys.user_key_type must match your users-table primary key.
Add the HasDeviceSessions trait to your authenticatable model and point its auth provider at the device-aware driver:
use KirchDev\DeviceSessions\Concerns\HasDeviceSessions; class User extends Authenticatable { use HasDeviceSessions; }
// config/auth.php 'providers' => [ 'users' => [ 'driver' => 'device-aware-eloquent', // was 'eloquent' 'model' => App\Models\User::class, ], ],
Then alias the tracking middleware and attach it to your authenticated routes β that's the whole wiring; remember-me logins are now device-bound and the device list populates automatically:
// bootstrap/app.php use KirchDev\DeviceSessions\Http\Middleware\TrackAuthenticatedUserDevice; ->withMiddleware(fn (Middleware $middleware) => $middleware->alias([ 'track.device' => TrackAuthenticatedUserDevice::class, ]))
Route::middleware(['auth', 'track.device'])->group(function () { // ...authenticated routes });
β¨ Features
- π Device-bound remember-me β a custom
device-aware-eloquentdriver binds each remember token to a device row + cookie instead of the singleremember_tokencolumn (one active token per device, rotated on login). - π "Where am I signed in" β list active devices (OS, friendly name, masked IP, last-seen), revoke one, revoke all others, or rename β all as plain actions.
- π΅οΈ Privacy-respecting β IP masking on by default (IPv4 β /24, IPv6 β /48), swappable via the
IpMaskercontract. - π Fortify-agnostic β works under any login mechanism; the two-factor cookie bridge auto-wires only when Fortify is present.
- π§© Overridable everything β name parsing, OS detection, cookie policy, IP masking and token hashing are contracts with sensible defaults.
- π§° Config-driven schema β models, table names and key types (
id/uuid/ulid) all overridable. - π‘ Event-driven β a
DeviceTouchedevent lets you react without the package assuming your schema. - π§ͺ Library-grade β Pest 4 + Testbench, no host app needed.
π Managing devices
The package ships no routes β every operation is a plain action you call from your own controllers, so the response shape stays yours:
use KirchDev\DeviceSessions\Actions\{ListUserDevices, RevokeUserDevice, RevokeOtherUserDevices, UpdateUserDeviceName}; $devices = app(ListUserDevices::class)->execute($user); // active devices, last-seen first app(RevokeUserDevice::class)->execute($user, $deviceId); // revoke one (+ its tokens) app(RevokeOtherUserDevices::class)->execute($user, $currentId); // keep only the current device app(UpdateUserDeviceName::class)->execute($user, $deviceId, 'Work Laptop');
The middleware exposes the active device as the current_device_id request attribute (also $user->currentDevice()), so you can flag "this device" in the list.
π§© Overridable contracts
Every host-facing behaviour is a contract bound to a Default* β rebind any of them in a service provider:
$this->app->bind( \KirchDev\DeviceSessions\Contracts\IpMasker::class, \App\Support\MyStrictIpMasker::class, );
All contracts and their defaults
| Contract | Default | Controls |
|---|---|---|
DeviceResolver |
ResolveOrCreateUserDevice⦠|
cookie β bootstrap-cache β create flow |
DeviceNameResolver |
DefaultDeviceNameResolver |
User-Agent β "Chrome on Windows" |
OsFamilyDetector |
DefaultOsFamilyDetector |
User-Agent β DeviceOsFamily |
DeviceCookieFactory |
DeviceCookieBuilder |
device cookie name / TTL / SameSite |
IpMasker |
DefaultIpMasker |
IP minimisation (IPv4 /24, IPv6 /48) |
RememberTokenHasher |
Sha256RememberTokenHasher |
at-rest token hashing |
π‘ Events & Fortify
TouchDeviceLastSeen fires DeviceTouched on a real (throttled) touch β listen instead of patching the package, e.g. to stamp your own user column:
Event::listen(fn (DeviceTouched $event) => $event->user->forceFill(['last_seen_at' => now()])->save());
Two opt-in integrations:
OtherDeviceLogoutβ revokes all other devices (mirrorsAuth::logoutOtherDevices()); toggle viadevice-sessions.events.- Fortify two-factor β a listener queues the device cookie at the 2FA challenge (where the
Loginevent hasn't fired yet), auto-wired viaclass_existsso Fortify is never required. Using another 2FA flow? Write your own bridge against theDeviceResolvercontract.
π§Ή Pruning revoked devices
Revoked devices are kept (audit/undo window), then pruned. The command ships unscheduled β wire it into your scheduler with Schedule::command('device-sessions:prune')->dailyAt('03:10'):
php artisan device-sessions:prune # retention from device-sessions.prune.retention_days (180)
php artisan device-sessions:prune --days=90
βοΈ Configuration
config/device-sessions.php is parameterised with inline docs β e.g. rename the cookie or switch key types:
'cookie' => ['name' => 'device', 'same_site' => 'lax'], 'keys' => ['primary_key_type' => 'id', 'user_key_type' => 'id'],
All configuration keys
| Key | What it controls |
|---|---|
models.* |
Swap the user / device / remember_token Eloquent models. |
table_names.* |
Override defaults if they collide with existing tables. |
keys.* |
id / uuid / ulid for device PKs and the user FK. Set before migrating. |
cookie.* |
Device cookie name (default device), lifetime, SameSite, secure. |
cache.* |
Cache store, key prefix, loginβ2FA bootstrap TTL, last-seen throttle. |
remember.lifetime |
Minutes until a remember token expires (null = never). |
events.* |
Toggle the core event listeners. |
prune.retention_days |
Retention window for the prune command (default 180). |
π§ͺ Testing
composer install composer test # Pest 4 composer pint # Laravel Pint (test mode) composer larastan # Larastan / PHPStan
The test suite runs via Testbench + in-memory SQLite β no host app required.
π€ Contributing
PRs welcome. Conventional Commits required (enforced via commitlint). Husky runs Pint + Larastan + oxlint + oxfmt on git commit, so you can mostly forget about style.
Tip
Run pnpm check:fix (Node tooling) and composer pint:fix (PHP) before pushing β CI will catch what husky missed.
π£οΈ Versioning
Semantic Versioning. Release notes in CHANGELOG.md β managed by release-please.