mei-labs/filament-renew-password
Filament Renew Password is a Laravel Filament plugin for prompting users to renew passwords, helping enforce periodic password updates with configurable expiration rules and simple integration into Filament panels.
The package adds password expiry/renewal to Filament panels by intercepting authenticated requests and redirecting users to a renewal page until they change their password. It is small and config-driven, so most of your day-to-day work is in the config file, the published migration, and the panel provider — not in vendor code.
Install and publish in this order (order matters because each step depends on the previous):
composer require mei-labs/filament-renew-password
php artisan vendor:publish --tag="filament-renew-password-config"
php artisan vendor:publish --tag="filament-renew-password-migrations"
php artisan migrate
Register the plugin per panel — not in a shared service provider:
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(RenewPasswordPlugin::make());
}
Read these three things first, in this order:
config/filament-renew-password.php — expiry window, redirect/renewal route name, and the "force on first login" toggle. Most behaviour is config, so read this before writing code.users (typically password_changed_at and/or a must_renew_password-style flag). This determines your backfill strategy and your expiry queries.First use case: enforce a 90-day policy and force existing users to rotate on next login.
// config/filament-renew-password.php
'expiry_days' => 90,
// verify the actual key names against the published file
Then backfill deliberately (see Gotchas — the null case is the biggest risk):
// choose one deliberately, test on staging first
User::query()->update(['password_changed_at' => null]); // "prompt everyone"
// or a graceful ramp:
User::query()->update(['password_changed_at' => DB::raw('created_at')]);
Verify with one test user (force expiry, log in, confirm redirect to the renewal route, change password, confirm access), then roll out. This single flow is where 90% of day-one value comes from.
Use the trait, don't patch the base model. Apply the package's trait to your authenticatable so the renewal-state contract lives in one place. If you later swap auth models (e.g. multi-guard), you can re-apply the trait without touching the original model.
Middleware placement is the whole game. The renewal check must run after Filament's Authenticate middleware (so auth()->user() is populated) but before panel resource routes resolve. Register it on the panel's middleware stack, after authentication:
$panel->middleware([
// ... Filament defaults, including Authenticate
\MeiLabs\FilamentRenewPassword\Http\Middleware\EnforceRenewal::class,
]);
Adding it globally in bootstrap/app.php is the classic cause of redirect loops — the package's exempt-route allowlist is matched by route name, so the renewal and login routes must still be reachable.
Per-panel policy via multiple plugin instances. If you have admin and customer panels with different risk profiles, configure each instance rather than branching inside one shared config. This keeps policy explicit at the panel definition:
// AdminPanelProvider
->plugin(RenewPasswordPlugin::make()->expiryDays(30));
// CustomerPanelProvider
->plugin(RenewPasswordPlugin::make()->expiryDays(180));
Customise the renewal page by subclassing, not by editing vendor files. Extend the package's renewal page class, override the view/Livewire component as needed, and point the plugin at your class. This survives minor releases (v2.1.0 is a non-breaking minor bump), which vendor-file overrides will not.
Own notifications yourself. The package enforces; it doesn't warn. Add a scheduled command that queries users whose password_changed_at is within N days of the threshold and dispatches your own notification — e.g. routes/console.php:
Schedule::command('users:notify-expiring-passwords')->daily();
This keeps comms and copy under your control and avoids surprises if the package's behaviour changes.
Prune any history table on a schedule. If the release supports password history / reuse prevention, prune it daily, and keep the retention window at least as long as the "don't reuse last N" setting. Shorter retention silently degrades reuse prevention; longer retention means unbounded growth.
Test with frozen time. Cover three cases explicitly:
$this->travel(91)->days();
$this->actingAs($user)->get('/admin')->assertRedirect('/admin/renew-password');
Add a fourth for the "force on first login" flag if you enable it: a fresh user with password_changed_at = now() but the force flag set should still be prompted.
Harden the session after renewal. The plugin prompts, it doesn't necessarily rotate sessions or remember-me tokens. After a successful password change, call Auth::logoutOtherDevices($password) and let Laravel rotate the remember token. Do this in your renewal flow / event listener.
Keep validation in one place. Define your rules via Password::default() in a service provider, and make sure the package's form uses the same ruleset (either via config or by pointing the plugin at your subclassed page). Diverging rules produce the confusing UX where a password is accepted at renewal but rejected at registration or login.
The password_changed_at backfill is the #1 production incident. After migration, existing rows are NULL. Depending on the package's null handling, that either means "never expires" (silent no-op — users are never prompted) or "expired since epoch" (everyone is locked out at once). Reproduce both cases on a staging copy with real user data, decide the semantics explicitly, and backfill to users.created_at if you want a gradual ramp. Never ship the migration without a backfill plan.
Caches are the most common "the package is broken" false alarm. Two layers to check:
php artisan config:clear locally and php artisan config:cache in production (or a deploy step that rebuilds it). A cached config with old expiry_days silently ignores your change.php artisan filament:optimize-clear (or optimize:clear). Stale compiled panel config makes you debug a change that isn't loaded.Redirect loops (ERR_TOO_MANY_REDIRECTS after login) mean the exempt-route allowlist doesn't match the renewal route — usually because you changed the route name, added a route prefix, or localised the panel path. Fix it in the package's exempt-route config rather than patching the middleware; a patched middleware is the first thing that breaks on upgrade.
Livewire requests bypass naive middleware. Filament is Livewire-driven: a page can render, then component updates hit the update endpoint. Ensure the guard covers Livewire update routes too, otherwise an expired user can still partially interact with the panel between page loads.
Impersonation and API tokens are separate auth paths. Filament's impersonation can trap an admin inside a user with an expired password (they get bounced to renewal and can't leave the impersonation cleanly). Token-authenticated API routes (Sanctum) aren't intercepted at all — if the requirement extends beyond the panel, enforce expiry in your API middleware separately.
Don't let it fight Password::defaults(). Keep rules centralised; if the package's form diverges from your global password rules, users get a password accepted at renewal but rejected elsewhere, or vice versa. Prefer configuring the package to use your rules over duplicating a second ruleset.
Extension points to prefer, in order:
Pinning and upgrade cadence. v2.1.0 is a patch-level minor with no breaking changes implied by semver, so anything that breaks on upgrade is most likely a vendor-file override rather than a public API change. Review the diff between v2.0.1 and v2.1.0 (https://github.com/Mei-LABS/filament-renew-password/compare/v2.0.1...v2.1.0) before bumping, pin to ^2.1 if you want the fixes without surprise minors, and keep a smoke test for the renewal flow in CI so an upgrade can't silently disable enforcement.
Scope check before adopting deeper. It is a Filament admin-panel password-renewal plugin, not an IAM platform. If requirements expand to SSO-enforced policy, MFA orchestration, SCIM, audit trails, or non-Filament user journeys, plan to build or buy elsewhere rather than stretching this package.
How can I help you explore Laravel packages today?