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
- spatie/laravel-package-tools: ^1.93.1
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/fortify: ^1.0
- laravel/pint: ^1.29
- orchestra/testbench: ^11.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-laravel: ^5.0
Suggests
- laravel/fortify: Auto-wires the two-factor challenge device-cookie bridge when present.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 12:03:14 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.
The publish step is required, once: the package does not auto-load its migrations, so nothing schema-related runs until the two files sit in your own database/migrations. Your DDL stays reviewable, in your repository, and on your deploy pipeline's terms.
Upgrading: migrations became publish-only
Earlier versions registered the package's migration path themselves, so php artisan migrate picked up create_user_devices_table and create_user_device_remember_tokens_table straight out of vendor/. That auto-load is gone β publishing is now the only way they reach you:
php artisan vendor:publish --tag=device-sessions-migrations
Published copies are stamped with the moment you publish them, so they slot into your own migration timeline rather than carrying the package's dates into your repository.
- Already published? Nothing to do. Publishing reuses the filenames you already have, so nothing is duplicated and
php artisan migratefinds nothing new. - Never published? This is a breaking change and needs a hand. Your database already has the two tables, but recorded them under the package's own filenames β the published copies carry generated ones, which Laravel reads as unrun. The next
migratewould try to create tables that already exist. Reconcile yourmigrationstable with the published filenames before that run.
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.