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 Password Rotation Laravel Package

bbs-lab/filament-password-rotation

A Filament plugin for Laravel that adds configurable password rotation policies. Enforce password expiration, track password history, and prompt users to update credentials at login or within Filament panels, helping teams meet security requirements without custom code.

View on GitHub
Deep Wiki
Context7

Getting Started

bbs-lab/filament-password-rotation is a Filament plugin that enforces periodic password changes for panel users: it tracks when a password was last set, redirects users with expired passwords to a forced-change form, and blocks the rest of the panel until the change is made.

Minimal setup, in order:

  1. Install
    composer require bbs-lab/filament-password-rotation
    
  2. Publish config and migrations
    php artisan vendor:publish --tag="filament-password-rotation-config"
    php artisan vendor:publish --tag="filament-password-rotation-migrations"
    php artisan migrate
    
    The migration adds the rotation timestamp column to users (check the published file for the exact column name in your version).
  3. Prepare your User model — add the package trait and make sure the timestamp column is fillable/castable if your code writes it:
    use BBSLab\PasswordRotation\Concerns\HasPasswordRotation;
    
    class User extends Authenticatable
    {
        use HasPasswordRotation;
    }
    
  4. Register the plugin on the panel(s) in app/Providers/Filament/AdminPanelProvider.php:
    ->plugin(PasswordRotationPlugin::make())
    
  5. Set the policy in config/filament-password-rotation.php (expiry window, redirect/forced-change page, exemptions).

Where to look first: the published config file defines the entire behaviour surface — expiry interval, which page is rendered, and how users without a recorded timestamp are treated. Read that before touching code.

First use case: compliance-driven rotation. A 90-day window on the admin panel, with the forced-change page appearing instead of the dashboard on the first request after expiry.

Implementation Patterns

Scope the plugin per panel. Register it only where the policy applies. A customer-facing panel and an internal admin panel rarely share a rotation policy:

public function panel(Panel $panel): Panel
{
    return $panel
        ->id('admin')
        ->plugin(PasswordRotationPlugin::make());
}

Leave it off panels backed by SSO or API-only access.

Backfill before you deploy. The single biggest rollout risk is that every existing user has a NULL timestamp. Either seed the column from updated_at in a migration, or configure the "no timestamp" case explicitly:

DB::table('users')->update(['password_changed_at' => DB::raw('updated_at')]);

Without this you either lock out the entire user base at once or silently exempt everyone — both are surprising.

Exempt non-interactive accounts. Service accounts, seeded demo users, and break-glass admins should not be forced through rotation. Prefer a small policy check (trait method or config callback, depending on the version) over scattering if statements in the model:

// app/Models/User.php
public function isExemptFromPasswordRotation(): bool
{
    return $this->hasRole('service-account');
}

Confirm the exact hook name in the package source — the pattern is stable, the method name has moved between minor versions.

Tune the forced-change form, don't rebuild it. The package ships a Filament page with the right password field configuration, including ->revealable() for the eye toggle and the current-password / new-password autocomplete tokens. If you need extra copy, a heading, or an audit log entry, extend the page class and swap it via config rather than writing a parallel form — you'll inherit future fixes like the v2.1.3 autofill change for free.

Pair with validation rules. Rotation is orthogonal to strength. Add Filament's password rule to the same field so a user rotating every 90 days can't rotate from Password1! to Password2!:

TextInput::make('password')
    ->password()
    ->revealable()
    ->rule(Password::default()->uncompromised());

Test with time travel. Rotation logic is pure date arithmetic, so it's cheap to cover:

it('forces a change after the expiry window', function () {
    $user = User::factory()->create(['password_changed_at' => now()->subDays(91)]);

    $this->actingAs($user)
        ->get('/admin')
        ->assertRedirect(/* the forced-change page */);
});

Set password_changed_at explicitly in factories (default to now()) so unrelated tests don't get redirected mid-suite.

Reminder notifications. A scheduled command that queries users approaching expiry and sends a Filament database notification gives people a week's warning and dramatically reduces "I was locked out mid-task" support tickets. The package handles enforcement, not warning — that's yours to build.

Gotchas and Tips

Pin to ^2.1.3, not 2.1.2. The v2.1.2 tag on Packagist points at a commit that does not contain the autofill fix; the notes say so explicitly. If you locked 2.1.2 exactly, or cached a build against it, you have the bug without the fix. Clear composer.lock staleness with composer update bbs-lab/filament-password-rotation.

The Chrome autofill fix is fragile if you fork the view. v2.1.3 adds a hidden username input as an anchor so Chrome's saved-password manager and strong-password generator stop overwriting the current-password field. If you published and edited the forced-change Blade view, re-publish or manually port that hidden input — otherwise the bug returns, and it presents as "the user's current password field silently gets filled with a generated password," which is very hard to diagnose from a bug report.

Passwords changed outside the package won't reset the clock. An admin reset via the users resource, a Password::reset() flow, an artisan command, or a social-login password set will not go through the forced-change page. Listen for the relevant events (or override the model's password setter) and touch the timestamp, or those users get prompted again immediately after a legitimate reset.

Middleware only covers panel routes. Sanctum/API tokens, Fortify endpoints, queued jobs, and any custom routes outside the panel are unaffected. If your policy is a compliance requirement, it applies to the human login path only — document that boundary rather than assuming it's enforced everywhere. Pair with Auth::logoutOtherDevices() on password change so remembered sessions don't linger past the rotation.

Check the NULL timestamp semantics in your version. "Never rotated" is either treated as "expired now" (mass lockout on install) or "never expires" (policy silently no-ops for pre-existing users). Both are defensible defaults; neither is what most people assume. Test it before deploying.

Timezone and comparison precision. Expiry is compared against now() in the app timezone. On the boundary day, a user can be prompted a few hours "early" or "late" relative to what they expect. That's fine — don't build anything that depends on minute-level precision.

Config caching. The config file is read through Laravel's config repository; after changing the expiry window in production run php artisan config:cache (or config:clear locally). A stale cached config is the usual explanation for "I changed it to 30 days and nothing happened."

Impersonation and the user menu. Filament's impersonation feature and custom user-menu actions run inside the panel and will hit the rotation middleware. Decide deliberately whether impersonating admins are redirected to the forced-change page — usually you want to exempt impersonation so support staff aren't blocked.

Extension points worth knowing: the config file (policy knobs), the page class (UI), the model trait (exemption predicate), and the migration (column name). Almost every real-world customisation lands in one of those four; if you find yourself patching vendor files, you've probably missed a config key.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource