laravelauthenticationmd5laravel-passport

How to securely migrate MD5 hashed passwords to bcrypt on user's first login with Laravel Passport?


I have a Laravel application where I've recently migrated users from an old system. The passwords in the old system were hashed using MD5.

I've successfully implemented a solution using Laravel Sanctum for local logins. In this solution, I extended EloquentUserProvider, and in the validateCredentials() method, I check the password against MD5. If it's an MD5 hash, I bcrypt it and save it.

However, I'm encountering difficulties when attempting to implement the same functionality with Laravel Passport for API authentication. Despite my attempts to extend PassportUserProvider and PassportServiceProvider, I've been unable to get it working. I'm starting to worry that I may be approaching this problem incorrectly. Could someone provide guidance on how to proceed?

Here's one of my attempts:

<?php
// Path: app/Providers/PassportUserProvider.php

namespace App\Providers;

use Laravel\Passport\PassportUserProvider as BasePassportUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;

use Log;

class PassportUserProvider extends BasePassportUserProvider implements UserProvider
{
    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        Log::build(['driver' => 'single', 'path' => storage_path('logs/MD5_rehashed.log'),])->info('PassportUserProvider Attempting to check MD5.');

        $plain = $credentials['password'];

        // Check if the password is MD5 hashed
        if ($this->isMD5($user->getAuthPassword())) {
            // Retrieve the user model by ID using Laravel's default user provider
            $userModel = $this->retrieveById($user->getAuthIdentifier());

            // If the user exists and MD5 password matches, update the password
            if ($userModel && $this->checkMD5Password($plain, $userModel)) {
                $userModel->password = bcrypt($plain);
                $userModel->save();

                Log::build(['driver' => 'single', 'path' => storage_path('logs/MD5_rehashed.log'),])->info('#' . $userModel->id);

                return true;
            }
        }

        // If password is not MD5 hashed, or MD5 validation fails, fall back to default validation
        return parent::validateCredentials($user, $credentials);
    }

    /**
     * Check if the password is MD5 hashed.
     *
     * @param string $password
     * @return bool
     */
    protected function isMD5($password)
    {
        return preg_match('/^[a-f0-9]{32}$/', $password);
    }

    /**
     * Check if the plain password matches the MD5 hash.
     *
     * @param string $plain
     * @param \Illuminate\Contracts\Auth\Authenticatable $userModel
     * @return bool
     */
    protected function checkMD5Password($plain, $userModel)
    {
        // Implement your MD5 password check logic here
        // Example:
        return md5($plain) === $userModel->getAuthPassword();
    }
}

...binding it in AuthServiceProvider's boot or register like this:

   $this->app->bind(
       \Laravel\Passport\PassportUserProvider::class,
       \App\Providers\PassportUserProvider::class
   );

Solution

  • That easy:

    Customizing the Password Validation

    So in used User model:

        public function validateForPassportPasswordGrant(string $password): bool
    {
        // Check if the stored password is MD5 hashed
        if (preg_match('/^[a-f0-9]{32}$/', $this->password)) {
            $hash = md5($password);
            
            // If the password matches the MD5 hash, bcrypt the new password and update it and return true
            if ($hash === $this->password) {
                $this->password = bcrypt($password);
                $this->save();
                return true;
            } else {
                // If the password does not match the MD5 hash, return false
                return false;
            }
        } else {
            // If the stored password is not MD5 hashed, use default
            return Hash::check($password, $this->password);
        }
    }