New

Just launched: Vueblocks, a brand-new search engine for Vue packages! Explore Vueblocks →

bbs-lab/laravel-password-rotation

Password rotation for any Laravel app (no admin panel required): force any authenticatable to rotate its password every N days, with reuse prevention, a first-login gate, expiry warnings and a redirect middleware.
1,138 1
Install
composer require bbs-lab/laravel-password-rotation
Latest Version:v1.3.0
PHP:^8.2
Laravel:11 · 12 · 13
License:MIT
Last Updated:Sep 24, 2026
Links: GitHub  ·  Packagist
Maintainer: Kezho

Laravel Password Rotation

Latest Version on Packagist Tests Total Downloads

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:

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, when force_on_first_login is 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.

Bypass rotation per request (e.g. SSO users)

Some users must never be forced through the local change screen — SSO users, for instance, whose password lives in the identity provider. Register a bypass callback via the PasswordRotation facade (in a service provider's boot()). It receives the request and the expired user, and returning true lets that request through:

use BBSLab\LaravelPasswordRotation\Facades\PasswordRotation;
use Illuminate\Http\Request;

PasswordRotation::bypass(
    fn (Request $request) => $request->hasSession()
        && $request->session()->get('sso') === true,
);
  • Register the callback in code, not in the config file — a Closure there would break php artisan config:cache.
  • Guard hasSession() before reading the session so the callback is safe if the middleware ever runs outside the web group (a token/API guard has no session). Store the SSO marker as a real boolean (session(['sso' => true])).
  • bypass() may be called several times; the request is exempt as soon as any callback returns true.

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.

Related Packages

minvws/horsebattery-laravel

A Laravel package for the horsebattery password generator

39,496 1
olssonm/laravel-backup-shield

Protection for your laravel backups

19,620 30
olssonm/blockip

Block requests from specified IPs in Laravel 5.

242 6
richardstyles/wire-shield

Monitors Livewire update requests for deserialization attack patterns (CVE-2025-...

1 0
alkhwlani/xss-middleware

A Laravel Middleware to filter user inputs from XSS and iframes and other embed...

531,339 5

Version History

Version Released PHP Laravel License
v1.3.0 ^8.2 ^11.0 || ^12.0 || ^13.0 MIT
v1.2.1 ^8.2 ^11.0 || ^12.0 || ^13.0 MIT
v1.2.0 ^8.2 ^11.0 || ^12.0 || ^13.0 MIT
v1.1.0 ^8.2 ^11.0 || ^12.0 || ^13.0 MIT
v1.0.0 ^8.2 ^11.0 || ^12.0 || ^13.0 MIT