jeffersongoncalves/laravel-session-management
Laravel Session Management lets you track and manage active user sessions stored in the database, so users can review logged-in devices and revoke or log out other sessions. Lightweight package for secure session control.
This package is a thin, opinionated layer over Laravel's database session driver: it gives you an Eloquent model for the sessions table, safe payload inspection, and a small service for kicking people out. Everything hinges on one prerequisite, so verify it before anything else.
SESSION_DRIVER=database in .env, and config/session.php has 'driver' => 'database'. If you're on file, redis, or cookie, there is nothing in the sessions table and this package has no data to work with. This is the single most common reason "it doesn't work."sessions migration in the default 0001_01_01_000000_create_users_table.php. Older apps or apps that never switched drivers need php artisan session:table && php artisan migrate. The package follows session.table and session.connection, so a custom table name or a dedicated sessions connection (common in multi-tenant setups) is picked up automatically.active(), expired(), guests(), authenticated(), forUser() map directly onto the two columns that matter — last_activity and user_id. Almost every feature you'll build is a composition of those plus decodePayload().First use case — an "active sessions" list on a user profile page. This is the fastest way to validate the install end to end, because it exercises the model, the user() relation, and the scopes together:
use Package\SessionManagement\Models\Session; // namespace per the package README
$sessions = Session::query()
->forUser($user)
->active()
->orderByDesc('last_activity')
->get();
foreach ($sessions as $session) {
$session->ip_address;
$session->user_agent;
$session->last_activity; // unix timestamp
$session->decodePayload(); // masked array
}
If that returns the row you're currently browsing with, you're wired up correctly. Then try SessionManagement::revoke($session) on a second browser and watch that browser get logged out on its next request.
"Log out other devices" for the current user. The canonical feature. Resolve the current session id from the request session and revoke the rest:
SessionManagement::revokeUser($user, except: $request->session()->getId());
Whether the except parameter is named that way or takes a session id in a different position, check the signature — but the pattern of "revoke all sessions for this user except mine" is the one you want. Wrap it with a password confirmation and, ideally, Auth::logoutOtherDevices($password) so the password_hash_* marker in remaining sessions is also rotated.
Admin-side session visibility. Session::authenticated() for logged-in rows, Session::guests() for anonymous traffic — the latter is surprisingly useful (bot traffic, abandoned carts, "who is hammering this endpoint" debugging). Paginate aggressively; this table gets large on busy apps. Eager-load with('user') when rendering a list, otherwise you'll N+1 across every row.
Incident response / forced logout. On password change, role change, or a suspected compromise, revokeUser($user) is the one-liner. Pair it with a password reset and remember-token rotation (see Gotchas). For a single suspicious session, revoke($session) or revoke($sessionId).
Payload inspection for support tickets. decodePayload() is the reason this package exists over hand-rolled unserialize(). You get the session's key/value array without instantiating any classes, and _token plus password_hash_* are masked by default — safe to render in an admin panel or paste into a ticket:
$session->decodePayload(); // ['_token' => '***', 'password_hash_web' => '***', ...]
Prefer the default. If you genuinely need the real CSRF token (e.g. debugging a token mismatch), the masking is a default rather than a hard rule — pass the flag to disable it, and never log the unmasked output.
Scheduled pruning instead of relying on the lottery. Laravel's own session GC is probabilistic (lottery: [2, 100]), so on low-traffic apps expired rows linger for a long time. Replace or supplement it with a deterministic schedule:
// routes/console.php or bootstrap/app.php
Schedule::command('session-management:prune')->hourly();
prune() only touches expired() rows (older than session.lifetime minutes). It will not log out an active user — that's revokeUser(). Keep that distinction clear in your head and in your code comments.
Testing. Set SESSION_DRIVER=database in phpunit.xml, use RefreshDatabase, and assert on row counts rather than payload contents. A factory-less test like Session::factory-style inserts are usually easiest done by hitting a real login route, which also proves the user() relation resolves against your actual provider.
remember_token ($user->forceFill(['remember_token' => Str::random(60)])->save()) or call Auth::logoutOtherDevices().user() is a plain belongsTo via your auth provider. It works for the standard users table with an integer/ULID key. Custom guards, a separate admin table, or polymorphic actors won't resolve — in that case query on user_id yourself and join manually.APP_KEY. With session.encrypt on, old rows become undecodable after a key rotation. Expect decodePayload() to fail or return partial data for pre-rotation sessions; don't treat that as a bug. Same story for session.serialization => 'json' — the package handles both, but rows written under a previous setting won't match the current one.last_activity is a raw unix timestamp. Don't ->diffForHumans() it directly expecting a Carbon instance; wrap with Carbon::createFromTimestamp(). And active()/expired() hinge on session.lifetime, so a config change shifts the boundary retroactively for every row.last_activity and user_id if you don't have them — forUser()->active() and the prune command both scan on those. On a million-row sessions table an unindexed prune is a table scan under a scheduled task.is_current_device, parsed UA), or wrap it in a repository. For anything the service doesn't cover (bulk revoke by IP, revoke by device fingerprint), drop down to the model rather than asking for new API.database, (2) session.connection points where you think it does, (3) the user's browser didn't have a remember cookie, (4) no AuthenticateSession middleware re-establishing things in an unexpected order.How can I help you explore Laravel packages today?