devdasun/password-history

Prevent users from reusing recent passwords in Laravel

Maintainers

Package info

github.com/DasunMuthuruwan/laravel-password-history

pkg:composer/devdasun/password-history

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.1 2026-07-30 10:37 UTC

This package is auto-updated.

Last update: 2026-07-30 16:25:03 UTC


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.0 of 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:

  1. Fork the repo and create a feature branch.
  2. Add or update tests for any behavior change.
  3. Run composer test and ensure it passes.
  4. Open a PR describing the change and, if relevant, link the issue.

License

The MIT License (MIT). See LICENSE.md for details.