Migrating old md5 passwords to bcrypt with Laravel 5.2's built in auth

Viewed 4423

I'm migrating an old PHP app over to Laravel 5.2. The app has a huge users table (about 50K users) and the passwords are all MD5 hashes.

Obviously this is unacceptable but rather than sending out an email to all 50,000 users asking them to reset their passwords, I want to change the passwords to bcrypt hashes behind the scenes.

To do this, I want to create an old_password column with the MD5 hash in it and then whenever a user logs in, I check the password against the MD5 hash (if it exists) and then make a new bcrypt hash for next time, deleting the MD5 hash.

I've seen a few examples about how to do this (such as this and this), but none specifically for Laravel 5 and none specifically for use with Laravel 5.2's built in auth.

Is there a clean way to adapt the built-in auth to do this, or am I better off writing my own manual auth system in this case?

5 Answers

So, in Laravel 5.8 (and probably earlier), I think the best / safest solution is to use an event listener. I cannibalized some code from the other solutions...

Add to your app\Providers\EventServiceProvider.php

    protected $listen = [
...
        'Illuminate\Auth\Events\Attempting' => [
            'App\Listeners\DrupalPasswordUpdate',
        ],
...
    ];

Then create the file app\Listeners\DrupalPasswordUpdate.php

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Attempting;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class DrupalPasswordUpdate
{
    public function handle(Attempting $event)
    {
        $this->check($event->credentials['password'], \App\User::where('email', $event->credentials['email'])->first()->password??'not found');
    }

    public function check($value, $hashedValue, array $options = [])
    {
        if($this->needsRehash($hashedValue))
        {
            if($this->user_check_password($value, $hashedValue))
            {
                $newHashedValue = (new \Illuminate\Hashing\BcryptHasher)->make($value, $options);
                \Illuminate\Support\Facades\DB::update('UPDATE users SET `password` = "'.$newHashedValue.'" WHERE `password` = "'.$hashedValue.'"');
                $hashedValue = $newHashedValue;
            }
        }
    }

    public function needsRehash($hashedValue, array $options = [])
    {
        return substr($hashedValue, 0, 4) != '$2y$';
    }

    // DRUPAL PASSWORD FUNCTIONS
    function user_check_password($password, $stored_hash) {
      $hash = md5($password);
      return ($hash && $stored_hash == $hash);
    }
}

This will listen for an attempt to login, check for the user in the database, and update their password if appropriate, then continue on with the normal login process.

You can also replace the md5() call with a different hashing method, you can probably find in your Drupal installation includes/password.inc

I updated AuthController from @Leonardo Beal to Laravel 5.6.

I migrated my app from Laravel 4.2 to 5.6 and this works like a charm (add it to app/Http/Controllers/Auth/LoginController.php):

/**
 * Handle a login request to the application.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
 *
 * @throws \Illuminate\Validation\ValidationException
 */
public function login(Request $request)
{
    $this->validateLogin($request);

    // If the class is using the ThrottlesLogins trait, we can automatically throttle
    // the login attempts for this application. We'll key this by the username and
    // the IP address of the client making these requests into this application.
    if ($this->hasTooManyLoginAttempts($request)) {
        $this->fireLockoutEvent($request);

        return $this->sendLockoutResponse($request);
    }

    if ($this->attemptLogin($request)) {
        return $this->sendLoginResponse($request);
    }

    //If user got here it means the AUTH was unsuccessful
    //Try to log them IN using MD5
    if ($user = User::whereEmail($request->input('email'))
        ->wherePassword(md5($request->input('password')))->first()) {
        //It this condition is true, the user had the right password.

        //encrypt the password using bcrypt
        $user->password = bcrypt($request->input('password'));
        $user->save();

        $this->validateLogin($request);

        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }

        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }
    }

    // If the login attempt was unsuccessful we will increment the number of attempts
    // to login and redirect the user back to the login form. Of course, when this
    // user surpasses their maximum number of attempts they will get locked out.
    $this->incrementLoginAttempts($request);

    return $this->sendFailedLoginResponse($request);
}
Related