The Goal
We recently built an application in Laravel that was previously hosted on WordPress and had several hundred users. We wanted to avoid forcing users to change their passwords for the new system.
Developing a Solution
Research on this issue quickly revealed a small package by Mike McLin that allowed us to accomplish this task; however, the instructions are geared towards Laravel 4-7. Closer inspection of the src folder revealed that the entire package is just a simple facade utilizing the phpass hashing framework, which is used by modern versions of WordPress.
Essentially, our outlined solution would be as follows:
The user attempts to log in, and Laravel’s selected hashing algorithm accepts the password:
The user is logged in as usual.
The User attempts to log in, and the password is rejected:
- Check if it is acceptable via WordPress’s hashing algorithm.
- If not, continue the normal flow, letting the user know their password has been denied.
If the user’s password is acceptable via the WordPress hashing algorithm:
- Hash and store the password following Laravel’s selected hashing algorithm.
- Log the user in as usual.
—
There are two nice things about this approach:
- The user is not impacted in any way. The password is quietly updated to a password using Laravel’s selected hashing algorithm when the user logs in successfully for the first time.
- Because WordPress passwords have a consistent starting string (i.e., $P$B), we can store the passwords in the password column of our User model table just like the Laravel passwords. Our solution presumes you will import the user’s WordPress passwords directly into the User’s model table.
Implementing our Tests and Code
We utilized the following tests to make sure our final code works as expected:
Tests
public function test_user_can_login_with_wordpress_password(): void { $user = User::factory()->create(); DB::table('users')->where('id', $user->id)->update([ 'password' => '$P$BqjSTLtx9DWMvRj0Kv3Rvr/xhHAGwv1' ]); $plain_text_password = 'KNOWN_CORRECT_PASSWORD_FOR_ABOVE_HASH'; $response = $this->post('/login', [ 'email' => $user->email, 'password' => $plain_text_password, ]); $response->assertStatus(302); $response->assertSessionDoesntHaveErrors(); $this->assertAuthenticated(); } public function test_user_cannot_login_with_bad_wordpress_password() { $user = User::factory()->create(); DB::table('users')->where('id', $user->id)->update([ 'password' => '$P$BqjSTLtx9DWMvRj0Kv3Rvr/xhHAGwv1' ]); // Bad password by one letter. $plain_text_password = 'INCORRECT_PASSWORD_FOR_ABOVE_HASH'; $response = $this->post('/login', [ 'email' => $user->email, 'password' => $plain_text_password, ]); $response->assertStatus(302); $response->assertSessionHasErrors('email'); $this->assertGuest(); } public function test_user_can_still_login_with_laravel_creds() { $user = User::factory()->create([ 'password' => 'Test_Password_2025' ]); $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'Test_Password_2025', ]); $response->assertStatus(302); $response->assertSessionDoesntHaveErrors(); $this->assertAuthenticated(); }
Code
Finally, here’s how we executed our implementation – your implementation may vary depending upon your Authentication procedures (we’re using Laravel Breeze)
1) Install Mike McLin’s Composer Package
composer require mikemclin/laravel-wp-password
2) Register the Provider in app/bootstrap/providers.php
use MikeMcLin\WpPassword\WpPasswordProvider; return [ App\Providers\AppServiceProvider::class, ... WpPasswordProvider::class ];
3) Intercept the login process by tapping into the Illuminate\Auth\Events\Attempting
Event
Initially, we were going to listen to the Illuminate\Auth\Events\Failed
event, but a runtime exception is thrown when checking a WordPress password in the Laravel database because it does not conform to the Bcrypt hashing algorithm, and the Failed event is never dispatched.
So, instead, we used the Illuminate\Auth\Events\Attempting
event, which is dispatched at the beginning of Laravel’s authentication flow, and did the following:
- Check to see if the User exists and uses a WordPress password. (if either is false, we abort and let Laravel do its thing)
- If the user exists and the WordPress password is valid, we immediately hash and update the user’s entered password, allowing the authentication process to continue.
- If the user exists but the WordPress password is invalid, we throw a Validation Exception.
Our completed listener looks like the following:
<?php namespace App\Listeners; use App\Models\User; use Illuminate\Auth\Events\Attempting; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; use MikeMcLin\WpPassword\Facades\WpPassword; class CheckAndUpdateWordPressPassword { /** * Create the event listener. */ public function __construct() { // } public function handle(Attempting $event): void { $credentials = $event->credentials; // Check if User Exists; $user = User::where('email', $credentials['email'])->first(); if (!$user) { return; } // If not WordPress password, carry on. if (!str_starts_with($user->password, '$P$')) { return; } // Check if password is correct $valid = WpPassword::check($credentials['password'], $user->password); if (!$valid) { throw ValidationException::withMessages([ 'email' => trans('auth.failed'), ]); } // If Valid, convert to Laravel Hash and allow process to continue. $user->update ([ 'password' => Hash::make($credentials['password']) ]); } }
And that’s it! We hope this helps. If you have another way of approaching this problem that you’d like to share or need help with in a similar situation, we’d love to hear from you.