Upgrading Legacy Passwords With Laravel

I recently began to rebuild a legacy application using Laravel. I soon ran into a problem: the passwords stored in the database were using an old SHA hashing mechanism. I didn't want to bother existing users to enter a new password, so I wanted to easily upgrade the passwords without causing these users any trouble. The problem is that you can't backtrack their passwords, so just rehashing them isn't an option. How do you go about doing this? Let me show you what I came up with.

The Idea

I came up with the following idea: upgrade the password upon login. That way you have the plain old password and you can easily rehash it at that point. I came up with the following process, which I have displayed in the flowchart below:

Upgrading legacy passwords flowchart

As you can see I have added a step in between of the regular login flow. This way the user is never asked for a new password, but the password is 'silently' upgraded to a new hash. So how can we easily intercept the regular login flow when using Laravel's built-in Auth classes? Let's dig in.

Setting Up The Configurations

To make our class a little more extendable, let's utilize Laravel's Config system. We will use the app/config/auth.php configuration file (you can place it elsewhere if you'd like), where we will store the details for the legacy hashing method. Add the following to this config file:

// From app/config/auth.php

'legacy' => array(
    'salt_prefix' => 'your_legacy_salt_prefix',
    'salt_suffix' => 'your_legacy_salf_suffix',
    'hash_method' => 'sha1',
),

You can also directly embed these details into the class we're going to make, however I'd recommend using configuration files for these kinds of things: they greatly improve maintainability.

Now that we have set up these configurations, let's put them to use.

Implementing The Password Upgrader Service

We are going to make a dedicated class that checks if we are dealing with a legacy password, and if so, upgrade them. I would advice against putting this in your controller; your controller should not know about this kind of logic! For the purpose of this post I will be using a namespace called Acme, but you can change this to your own preferred namespace. This namespace will be placed in the app/src/Acme/ directory. For the user model we will be using the User model that is provided with every new Laravel installation. To start, let's create a Services folder where we can place our password upgrader class. In this directory, create a new file called PasswordUpgrader.php.

// From app/src/Acme/Services/PasswordUpgrader.php

<?php

namespace Acme\Services;

class PasswordUpgrader
{

}

Now that we have created an empty class, let's first go over the requirements a bit. What do we want this class to do and which dependencies do these actions require?

  1. The class must be able to check if we are dealing with a legacy hashed password.
  2. The class must be able to hash a plain password using the legacy configurations we just created.
  3. The class must be able to rehash the password using the new hashing method.
  4. The class must be able to update the password on the user model.

Okay, so what do these rules mean for our dependencies? First, to load the configurations we will be needing the config repository class provided by Laravel. Next, we will be needing the hasher provided by Laravel to rehash our legacy passwords. Finally, for strictly requiring a user, we need to type hint the UserInterface. Let's update the empty PasswordUpgrader class to reflect these dependencies:

// From app/src/Acme/Services/PasswordUpgrader.php

<?php

namespace Acme\Services;

use Illuminate\Config\Repository;
use Illuminate\Auth\UserInterface;
use Illuminate\Hashing\HasherInterface;

class PasswordUpgrader
{
    /**
     * @var \Illuminate\Hashing\HasherInterface
     */
    protected $hasher;

    /**
     * @var \Illuminate\Config\Repository
     */
    protected $config;

    /**
     * @param  \Illuminate\Hashing\HasherInterface  $hasher
     * @param  \Illuminate\Config\Repository  $config
     */
    public function __construct(HasherInterface $hasher, Repository $config)
    {
        $this->hasher = $hasher;
        $this->config = $config;
    }
}

Now we are ready to implement the actual functionality of this class. First, let's set up the first version of our functionality. After that, we are going to refactor and clean up the code.

// From app/src/Acme/Services/PasswordUpgrader.php

// ...

class PasswordUpgrader
{
    // ...

    public function checkLegacyPassword(UserInterface $user, $plain, $hashed)
    {
        // First, let's grab our legacy hashing configurations.
        $prefix = $this->config['auth.legacy.salt_prefix'];
        $suffix = $this->config['auth.legacy.salt_suffix'];
        $method = $this->config['auth.legacy.hash_method']; 

        // Next, let's hash the plain password using the legacy method.
        $legacy = hash($method, $prefix . $plain . $suffix);

        // Check if we are dealing with a legacy hashed password.
        if ($legacy === $hashed)
        {
            // We are deadling with a legacy hashed password,
            // so let's update it using the new hashing method
            // and save it on the user model.
            $password = $this->hasher->make($plain);

            // The following can be extracted to a seperate class, but
            // let's leave it here for the purpose of this post.
            $user->update([ 'password' => $password ]);

            // Return true, because the credentials were correct
            // and the password was updated.
            return true;
        }

        // If we are not dealing with a legacy password,
        // the user has most probably provided incorrect
        // credentials, so let's return false.
        return false;
    }
}

While this code works perfectly fine, I think it's a little unreadable. On top of that, some of the lines require comments to describe exactly what those particular lines do. To solve this, we can extract the lines into separate methods that have descriptive names of what's happening. Let's do that now. First, let's extract the legacy hashing:

// From app/src/Acme/Services/PasswordUpgrader.php

// ...

class PasswordUpgrader
{
    // ...

    protected function performLegacyHash($plain)
    {
        $prefix = $this->config['auth.legacy.salt_prefix'];
        $suffix = $this->config['auth.legacy.salt_suffix'];
        $method = $this->config['auth.legacy.hash_method']; 

        return hash($method, $prefix . $plain . $suffix);
    }
}

Next, we can take the comparison of the legacy hash with the current password hash and extract it to a more descriptive method:

// From app/src/Acme/Services/PasswordUpgrader.php

// ...

class PasswordUpgrader
{
    // ...

    protected function isLegacyPassword($plain, $hashed)
    {
        $legacy = $this->performLegacyHash($plain);

        return $legacy === $hashed;
    }
}

And finally, we can extract the rehashing of the password and updating of the user into a separate method that describes the behavior better:

// From app/src/Acme/Services/PasswordUpgrader.php

// ...

class PasswordUpgrader
{
    // ...

    protected function upgradeLegacyPassword(UserInterface $user, $plain)
    {
        $password = $this->hasher->make($plain);

        $user->update([ 'password' => $password ]);
    }
}

All this extracting might seem a bit overkill, but I think it makes the class that much more understandable, as each method clearly describes what is happening in the process. To finish up, let's take a look at what happened to our checkLegacyPassword method after the refactor!

// From app/src/Acme/Services/PasswordUpgrader.php

// ...

class PasswordUpgrader
{
    // ...

    public function checkLegacyPassword(UserInterface $user, $plain, $hashed)
    {
        if ($this->isLegacyPassword($plain, $hashed))
        {
            $this->upgradeLegacyPassword($user, $plain);

            return true;
        }

        return false;
    }
}

There we have it, clean and readable! But how do we integrate this into our login flow? Let's deal with that now.

Extending The Core

While you can call the PasswordUpgrader class from your controller, I think it's better to integrate it directly in the login flow. That way you seperate this logic from the controller and keep them nice and thin. To do this, we have to extend the UserProvider class that comes with Laravel. In this case I will be using the EloquentUserProvider. Let's set up the class in the same directory as the PasswordUpgrader to keep them together:

// From app/src/Acme/Services/UserProvider.php  

<?php

namespace Acme\Services;

use Illuminate\Auth\UserProviderInterface;
use Illuminate\Auth\EloquentUserProvider as LaravelUserProvider; 

class UserProvider extends LaravelUserProvider implements UserProviderInterface
{

}

Here you can see that along with extending the EloquentUserProvider, we also implement the UserProviderInterface provided by Laravel. This is necessary to make it work together nicely with Laravel's Guard class. Now that we have set up the class, we have to override it's constructor. We need to do this to inject the PasswordUpgrader class that we created into the UserProvider class so that we can utilize it. Other than injecting this class, we don't need to make any changes to the constructor. Let's set it up:

// From app/src/Acme/Services/UserProvider.php  

<?php

namespace Acme\Services;

use Illuminate\Auth\UserInterface;
use Illuminate\Hashing\HasherInterface;
use Illuminate\Auth\UserProviderInterface;
use Illuminate\Auth\EloquentUserProvider as LaravelUserProvider; 

class UserProvider extends LaravelUserProvider implements UserProviderInterface
{
    protected $upgrader;

    public function __construct(HasherInterface $hasher, PasswordUpgrader $upgrader, $model)
    {
        $this->hasher = $hasher;
        $this->model = $model;
        $this->upgrader = $upgrader;
    } 
}

As you can see I have also included a use for the UserInterface. This is necessary because we will be using the getAuthPassword on the User model and we need to make sure that this exists. The UserInterface requires this method to be implemented. Looking at the original class provided by Laravel we can determine that we are interested in overriding the validateCredentials method. This is the method that checks the input password against the hashed password in the database. Before we start implementing this, let's look back at the flow chart real quick to see how we want to implement this method. Looking at this chart, we can define the following steps for our method.

  1. Check the input password against the hashed password in the database.
  2. If this password is correct, return true.
  3. If this password is incorrect, return the result from the checkLegacyPassword from the PasswordUpgrader class.

Now that we have defined these steps, let's implement the method:

// From app/src/Acme/Services/UserProvider.php  

<?php

// ...

class UserProvider extends LaravelUserProvider implements UserProviderInterface
{
    // ...

    public function validateCredentials(UserInterface $user, array $credentials)
    {
        // Get the plain password from the user input and 
        // get the hashed password from the user model.
        $plain = $credentials['password'];
        $hashed = $user->getAuthPassword();  

        // Step 1.
        if ($this->hasher->check($plain, $hashed))
        {
            // Step 2.
            return true;
        }
        else
        {
            // Step 3.
            return $this->upgrader->checkLegacyPassword($user, $plain, $hashed);
        }
    } 
}

There we have it, we have now integrated the PasswordUpgrader service into our login flow. To finish up, we have to register our UserProvider with Laravel, so that we can simply use the Auth::attempt() method from our controller! To accomplish this, let's create a service provider and extend the Auth class from there. We need to do this because we have to inject our extended UserProvider class into Laravel's Guard class. This also gives us a place to register the PasswordUpgrader class with the container. Let's start with registering the PasswordUpgrader class:

// From app/src/Acme/Providers/AuthServiceProvider.php

<?php

namespace Acme\Providers;

use Illuminate\Auth\Guard;
use Acme\Services\UserProvider;
use Acme\Services\PasswordUpgrader;
use Illuminate\Support\ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app['auth.upgrader'] = $this->app->share(function ($app)
        {
            // Instantiate the PasswordUpgrader and inject
            // the Hasher and Config classes into it.
            return new PasswordUpgrader($app['hash'], $app['config']);
        });
    }
}

