solution-forest/filament-loginguard
Filament plugin that adds login guard and session management to your panel: track active devices, spot suspicious sign-ins, and let users review or revoke other sessions from a simple account page.
Install and publish the package assets:
composer require solution-forest/filament-loginguard
php artisan vendor:publish --tag="filament-loginguard-config"
php artisan vendor:publish --tag="filament-loginguard-migrations"
php artisan migrate
Register the plugin on the panel(s) where users should manage their devices:
public function panel(Panel $panel): Panel
{
return $panel
->plugin(LoginGuardPlugin::make());
}
Then expose the "My Devices" page — either via the plugin's navigation registration or by adding it to the user menu, depending on how your panel is organized.
Where to look first:
config/filament-loginguard.php) — it controls the device/session model, page registration, and which panels get the UI.trusted_at (and the device↔session mapping table). Understanding this schema is what makes the rest of the package predictable.First use case: a user loses a laptop, logs in from a new browser, opens "My Devices", sees the old session, and clicks "This isn't me". After v0.7.1 that click now actually deletes the underlying Laravel session row, so the stolen browser is logged out on its next request instead of merely disappearing from the list.
Treat the revocation service as the public API. Don't delete device rows or session rows yourself. If you need revocation from an admin panel, an artisan command, or a support tool, resolve the service and call it — you get the transaction, the trusted_at reset, and the 403 guard for free. Hand-rolled Model::delete() calls will reintroduce exactly the bug v0.7.1 fixed.
Handle the current-session 403 explicitly. The service now refuses to revoke the device mapped to the current session. In your UI, disable or hide the revoke action for the row flagged as the current device; in any non-Filament caller, catch the authorization exception and surface it as a validation-style message rather than a 500.
Pair "This isn't me" with a password/2FA re-confirmation. The flow is a security-sensitive action triggered by someone who may already be authenticated as the victim. Gate the action behind ->requiresConfirmation() plus a password challenge (->form() with current_password rule or your 2FA challenge), so a hijacked session can't silently nuke the legitimate user's devices.
Listen for revocation to notify the user. Fire-and-forget notifications on revocation are the highest-value integration: an email or database notification ("A device was signed out from /") turns the feature from a self-service button into an intrusion-detection signal. Hook the package's event (or a model observer on the mapping table) and dispatch a queued notification. Keep the listener queued — the revocation itself should stay synchronous and inside the transaction.
Customize the device list without forking the page. Extend the package's page class and override the table/query, or publish the views if the package supports it. Typical customizations: resolving device names from a user agent parser, masking IPs for GDPR, adding a "last seen" column, hiding devices older than N days.
Multi-panel and multi-guard setups. Register the plugin per panel and confirm the config's model/guard settings match the panel's guard. A common mistake is enabling the page on an admin panel where the User model differs from the one the device rows reference — you'll get an empty or foreign-scoped list.
Test it with real session rows. In feature tests, use the database session driver and create multiple sessions for the same user ($this->actingAs($user) in separate test requests, or insert sessions rows directly), then assert that after revocation the row count drops and a request with the revoked session id is unauthenticated. Asserting only that the device row disappeared is what let the original gap ship.
database session driver. On cookie, file, redis, or array, revocation cannot reach into the driver and the compromised session may survive. If you're on a non-database driver, either switch, or accept that "This isn't me" is a UI-level signal only — and document that for your team.composer update. But verify your sessions table has an index on user_id (Laravel's session:table stub includes it) — the new deletion path queries by user, and a missing index turns revocation into a table scan on busy apps.sessions table. Let the service own the boundary.trusted_at being cleared is a side effect with a UX cost. Users who marked a device "trusted" (skipping 2FA) will be challenged again after a revocation. That's intended, but expect support tickets; surface it in the confirmation copy.sessions for the user in the DB before and after the revoke click. If the device row vanishes but the session row survives, you're either on a non-database driver, calling an old code path, or the mapping row pointed at a session id that no longer matches the live session (which can happen if the mapping is written once at login and Laravel regenerated the id).composer.json and read release notes before bumping. This release is a security fix on a self-service action; older versions silently leave live sessions behind, so treat any pre-0.7.1 install as a known vulnerability rather than a cosmetic bug.How can I help you explore Laravel packages today?