devdasun / password-history
Prevent users from reusing recent passwords in Laravel
Package info
github.com/DasunMuthuruwan/laravel-password-history
pkg:composer/devdasun/password-history
Requires
- php: ^8.3
- illuminate/database: ^13.0
- illuminate/hashing: ^13.0
- illuminate/support: ^13.0
Requires (Dev)
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^12.0
README
Prevent users from reusing their recent passwords in a Laravel application. Works with any Eloquent model — User, Admin, or any custom authenticatable — via a polymorphic history table.
Requirements
| Package | Version |
|---|---|
| PHP | ^8.3 |
| Laravel | ^13.0 |
For Laravel 12 / PHP 8.2 support, use
^1.0of this package.
Installation
Install via Composer:
composer require devdasun/password-history
Publish and run the migration:
php artisan vendor:publish --tag=password-history-migrations php artisan migrate
Optionally publish the config file:
php artisan vendor:publish --tag=password-history-config
Optionally publish the language file (if you want to customize the validation message):
php artisan vendor:publish --tag=password-history-lang
Setup
Add the HasPasswordHistory trait to any model you want to track (typically User):
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use DevDasun\PasswordHistory\Traits\HasPasswordHistory; class User extends Authenticatable { use HasPasswordHistory; // ... }
Usage
Validating a new password against history
use DevDasun\PasswordHistory\Rules\DifferentFromHistory; use Illuminate\Validation\Rules\Password; $request->validate([ 'password' => ['required', 'confirmed', Password::defaults(), new DifferentFromHistory], ]);
Recording a password after it's changed
Call this right after you save the new hashed password — it's not automatic, since Laravel has no single universal "password changed" event across all flows (registration, reset, profile update):
$user->forceFill([ 'password' => Hash::make($request->password), ])->save(); $user->recordPasswordHistory($user->password);
Checking manually
if ($user->passwordWasUsedBefore($plainPassword)) { // reject }
Configuration
config/password-history.php:
return [ // How many previous passwords to remember and check against. // Older entries beyond this limit are pruned automatically. 'limit' => env('PASSWORD_HISTORY_LIMIT', 5), ];
Set via .env:
PASSWORD_HISTORY_LIMIT=5
Customizing the validation message
Default (lang/en/messages.php):
return [ 'reused' => 'You have used this password before. Please choose a password you haven\'t used in the last :count changes.', ];
After publishing (--tag=password-history-lang), edit your copy at:
lang/vendor/password-history/en/messages.php
Pruning old history via scheduler (optional)
History beyond the configured limit is pruned automatically each time recordPasswordHistory() runs. If you also want a scheduled cleanup (e.g. for orphaned records after a model is deleted without cascading), add to routes/console.php:
use Illuminate\Support\Facades\Schedule; # Trim each user's history down to the configured limit php artisan password-history:prune # Remove history rows for deleted users (e.g. force-deleted without cascade) php artisan password-history:prune --orphaned
Testing
composer install
composer test
Or directly:
./vendor/bin/phpunit
Security
The password_histories table stores hashed passwords only (whatever your app passes into recordPasswordHistory() — always pass the hashed value, never plaintext). Comparison uses Hash::check(), matching Laravel's standard hashing driver.
Changelog
See CHANGELOG.md for recent changes.
Contributing
Pull requests are welcome. Please:
- Fork the repo and create a feature branch.
- Add or update tests for any behavior change.
- Run
composer testand ensure it passes. - Open a PR describing the change and, if relevant, link the issue.
License
The MIT License (MIT). See LICENSE.md for details.