Now that we have register the PasswordUpgrader class we can use the binding to inject it into our UserProvider class more easily. Because we are registering a new driver with Laravel's AuthManager, we have to set this up in the boot method of the ServiceProvider. Let's do that now:

// From app/src/Acme/Providers/AuthServiceProvider.php

<?php

// ...

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app['auth']->extend('legacy', function ($app)
        {
            // Get the model name from the auth config file 
            // file and instantiate a new Hasher and our 
            // PasswordUpgrader from the container.
            $model = $app->config['auth.model'];
            $hash = $app['hash'];
            $upgrader = $app['auth.upgrader'];

            // Instantiate our own UserProvider class.
            $provider = new UserProvider($hash, $upgrader, $model);

            // Return a new Guard instance and pass our
            // UserProvider class into its constructor.
            return new Guard($provider, $app['session.store']);
        });
    } 
}

As you can see I have called the Auth driver legacy. However, you can name this anything you want. To finish up, let's register our service provider and the new Auth driver with Laravel so that we can use it in our code!

Wrapping Up

Open up the app/config/app.php file and add the following line to 'providers' array:

// From app/config/app.php

'providers' => array(

    // ...

    'Acme\Providers\AuthServiceProvider'

),

And finally, tell Laravel to use our legacy Auth driver. You can do this by changing the 'driver' key in the app/config/auth.php file:

// From app/config/auth.php

'driver' => 'legacy',

And that's it. Now whenever you use the Auth::attempt() method from your controller, if that user has a legacy hashed password, it will be automatically updated!