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?
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.
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:
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.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With