Seamless Password Migration: Shifting users from WordPress to Laravel
One of the biggest hurdles in migrating a legacy site is the users database. You want to move to Laravel, but you don't want to force every single user to reset their password the moment you go live. Here's how you can achieve this when migrating from WordPress!
The answer is a "Just-In-Time" (JIT) migration. We can teach Laravel to recognise old WordPress hashes, validate them, and then silently upgrade the user to a 'Laravel native' standard (like Bcrypt) as soon as they've completed their login.
The problem
Traditionally, WordPress uses the phpass framework (the irony of the name is not lost!), which generates MD5-based hashes usually prefixed with $P$. Not only does Laravel not support this, its also considered to be insecure!
Laravel on the other hand, uses Bcrypt as standard. To bridge this gap, we first need to install a library that understands the WordPress password hash format:
composer require bordoni/phpass
Creating a custom legacy hasher
We need a "traffic controller" class. This class will check if a password hash is an old WordPress string. If it is, it uses the legacy logic; if not, it falls back to Laravel’s native hashing.
Create the file app/Hashing/LegacyHasher.php with the contents below:
<?php
declare(strict_types=1);
namespace App\Hashing;
use Illuminate\Hashing\AbstractHasher;
use Illuminate\Contracts\Hashing\Hasher;
use Hautelook\Phpass\PasswordHash as WPHasher;
/**
* This isn't actually a WordPress hasher but is capable of
* detecting WordPress passwords and forcing a rehash.
*/
final class LegacyHasher extends AbstractHasher implements Hasher
{
public function __construct(public array $options)
{
//
}
/**
* Make a new hash with the given values,
* we'll never use the legacy hasher to create a new hash.
*/
public function make(#[\SensitiveParameter] $value, array $options = []): string
{
return $this->laravelHasher()->make($value, $options);
}
/**
* If we're using the legacy hasher, check the password is valid against that,
* otherwise we'll defer to Laravel's default hasher.
*/
public function check(#[\SensitiveParameter] $value, $hashedValue, array $options = []): bool
{
return !$this->isUsingLegacyDriver($hashedValue)
? parent::check($value, $hashedValue, $options)
: $this->legacyHasher()->CheckPassword($value, $hashedValue);
}
/**
* If we're using the legacy hasher, we need to rehash,
* otherwise we'll defer to Laravel's default hasher.
*/
public function needsRehash($hashedValue, array $options = []): bool
{
return $this->isUsingLegacyDriver($hashedValue) || $this->laravelHasher()->needsRehash($hashedValue, $options);
}
/**
* Check if our hash begins with the known
* WordPress format
*/
private function isUsingLegacyDriver(string $hash): bool
{
return str_starts_with($hash, '$P$');
}
/**
* Return our legacy/WordPress PHPAss hasher.
*/
private function legacyHasher(): WPHasher
{
return new WPHasher(10, true);
}
/**
* Return our Laravel hashing driver.
*/
private function laravelHasher(): Hasher
{
return app('hash')->driver($this->options['driver']);
}
}
Registering the Driver
Now, we need to tell Laravel that this "migration" driver exists. Open your AppServiceProvider.php or whatever Provider file you'd rather and extend the hashing manager within the boot method:
public function boot(): void
{
app('hash')->extend('legacy', function ($app) {
return new \App\Hashing\LegacyHasher([
'driver' => 'bcrypt', // we'll use bcrypt as our Laravel driver
]);
});
}
And that's it, the final step is to tell our application to use the new migration hashing driver that we've created! We'll add this to our .env file:
HASH_DRIVER=migration
So, how does it work?
Well, I'm glad you asked and it's actually quite simple!
- The check: When a user logs in, our Hasher checks the hash prefix, if it sees
$P$, it validates against the old WordPress logic - The silent upgrade: Because Laravel always checks for a rehash on user login, we force the
needsRehash()method to return true if we're using an old WordPress password. This forces Laravel to take the password the user has entered and rehash it into the more modernbcryptdriver and saves it to the database. - The result: The next time the user logs in, their password has been converted to a Laravel hash and will be covered by the full security that brings. Your users get to enjoy a seamless login experience, and your database becomes more secure with every login.
If this helped you out, please consider subscribing. Thanks!