Migrating old md5 passwords to bcrypt with Laravel

2019-03-31 02:57发布

问题:

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?

回答1:

I had a similar problem when migrated from Drupal. I did not make a new column for old passwords, but updated hasher to check the password Drupal-way and then if that fails, check it with bcrypt. This way old users could log in the same ways as new ones.

You will need to create a package anywhere in you app, say in app/packages/hashing. Put these two files there.

YourHashingServiceProvider.php

<?php namespace App\Packages\Hashing;

use Illuminate\Support\ServiceProvider;

class YourHashingServiceProvider extends ServiceProvider {

    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('hash', function() { return new YourHasher; });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return ['hash'];
    }

}

YourHasher.php

<?php namespace App\Packages\Hashing;

use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Hashing\BcryptHasher;
use Auth;

class YourHasher implements HasherContract
{

    protected $hasher;

    /**
     * Create a new Sha512 hasher instance.
     */
    public function __construct()
    {
        $this->hasher = new BcryptHasher;
    }

    /**
     * Hash the given value.
     *
     * @param string $value
     * @param array  $options
     *
     * @return string
     */
    public function make($value, array $options = [])
    {
        return $this->hasher->make($value, $options);
    }

    /**
     * Check the given plain value against a hash.
     *
     * @param  string $value
     * @param  string $hashedValue
     * @param  array  $options
     *
     * @return bool
     */
    public function check($value, $hashedValue, array $options = [])
    {
        return md5($value) == $hashedValue || $this->hasher->check($value, $hashedValue, $options);
    }

    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string $hashedValue
     * @param  array  $options
     *
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = [])
    {
        return substr($hashedValue, 0, 4) != '$2y$';
    }
}

Then put App\Packages\Hashing\YourHashingServiceProvider::class inside providers in your config/app.class. At this point, your old users should be able to log in to your laravel app.

Now, to update their passwords, somewhere in your User controller (login/registration forms) you can use Hash::needsRehash($hashed) and Hash::make($password_value) to generate a fresh bcrypt password for a user and then save it.



回答2:

In Laravel 5.2 your AuthController.php should override the login method, just adding the following.

When the login fails, it tries to login users in using md5().

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.
    $throttles = $this->isUsingThrottlesLoginsTrait();

    if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts($request)) {
        $this->fireLockoutEvent($request);

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

    $credentials = $this->getCredentials($request);


    if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {
        return $this->handleUserWasAuthenticated($request, $throttles);
    }

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

        //encrypt the password using bcrypt
        $user->password     = bcrypt($credentials['password']);
        $user->save();

        if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {
            return $this->handleUserWasAuthenticated($request, $throttles);
        }

        return $this->handleUserWasAuthenticated($request, $throttles);

    }



    // 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.
    if ($throttles && ! $lockedOut) {
        $this->incrementLoginAttempts($request);
    }

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


回答3:

I had a slightly different approach to the solution than @neochief based on an article I read on sustainable password hashing specifically the bottom bit on a meta-algorithm.

I did this in 3 steps:

  1. Applied bcrypt to all the users passwords in the database by wrapping the md5 passwords in bcrypt as if they are plain-text
  2. When a user attempts to authenticate use bcrypt alone using guard->attempt(...). If authentication fails then double encrypt by using md5 on the password sent in the request, and then attempt to re-authenticate using guard->attempt(...), which will then wrap the md5 in bcrypt for comparison.
  3. Once authenticated store the plain-text password using just bcrypt so the double encryption doesn't have to be applied twice to the same user.

I pulled up AuthenticatesUsers::login into AuthController to overwrite the logic with my own and placed a call to a protected method that contained the logic for the login attempts. I'm using JWT-Auth, but if you're not your solution won't be much different.

/**
 * Handle a login request to the application.
 *
 * @param  \Illuminate\Http\Request $request
 * @return \Illuminate\Http\Response
 */
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.
    $throttles = $this->isUsingThrottlesLoginsTrait();

    if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts($request)) {
        $this->fireLockoutEvent($request);

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

    $credentials = $this->getCredentials($request);

    if ($token = $this->authenticate($credentials)) {
        return $this->handleUserWasAuthenticated($request, $throttles, $token);
    }

    // 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.
    if ($throttles && !$lockedOut) {
        $this->incrementLoginAttempts($request);
    }

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

/**
 * Authentication using sustainable password encryption that allows for updates to the
 * applications hash strategy that employs modern security requirements.
 * ---
 * IMPORTANT: The meta-algorithm strategy assumes that all existing passwords that use
 * an obsolete security standard for encryption have been further encrypted with an
 * up-to-date modern security standard.
 * ---
 * NOTE: Mutator has been applied to User model to store any passwords
 * that are saved using a standard for modern encryption.
 *
 * @param $credentials
 * @return string|bool
 */
protected function authenticate($credentials)
{
    // Attempt to authenticate using modern security standards
    $token = Auth::guard($this->getGuard())->attempt($credentials);

    // If the authentication failed, re-attempt using obsolete password encryption
    // to wrap the plain-text password from the request
    if ($token === false) {

        // Make a copy of the plain-text password
        $password = $credentials['password'];

        // Apply obsolete password encryption to plain-text password
        $credentials['password'] = md5($password);

        // Re-attempt authentication
        $token = Auth::guard($this->getGuard())->attempt($credentials);

        if ($token) {

            // Store password using modern security standard
            $user = Auth::user();
            $user->password = $password;
            $user->save();
        }
    }

    return $token;
}

Hope this is useful for someone.



回答4:

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);
}


回答5:

Here's a reworked function for the @neochief answer to update the password:

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

user_check_password is a function to see if the old password is valid.