Search by

kirchdev / laravel-device-sessions

TitusKirch

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

Statistics

Installs: 163

Dependents: 1

Suggesters: 1

Stars: 0

Open Issues: 2

v0.6.1 2026-09-01 16:20 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.

Latest Version on Packagist Total Downloads Tests PHP Version Laravel Version License: MIT

$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 migrate finds 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 migrate would try to create tables that already exist. Reconcile your migrations table 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-eloquent driver binds each remember token to a device row + cookie instead of the single remember_token column (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 IpMasker contract.
  • πŸ”Œ 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 DeviceTouched event 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 (mirrors Auth::logoutOtherDevices()); toggle via device-sessions.events.
  • Fortify two-factor β†’ a listener queues the device cookie at the 2FA challenge (where the Login event hasn't fired yet), auto-wired via class_exists so Fortify is never required. Using another 2FA flow? Write your own bridge against the DeviceResolver contract.

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

πŸ“„ License

MIT Β© Titus Kirch / IT-Dienstleistungen Titus Kirch