samasend/laravel-needs-auto-rehash
Laravel Needs Auto Rehash automatically detects outdated password hashes and rehashes them after successful login, keeping user credentials aligned with your app’s current hashing algorithm and cost settings with minimal configuration.
Install the package:
composer require samasend/laravel-needs-auto-rehash
php artisan vendor:publish. Behavior follows your existing config/hashing.php (driver, bcrypt.rounds, argon.*).src/), specifically which events it hooks (Login, Registered, PasswordReset) and where it reads the plaintext password. The whole package is a few dozen lines; read it before trusting it.bcrypt.rounds from 10 → 12, or switched HASH_DRIVER to argon2id. Existing users shouldn't need to reset. After installing, on a user's next successful login their hash is silently upgraded.password column — it should now start with $2y$12$ (or $argon2id$) and differ from the old value.Standard Auth::attempt — the happy path. The listener needs the plaintext password and gets it off the current request (typically request()->input('password')). Any login built on Auth::attempt() where the field is named password works with no code changes: Breeze, Fortify/Jetstream, Laravel UI, your hand-rolled LoginController.
Custom credential field name. If your form posts pass or user_password, Auth::attempt() still succeeds — but the listener can't find the plaintext and silently does nothing. Normalize the field name in your controller, or replace the listener (see Gotchas).
API / token auth. Sanctum, Passport, and HTTP basic auth don't fire Illuminate\Auth\Events\Login, so no rehash happens. Handle it manually where you have the plaintext:
if (Hash::needsRehash($user->password)) {
$user->update(['password' => Hash::make($request->password)]);
}
Programmatic login. Auth::login($user) fires Login, but there's no password in the request, so the listener skips. Rehash explicitly at points where you hold the plaintext: registration, admin password reset, impersonation flows.
Testing. Don't Event::fake() in tests that assert rehashing — the listener won't run. Assert against the DB instead:
$oldHash = $user->password;
$this->post('/login', ['email' => $user->email, 'password' => 'secret']);
$this->assertNotSame($oldHash, $user->fresh()->password);
To force needsRehash() to return true, set a different bcrypt.rounds in the test environment than the one used by your factory.
Algorithm migration. Set HASH_DRIVER=argon2id, deploy, and let users upgrade on next login. bcrypt hashes keep verifying in the meantime — BcryptHasher still checks old $2y$ hashes even when the default driver is argon. Test this end-to-end in staging before rolling it out.
app/Listeners/ and own it rather than depending on an abandoned package.password field in the request silently does nothing. If "rehash isn't working," dump request()->input('password') in a temporary listener first.Illuminate\Auth\Events\Login — not before credential validation. Rehashing on a failed login would be wrong.Login listeners touch password (audit logging, etc.), check whether they observe the old or new value — registration order determines this.bcrypt.rounds or HASH_DRIVER without rebuilding the config cache rehashes with the stale cost, looking like nothing happened. Run php artisan config:cache after changes.needsRehash() is fine but Hash::make() throws at login. Verify the extension in the target environment before flipping the driver.EventServiceProvider::boot(), or uninstall and copy the logic. The package provides no config surface to hook into.Login listener — Log::info('login', ['has_plain' => request()->filled('password'), 'needs' => Hash::needsRehash($event->user->password)]). That single line tells you whether the problem is the package or your login flow.How can I help you explore Laravel packages today?