laravel-password-rotation maintained by bbs-lab
Laravel Password Rotation
Password rotation for any Laravel application — no admin panel required. Force any authenticatable model to rotate its password every N days, with reuse prevention, a first-login gate, an expiry-warning helper and a redirect middleware. No Nova, no Filament.
This is the shared core behind the two admin-panel twins:
bbs-lab/nova-password-rotation— Laravel Novabbs-lab/filament-password-rotation— Filament
The rotatable subject is not tied to the User model: everything keys off the
MustRotatePassword interface on whatever authenticatable you gate, and the password history is
polymorphic, so any model works.
Requirements
- PHP
^8.2 - Laravel
^11.0 || ^12.0 || ^13.0
Installation
composer require bbs-lab/laravel-password-rotation
The service provider auto-registers via Laravel package discovery.
The password_histories table migrates automatically. Publish the config and the
(review-before-run) user-column migration, then migrate:
# Config
php artisan vendor:publish --tag=laravel-password-rotation-config
# Adds the password_changed_at column to your authenticatable's table.
# Rename/edit it first — your rotatable model rarely lives on "users".
php artisan vendor:publish --tag=laravel-password-rotation-user-migration
# Optional — only to customise the shipped password_histories migration
# php artisan vendor:publish --tag=laravel-password-rotation-migrations
# Translations (en, fr) — only to customise the messages
php artisan vendor:publish --tag=laravel-password-rotation-translations
php artisan migrate
Usage
Make a model rotatable
Implement MustRotatePassword and use the RotatesPassword trait on any
authenticatable model. Nothing is tied to User — the trait keys off the
interface and the history table is polymorphic.
use BBSLab\LaravelPasswordRotation\Concerns\RotatesPassword;
use BBSLab\LaravelPasswordRotation\Contracts\MustRotatePassword;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements MustRotatePassword
{
use RotatesPassword;
}
The trait stamps password_changed_at whenever the password changes, records
each hash for reuse prevention, and dispatches PasswordRotated. It exposes:
passwordHasExpired(): bool— past the rotation window (or never set, whenforce_on_first_loginis on)passwordIsExpiring(): bool— inside the warning window (warn_days)passwordExpiresAt(): ?CarbonInterface
Force expired users to a change screen
Add EnsurePasswordIsNotExpired to the middleware group that guards your app
and point redirect_route at your own password-change screen:
// config/laravel-password-rotation.php
'redirect_route' => 'password.rotate',
// keep users able to sign out while their password is expired
'except_routes' => ['logout'],
Route::middleware(['web', 'auth', EnsurePasswordIsNotExpired::class])->group(function () {
// ...your app
});
An expired user is redirected to redirect_route on every request except that
route and anything in except_routes. Exempt your logout route (and, if the
change form POSTs to a different route, that route too) or the user is trapped
with no way out. The middleware reads the default guard's user; on a custom
guard, wire it accordingly.
Prevent password reuse
Add the PasswordNotReused rule to your change-password validation:
use BBSLab\LaravelPasswordRotation\Rules\PasswordNotReused;
$request->validate([
'password' => ['required', 'confirmed', Password::defaults(), new PasswordNotReused($request->user())],
]);
It rejects the current password and the last history_count hashes.
Report on rotation status
php artisan password-rotation:report # expired / expiring-soon accounts
php artisan password-rotation:report --all # every account
The command scans the classes listed under models in the config.
License
The MIT License (MIT). Please see License File for more information.