Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament User Sessions Laravel Package

smony/filament-user-sessions

Filament plugin that lists every active user session—user, IP, parsed device and browser, last activity—with one-click and bulk revoke to force-logout anyone. Flags logins from new devices. Requires Laravel's database session driver.

View on GitHub
Deep Wiki
Context7

Getting Started

Pre-flight check — this package only works under one session driver. Before installing, confirm config/session.php resolves to SESSION_DRIVER=database. The package reads Laravel's sessions table directly; file, cookie, redis, array, and memcached drivers all result in a plugin that installs cleanly and then shows an empty table with no error.

composer require smony/filament-user-sessions

# Only if you don't already have a sessions migration
php artisan session:table

php artisan migrate

The migrate step matters twice: it creates/updates the sessions table and runs the package migration for session_known_devices. If you skip it, the Sessions page still renders but the "New device" flagging silently never works.

Register the plugin in the panel provider you actually administer from:

use Smony\FilamentUserSessions\FilamentUserSessionsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(FilamentUserSessionsPlugin::make());
}

Version pinning is not optional. v2.x supports Filament ^4.0|^5.0; Filament v3 users must install smony/filament-user-sessions:^1.0. Installing v2 on a v3 app fails at the Filament\Resources\Resource type level (the BackedEnum/UnitEnum union on $navigationIcon/$navigationGroup fixed in v2.0.1).

First use case, in order:

  1. Log into the panel — your own session should appear in the Sessions list. This proves the driver and table wiring in one glance.
  2. Open a second browser in a private window, log in as a test user, refresh the Sessions page. Confirm the row appears with parsed device/browser and IP.
  3. From your admin session, hit Revoke on the test session. The test browser should be logged out on its next request. This exercises the whole loop before you rely on it.
  4. Note that your own current session has no revoke action — that's by design (and enforced for bulk too since v2.0.2).

Where to look first:

  • config/filament-user-sessions.php — publish it immediately (php artisan vendor:publish --tag=filament-user-sessions-config), even if you change nothing. Reading the real file beats guessing option names from the README.
  • Smony\FilamentUserSessions\Models\Session — this is the model your policy targets and the anchor for everything authorization-related.
  • Smony\FilamentUserSessions\Policies\SessionPolicy — read the default before overriding it; it tells you exactly which abilities the page calls.
  • Smony\FilamentUserSessions\Contracts\ResolvesNewDeviceRecipients — the only real integration seam if you want alerting.
  • FilamentUserSessionsPlugin — check for chained configuration methods before reaching for config file edits.

Implementation Patterns

Lock down authorization before you show this to anyone

The default policy is fully permissive: any user who can reach the panel can see every session in the app and revoke any of them. In a support-role or multi-tenant panel this is a privilege-escalation surface — a junior admin can kick out a superadmin mid-incident.

Override it in a dedicated service provider (app providers boot after package providers, so your registration wins):

use Illuminate\Support\Facades\Gate;
use Smony\FilamentUserSessions\Models\Session;

public function boot(): void
{
    Gate::policy(Session::class, \App\Policies\SessionPolicy::class);
}
class SessionPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->hasAnyRole(['superadmin', 'security']);
    }

    public function view(User $user, Session $session): bool
    {
        return $user->isSuperAdmin() || $user->tenant_id === $session->user?->tenant_id;
    }

    public function delete(User $user, Session $session): bool
    {
        return $user->isSuperAdmin() || $session->user_id === $user->id;
    }

    public function deleteAny(User $user): bool
    {
        return $user->isSuperAdmin();
    }
}

Two things people get wrong here: (a) implementing delete but not deleteAny and then wondering why bulk revoke bypasses their restriction, and (b) registering the policy inside the panel provider instead of a booted service provider, which can lose the race against the package's own registration.

Incident response runbook

The realistic day-to-day use is a compromise or an offboarding, not idle browsing:

  1. Search the Sessions list for the affected user.
  2. Triage the New flag first — a flagged session means that browser/platform combo was first seen for that user in the last new_device_window_hours. That's your starting suspect, not your conclusion (see gotchas).
  3. Note the IP and parsed device before revoking — the row disappears when you revoke.
  4. Bulk-select and revoke every session for that user. Their current browser gets logged out within one request cycle.
  5. Follow up outside the package: rotate the password, revoke API tokens, and check whether the session was created before or after a legitimate "I just got a new laptop" message from the user.

A useful habit: keep a lightweight audit trail yourself. The package revokes by deleting session rows, so there is no built-in "who revoked whom" record. If you need that for compliance, wrap it in your own Filament action or listen for the deletion on the Session model.

Wiring new-device notifications

This is entirely opt-in and a complete no-op until you implement the contract — a fresh install sends nothing.

use Illuminate\Contracts\Auth\Authenticatable;
use Smony\FilamentUserSessions\Contracts\ResolvesNewDeviceRecipients;

class SecurityTeamRecipients implements ResolvesNewDeviceRecipients
{
    public function resolve(Authenticatable $user): iterable
    {
        return User::where('is_superadmin', true)->get();
    }
}
// config/filament-user-sessions.php
'new_device_notification_recipients' => \App\Support\SecurityTeamRecipients::class,
'new_device_notification_channels'   => ['mail', 'database'],
'notify_on_new_device'               => true,
'new_device_window_hours'            => 24,

Note the README's own example has a mismatch: the class is defined as SecurityTeamRecipients but the config points at \App\Notifications\SecurityTeamRecipients::class. Decide on one namespace and be consistent — a wrong FQCN here fails silently rather than loudly.

Practical framing: this is a per-user, per-fingerprint, first-sighting-only alert. It fires when a user's session appears with a browser/OS combination never recorded for them, and never again for that combination. That means it's excellent for "Alice logged in from a Windows machine for the first time ever" and useless for "attacker logged in from Chrome on Windows when Alice already uses Chrome on Windows."

database channel requires php artisan notifications:table and recipient models using Notifiable, otherwise the notification is created and discarded (or throws, depending on your queue setup).

Coexisting with other auth surfaces

The plugin is a window onto the sessions table — nothing more. In an app with multiple guards:

  • Sessions belonging to guards that populate sessions.user_id show up with a user attached.
  • Sessions where user_id is null (guest/remember-me-only, some custom guards) render as orphan rows you can still revoke but can't attribute.
  • Sanctum / Passport API tokens are not sessions. They never appear here and revoking a session does not revoke a token. If your app is token-heavy, pair this with Sanctum's token management and document the split, or your incident runbook will have a hole.

Custom table names

If you renamed the sessions table, change it in both places — Laravel's session.table and the package's own config key — plus make sure your migration created it with the standard columns (id, user_id, ip_address, user_agent, payload, last_activity). A mismatch produces an empty, error-free list.

Local and CI

For feature tests that touch this page, set SESSION_DRIVER=database in that test's environment and seed a row manually or via an actual login. Do not rely on the default phpunit.xml — see below.


Gotchas and Tips

phpunit.xml will lie to you. Laravel's default test config sets SESSION_DRIVER=array. Every test that visits the Sessions page will find zero sessions and your "revoke logs the user out" test will pass vacuously or fail confusingly. Override it explicitly in the test that cares.

Stale rows accumulate. Revoking deletes rows, but expired sessions are only removed by Laravel's probabilistic session garbage collection, which depends on the GC lottery config and your session lifetime. On a busy or long-lifetime app the table grows and the Sessions page fills with rows for browsers that closed weeks ago. The "Online now" widget uses its own configurable threshold — keep that threshold at or below your real session lifetime, or it will count dead sessions as online. Use bulk revoke as periodic hygiene, and consider a scheduled prune of your own if the table is large.

The user_id column. Laravel's default sessions migration includes user_id, but if you're on a hand-rolled or very old migration you may not have it. The plugin then shows sessions with no user, and per-user revocation becomes guesswork. Verify the column exists before assuming the package is broken.

Your own session is unreachable. The single revoke action is disabled for the current session, and since v2.0.2 bulk revoke excludes it too. If you're testing "log myself out," that's not the mechanism. Also note: revoking a row kills the session, but a user with a valid "remember me" cookie may be transparently re-authenticated on their next visit — force-logout is not a substitute for a password reset.

The "New" flag is a sniff test, not a security control. The README is explicit and it's worth repeating to whoever reads your dashboards: the fingerprint is browser + platform only, with no IP component (deliberately, to avoid VPN/mobile false positives). Two different physical machines running Chrome on Windows look identical — same fingerprint, so the second one is not flagged. Conversely, a user legitimately trying Edge once gets flagged. Treat the flag as "worth a second look," never as evidence.

Wiped known devices make everything new. session_known_devices persists across logout, but if it's truncated (a migrate:fresh in staging, a bad rollback, a cleanup script) every user's current device looks brand new and your security team gets a mail storm. If you ever truncate it, disable notify_on_new_device first.

new_device_window_hours is a lookback, not a lifetime. A session is flagged if its fingerprint was first seen within the window. After 24 hours a genuinely suspicious session quietly stops being flagged — so an alert that arrives by email is the durable signal, and the table flag is only useful if you look promptly.

Config changes need php artisan config:clear. Publishing the config then editing new_device_window_hours and seeing no effect is almost always config caching, not a bug.

Don't re-run php artisan session:table on an existing app. It scaffolds a new migration; if you already have one, you'll hit a duplicate-table error on the next migrate.

Bulk revoke across tenants is one click. Because the default policy is permissive and the sessions table is global, a multi-tenant panel exposes cross-tenant sessions in one list. Scope viewAny and view by tenant before shipping — this is the single highest-risk default in the package.

Upgrading from v1 → v2 requires a Filament major upgrade. There's no v2 release for Filament v3. Plan that as part of the Filament upgrade, not as a standalone composer bump. Also read the v2.3.0 note: it adds session_known_devices, so existing installs need php artisan migrate — behaviour is otherwise unchanged until you opt into the notification config.

Extension points, ranked by usefulness: (1) your own SessionPolicy — do this on day one; (2) ResolvesNewDeviceRecipients — do this if you have a security contact; (3) config publishing for window/threshold tuning; (4) deleting Session models yourself from a custom Filament action if you need audit logging around revocation.

Privacy. The page renders IP addresses and user agents — personal data in most jurisdictions. If your panel is accessed by contractors or has broad read access, that's a data-protection decision, not just an authorization one.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin