Search by

solution-forest / filament-loginguard

solutionforest

Enterprise-grade login security for Filament: brute-force lockouts with escalation, self-service unlock, and active session management.

Package info

github.com/solutionforest/filament-loginguard

pkg:composer/solution-forest/filament-loginguard

Fund package maintenance!

SolutionForest

Statistics

Installs: 513

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 1

v0.7.1 2026-10-06 10:44 UTC

This package is auto-updated.

Last update: 2026-10-06 10:45:07 UTC


README

filament-loginguard-art

FILAMENT 5.x LARAVEL 11 | 12 | 13 Packagist Tests Passing Code Style Passing Downloads

Filament LoginGuard

Enterprise-grade login security for Filament and Laravel β€” persistent brute-force protection, escalating IP and email lockouts, and active-session management.

Note

Filament already throttles its login page at 5 attempts per minute per IP. This package adds the missing lockout layer on top of that: persistent tracking, escalating bans, per-email protection and an admin UI.

Features

  • πŸ”’ Brute-Force Protection
    • πŸ“¦ Persistent per-IP/email attempt tracking
    • πŸ“ˆ Escalating IP & email lockouts
    • πŸ•ΈοΈ Cross-email (per-IP) and cross-IP (per-email) aggregation
    • 🧠 Attempt decay window
    • 🎯 IP & email whitelists
    • πŸ”“ Self-service unlock link (anti-DoS) mailed to the blocked address
    • πŸ”” Administrator lockout notifications (cooldown + queue support)
  • πŸ‘₯ Session Management
    • πŸ–₯️ Active-session listing with "last active" state
    • πŸ†• New-device detection with optional email alert
    • πŸ”’ Concurrent-session limits with oldest-first eviction
    • πŸ”„ One-click session revoke
    • πŸ“± My Devices β€” self-service device list with sign-out, "This isn't me" and device forgetting (cookie-token device identity)
  • 🧰 Filament Management Interface
    • πŸ“‹ Login Attempts page β€” inspect & unblock recorded attempts
    • πŸ“Š Failed-attempts / lockout stats widget
    • πŸ“ˆ Failure-trend & top-attacked charts (7/30-day trend, Top-10 emails & IPs)
    • πŸ“€ CSV export of attempts & sessions (via Filament's export system)
    • πŸ›‘οΈ Gate-based per-page authorization

Compatibility

Requirement Version
PHP 8.3+
Laravel 11, 12 or 13
Filament 5.x (β‰₯ 5.8.2, Livewire 4)

Earlier 5.x releases have known security advisories; require at least Filament 5.8.2.

Table of Contents

Installation

1. Install the Package

You can install the package via composer:

composer require solution-forest/filament-loginguard

2. Publish the Config and Migrate

Migrations are loaded and run automatically (runsMigrations()), so you only need to publish the config file and run migrate:

php artisan vendor:publish --tag="filament-loginguard-config"
php artisan migrate

The interactive install command publishes the config file for you and reminds you to star the repo:

php artisan filament-loginguard:install

Optionally, you can publish the views and translations using:

php artisan vendor:publish --tag="filament-loginguard-views"
php artisan vendor:publish --tag="filament-loginguard-translations"

CSV export prerequisites

The CSV export buttons are built on Filament's export system, which needs its own database tables. Fresh apps may be missing them β€” run:

php artisan make:queue-batches-table
php artisan make:notifications-table
php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate

Without these, the first export attempt fails with a "table not found" error (exports, job_batches or notifications). Exports also run through the queue, so a queue worker (or the sync driver) must be available.

3. Register the Admin Pages

To enable the admin pages (Login Attempts: view / unblock recorded attempts; User Sessions: list / revoke active sessions), register the plugin in your panel provider, e.g. app/Providers/Filament/AdminPanelProvider.php:

use SolutionForest\FilamentLoginGuard\FilamentLoginGuardPlugin;

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

The core protection works without registering the plugin β€” only the admin pages need it. Each page can also be disabled independently via pages.attempts.enabled / pages.sessions.enabled.

How it works

The package hooks into Laravel's core auth events rather than any specific UI, so it protects every login flow that goes through Auth::attempt():

  • Native Laravel auth β€” the Laravel Starter Kits, Breeze, and Fortify all authenticate through Auth::attempt(), so their login forms are protected out of the box.
  • Filament β€” the built-in login page (and the Filament Starter Kit, which extends it) calls attemptWhen(), which fires the same events.
  • Custom controllers β€” anything that calls Auth::attempt() directly works too.

Social login (OAuth/Socialite) has no password to brute-force, so it doesn't fire Attempting/Failed; it still fires Login, so success tracking and session management apply.

Event Action
Illuminate\Auth\Events\Attempting If the IP or email is currently locked out, a ValidationException with the lockout message is thrown before any credential work happens.
Illuminate\Auth\Events\Failed The failure is recorded for the (IP, email) pair. When the threshold is reached, the IP and/or email is locked out and admins are notified by email.
Illuminate\Auth\Events\Login A successful login resets the failed-attempt counter for that (IP, email) pair and releases the email-scoped lock. IP-scoped locks remain in force so one valid login cannot unblock a shared or attacking IP.

Lockout semantics:

  • Rows are stored per (IP, email) pair in the filament_loginguard_attempts table.
  • With tracking.per_ip enabled, the sum of attempts across all emails from one IP triggers a lockout of that IP β€” rotating emails doesn't help attackers. tracking.per_email does the same across IPs for one email.
  • Lock durations escalate: the first lockout lasts lockout.initial_minutes; the 2nd, 3rd and subsequent lockouts use lockout.escalation_hours (e.g. [24, 72, 168] = 1 day, 3 days, 7 days+). The last value repeats for every further lockout. Escalation history is stored in its own append-only table, so cleaning up old attempt rows never resets the ladder.
  • Attempts are counted in a fixed window: failures only accumulate while they fall inside the same attempts_window_minutes window; the first failure after the window expires restarts the counter at zero. A slow drip just under the window length never reaches the threshold.
  • A lock is never extended by further attempts while it is active; a successful login resets the pair's counter and releases the email lock (forgiving the legitimate owner without unblocking the shared IP).
  • The lockout message is rendered in the Filament login form (data.email error key) and as a standard email validation error in non-Filament forms (redirect back for web requests, 422 for JSON).
  • Localhost (127.0.0.1, ::1) is whitelisted by default.
  • Behind a proxy/CDN? Set lockout.trusted_proxies to your proxy IPs or CIDR ranges. When the immediate request IP matches one, the real client IP is read from X-Forwarded-For (walking right-to-left past trusted proxies, so clients cannot spoof it). Laravel's own TrustProxies middleware may already rewrite request()->ip() β€” prefer configuring it there if you use the standard mechanism.
  • A LoginLockedOut event is dispatched on every lockout β€” listen for it to wire up custom alerting (Slack, webhooks, etc.) alongside or instead of the built-in email notification.

Warning

Email lockouts can be abused as a denial-of-service vector. An attacker who knows (or guesses) an account's email can deliberately fail logins to lock that email out β€” the legitimate owner is then blocked too. Mitigations:

  • Enable self-service unlock (lockout.notifications.self_unlock.enabled): the lockout email sent to the blocked address carries a signed, single-use "unlock now" link (a confirmation page, then the unlock β€” the URL contains only an opaque token, never the email). The real owner can clear the lock immediately. Attempt counters are kept, so repeated lockouts still escalate. IP locks are never lifted this way.
  • Whitelist critical accounts via lockout.whitelist.emails so they can always log in.
  • Keep lockout.initial_minutes moderate and review the escalation_hours ladder.
  • If you run behind a proxy/CDN, configure lockout.trusted_proxies (below) so lockouts key on the real client IP β€” otherwise all traffic appears to come from the proxy IP and a single attacker can lock out everyone.

Login Attempts page

The page also includes visualization and reporting tools:

  • Charts (pages.attempts.charts): a daily failed-attempts trend with a 7/30-day filter plus Top-10 leaderboards for attacked emails and source IPs β€” attack waves are obvious at a glance.
  • CSV export (pages.attempts.export): export the table (respecting your filters) via Filament's export system. Exports run through the queue, so a queue worker must be available in production.

Session management

Requires SESSION_DRIVER=database. The User Sessions admin page lists every active session with a human-readable "last active" state (Laravel updates last_activity on every request, including Livewire clicks) and a one-click Revoke.

User Sessions page

  • New-device detection: each login's browser+platform is fingerprinted (sessions.new_device); a session is flagged "New" on the page when its fingerprint was first seen within window_hours, and an optional email notifies the account owner the first time a device is seen.

  • Concurrent session limits: set sessions.concurrent_limit to cap sessions per user β€” the oldest sessions are evicted to make room when a new login would exceed the limit.

  • CSV export (pages.sessions.export): export the active-session list as CSV (via Filament's export system).

  • Closing the browser without logging out (or backgrounding the tab) simply stops updating last_activity, so the session ages out naturally and expires after session.lifetime; sweep expired rows with the filament-loginguard:cleanup-sessions command.

  • Closing the browser without logging out (or backgrounding the tab) simply stops updating last_activity, so the session ages out naturally and expires after session.lifetime; sweep expired rows with the filament-loginguard:cleanup-sessions command.

My Devices (self-service account security)

Every panel user gets a My Devices entry in the user menu (no admin permission required β€” the query is always scoped server-side to the authenticated user). Devices are grouped by live-session state:

  • This device β€” pinned on top, marked "This device", with no destructive actions.
  • Other signed-in devices β€” devices with live sessions, each with Sign out (delete that device's sessions, keep recognition) and "This isn't me" (a security incident: revoke all device sessions AND the identity β€” the cookie token can never silently become recognized again).
  • Recently signed out β€” a collapsible section of devices with no live session, each with Forget device (normal removal; the next sign-in from that browser is treated as a new device).

Each row shows a type-aware icon (desktop / mobile / tablet, falling back to a terminal icon for scripts and unrecognized clients), the device name, "Last active Β· IP", and a View details slide-over (client, browser/OS, first seen, last active, last IP, active-session count and the full user agent). A Sign out other sessions header action deletes all other sessions while keeping the current one and all device recognitions β€” a known device is never shown as online just because it was seen before.

Device identities are powered by an opaque cookie token: the browser holds a random token, the database only stores its SHA-256 hash, and identities are scoped per guard + user model + user identifier β€” so shared browsers, UUID/ULID users and multi-guard panels never collide. Devices beyond devices.retention_days with no active sessions are removed by filament-loginguard:cleanup-devices.

Configuration

This is the contents of the published config file (config/filament-loginguard.php), grouped into five sections: lockout (brute-force protection behavior), sessions (active-session tracking behavior), devices (device identity for My Devices), maintenance (optional auto-scheduling), and pages (Filament admin page wiring for both features):

return [
    'lockout' => [
        'enabled' => true,                 // master switch
        'max_attempts' => 10,              // failures allowed inside the window before a lockout
        'initial_minutes' => 15,           // duration of the first lockout
        'escalation_hours' => [24, 72, 168], // 2nd, 3rd, 4th+ lockout durations; last value repeats
        'attempts_window_minutes' => 30,   // decay + aggregate counting window
        'trusted_proxies' => [],           // proxy/CDN IPs or CIDRs; X-Forwarded-For is honored for these

        'tracking' => [
            'per_ip' => true,              // aggregate attempts across emails per IP
            'per_email' => true,           // aggregate attempts across IPs per email
            'guards' => [],                // restrict to specific guards, e.g. ['web']; empty = all
        ],

        'whitelist' => [
            'ips' => ['127.0.0.1', '::1'],
            'emails' => [],
        ],

        'notifications' => [
            'enabled' => true,
            'mail' => [
                'to' => [],                // admin addresses; empty = no notifications
                'cooldown_minutes' => 60,  // at most one notification per IP per window
                'queue' => false,          // false = sync; queue name string = queued
            ],
            'self_unlock' => [
                'enabled' => false,        // mail a signed, single-use unlock link to the blocked address
                'link_ttl_minutes' => 60,  // how long the unlock link stays valid
                'queue' => false,          // false = sync; queue name string = queued
            ],
        ],
    ],

    // Active user sessions (requires SESSION_DRIVER=database).
    'sessions' => [
        'enabled' => true,                 // master switch for session security features
        'table' => 'sessions',
        'online_threshold_seconds' => 60,  // "online now" cutoff
        'user_model' => null,              // null = auth.providers.users.model
        'concurrent_limit' => null,        // max concurrent sessions per user; null = unlimited

        'new_device' => [
            'enabled' => true,             // @deprecated v0.7 β€” see devices.*
            'window_hours' => 24,          // sessions first seen within this window are flagged "New"

            'notifications' => [
                'enabled' => false,        // emails the account owner the first time a device is seen
                'mail' => [
                    'queue' => false,
                ],
            ],
        ],
    ],

    // Device identity (My Devices): opaque cookie tokens, hashed in the DB.
    'devices' => [
        'enabled' => true,                 // master switch for the device identity layer

        'cookie' => [
            'name' => 'filament_loginguard_device',
            'lifetime_days' => 365,
            'same_site' => 'lax',
        ],

        'retention_days' => 90,            // drop devices with no sessions after this many days (0 = keep)
        'max_devices_per_user' => 20,      // hard cap of device identities per account (0 = unlimited)

        'notifications' => [
            'new_device' => false,         // email the account owner on a new device sign-in
            'mail' => [
                'queue' => false,
            ],
        ],
    ],

    'maintenance' => [
        'cleanup_attempts' => [           // delete stale attempt records
            'enabled' => false,           // false = schedule it yourself in routes/console.php
            'expression' => '0 0 * * *',  // cron expression (default: daily at midnight)
        ],

        'cleanup_sessions' => [           // delete expired session rows (needs SESSION_DRIVER=database)
            'enabled' => false,
            'expression' => '0 * * * *',  // cron expression (default: hourly)
        ],
    ],

    'pages' => [
        'attempts' => [
            'enabled' => true,
            'slug' => 'login-guard',
            'cluster' => null,             // optional Filament Cluster class-string to nest the page under
            'navigation_label' => null,    // null falls back to translations
            'navigation_icon' => 'heroicon-o-shield-exclamation',
            'navigation_group' => null,
            'navigation_sort' => null,
            'authorize' => null,           // gates everything; the finer keys below win when set
            'authorize_view' => null,      // viewing the page
            'authorize_unblock' => null,   // unblock / bulk-unblock actions
            'stats_widget' => true,        // show the failed-attempts / lockout stats widget
            'charts' => true,              // show the failure-trend & top-attacked charts
            'export' => true,              // allow CSV export of the attempts table
        ],

        'sessions' => [
            'enabled' => true,             // admin page only; session security uses sessions.enabled
            'slug' => 'user-sessions',
            'cluster' => null,
            'navigation_label' => null,
            'navigation_icon' => 'heroicon-o-computer-desktop',
            'navigation_group' => null,
            'navigation_sort' => null,
            'authorize' => null,
            'authorize_view' => null,      // viewing the page
            'authorize_revoke' => null,    // revoke / bulk-revoke actions
            'export' => true,              // allow CSV export of the sessions table
        ],

        // Self-service account security (no admin permission needed).
        'my_devices' => [
            'enabled' => true,
            'slug' => 'my-devices',
            'user_menu' => true,           // show "My Devices" in the user menu
            'navigation' => false,         // also list it in the panel navigation
            'navigation_label' => null,
            'navigation_icon' => 'heroicon-o-device-phone-mobile',
            'navigation_group' => null,
            'navigation_sort' => null,
        ],
    ],
];

Warning

Both admin pages are visible to any authenticated panel user by default. Restrict them with the authorize option, which accepts a permission/ability name checked via $user->can(...). For finer control, use the split keys: authorize_view gates viewing the page and authorize_unblock / authorize_revoke gate the security actions β€” e.g. grant reviewers view-only access and let operators perform unblocks/revokes.

With spatie/laravel-permission (e.g. when using Filament Shield), the string is a permission name β€” just create the permission and assign it to a role; no Gate is required:

// config/filament-loginguard.php
'pages' => [
    'attempts' => ['authorize' => 'view-filament-loginguard', /* ... */],
    'sessions' => ['authorize' => 'view-filament-loginguard', /* ... */],
],

Without spatie, define a Gate for the ability in a service provider:

// app/Providers/AppServiceProvider.php
Gate::define('view-filament-loginguard', fn (User $user) => $user->can('access-admin-settings'));

Maintenance

Delete stale, expired attempt records (outside the decay window with no active lock):

php artisan filament-loginguard:cleanup-attempts
php artisan filament-loginguard:cleanup-attempts --all   # delete every record

Delete expired session rows (Laravel's session garbage collection is probabilistic, so rows from closed browsers can linger):

php artisan filament-loginguard:cleanup-sessions
php artisan filament-loginguard:cleanup-sessions --all   # delete every session

Schedule both in routes/console.php for automatic upkeep:

use Illuminate\Support\Facades\Schedule;

Schedule::command('filament-loginguard:cleanup-attempts')->daily();
Schedule::command('filament-loginguard:cleanup-sessions')->hourly();

Alternatively, set maintenance.cleanup_attempts.enabled / maintenance.cleanup_sessions.enabled to true in the config and the package registers them for you using each command's expression cron value (cleanup-sessions only when SESSION_DRIVER=database). Prefer scheduling manually when you need a custom frequency, timezone, or onOneServer().

Roadmap

Planned features, roughly in the order they are likely to land. Suggestions and PRs welcome β€” open an issue to discuss.

  • Progressive response & CAPTCHA β€” soften the cliff between "allowed" and "locked out". After a configurable number of failures, the login form requires a CAPTCHA (Cloudflare Turnstile or hCaptcha) instead of blocking outright; only repeated failures beyond a second threshold trigger a lockout. Shared-IP environments (offices behind NAT) stop being collateral damage, while bots fail the challenge automatically.
  • Risk-based login (adaptive authentication) β€” score every login from signals the package already collects (unseen device fingerprint, new IP range, unusual time of day, recent failure counts) and respond proportionally: allow, step-up verification (email code), or block and alert the account owner. An attacker with a stolen password is challenged on an unfamiliar device, while the legitimate owner logs in without noticing anything.
  • My devices (self-service account activity) β€” a per-user panel section (like Google's "Your devices") listing the user's own signed-in devices β€” browser, OS, IP, last active β€” with a "View details" slide-over, one-click "This isn't me" revoke and a "sign out other sessions" button. Users can react to suspicious logins themselves instead of waiting for an admin. (shipped in v0.7.0 β€” powered by opaque device-token identities)
  • Geo / ASN blocking β€” resolve the login source against an IP geolocation database (e.g. MaxMind GeoLite2) and optionally block or flag traffic by country or ASN. Blocking datacenter ASNs is a very cheap way to cut most scripted attacks without touching residential users; flagged attempts get a country badge on the admin pages.
  • Charts widget β€” visualize the lockout stats that the numeric widget already tracks: a daily failed-attempts trend for the last 7/30 days and Top-10 leaderboards (attacked emails, source IPs, devices). Makes attack waves obvious at a glance. (shipped in v0.6.0)
  • Slack / Telegram / Webhook notifications β€” deliver lockout alerts beyond email via Laravel notification channels β€” a Slack channel is where on-call engineers actually look. Configured per channel (webhook URL, queue) alongside the existing mail notifications.
  • Cloudflare integration β€” push lockouts to the edge: when an IP is locked out, automatically create a block rule in your Cloudflare zone via the API (and optionally remove it on unlock). Attack traffic is dropped at the CDN instead of ever reaching Laravel, which changes the game under volumetric attacks.
  • CSV export β€” export filtered Login Attempts and User Sessions as CSV from the admin tables for compliance audits (ISO 27001 / SOC 2), incident forensics, and management reports. (shipped in v0.6.0)

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.