Written by BurinaTec
Category:
Coding
A seamless migration of MD5 passwords from your old web applicatin to Laravel
Recently, we decided to retire some old web application and replace it with a brand new one, based on Laravel. The new app was finally ready, and the only thing left for me to do before deployment was to migrate the existing MySQL data to a new, differently structured database. The old app stored users' passwords as a MD5 hash of username:realm:password
for HTTP digest authentication, which we also decided to replace with Laravel authentication mechanism and encryption format. What and how was originally hashed doesn't really matter for this article, as long as you have the original hashing/checking method you can implement in your new code.
So, here's the final plan: the existing MD5 passwords will be copied to a new users table as they are. The new Laravel application will allow logins using the old credentials (MD5 passwords), but on first login of each user the password will be re-hashed using bcrypt()
. This way, all the passwords will eventually be seamlessly migrated to a new format, without anyone ever noticing.
Since the default user provider in Laravel is Eloquent, and the plan is to use it after all the passwords have been rehashed, we will extend EloquentUserProvider
, override validateCredentials()
method and add a few of our own.
First we will create a custom user provider. Create the file app/Providers/CustomUserProvider.php
with the following content:
<?php
namespace App\Providers;
use App\Lib\Traits\Log\CustomLogger;
use App\User;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Log;
class CustomUserProvider extends EloquentUserProvider
{
/**
* Create a new database user provider.
*
* @param \Illuminate\Contracts\Hashing\Hasher $hasher
* @param string $model
*
* @return void
*/
public function __construct(HasherContract $hasher, $model)
{
parent::__construct($hasher, $model);
}
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials
*
* @return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
if ($this->isMD5($user->getAuthPassword())) {
if ($userModel = User::find($user->getAuthIdentifier())) {
if ($this->checkMD5Password($plain, $userModel)) {
$userModel->password = bcrypt($plain);
$userModel->save();
Log::debug('MD5 password for user ' . $userModel->username
. ' (' . $userModel->name . ') successfully rehashed.');
return true;
}
}
}
return $this->hasher->check($plain, $user->getAuthPassword());
}
/**
* Check if the given string looks like a MD5 hash
*
* @param $password
*
* @return false|int
*/
private function isMD5($password)
{
return preg_match('/^[a-f0-9]{32}$/', $password);
}
/**
* Custom method for checking passwords for our former digest authentication.
*
* @param $plain
* @param User $user
*
* @return bool
*/
private function checkMD5Password($plain, User $user)
{
// If the old password was MD5-hashed alone, then the following would suffice:
// return $user->password === md5($plain);
// Our case: username:realm:password
$hash = md5($user->username . ':' . config('app.old_realm') . ':' . $plain);
return $hash === $user->password;
}
}
If you take a look at the orignal validateCredentials()
method, you will notice we have added some code to check whether the existing stored password looks like it's MD5-hashed or not. If it does, the old app-style check is performed and on success user's password gets rehashed and stored. After that, the login can proceed as usual. If the stored password is already bcrypt()-ed, the original Eloquent code takes over and processes the login.
Depending on the original password format in your database, you need to adapt the checkMD5Password()
method according to your needs. Look at the comments in there, it could be just that easy.
Next, we need to register our new user provider with Laravel app. Edit app/Providers/AuthServiceProvider.php
and make the boot()
method look like this:
public function boot()
{
$this->registerPolicies();
Auth::provider('custom-provider', function () {
return new CustomUserProvider($this->app['hash'], \App\User::class);
});
}
Finally, edit config/auth.php
and change the user provider:
'providers' => [
'users' => [
// 'driver' => 'eloquent',
'driver' => 'custom-provider',
'model' => App\User::class,
],
],
That's all there is to it, your new Laravel application is now ready for production.