cult3

Migrating existing users to a new password hashing algorithm

Aug 24, 2015

Table of contents:

  1. How this process is going to work
  2. Migrate the existing legacy credentials
  3. Encapsulate the existing hashing algorithm
  4. Create the Migrator class
  5. Create the Authenticator class
  6. Conclusion

Rebuilding an application from the ground up is definitely a double-edged sword.

On the one hand you get to start from a clean slate and use all the knowledge gained through past mistakes. But on the other, there are a million things that you need to consider.

One of those important things to consider is how to migrate existing user passwords.

If you plan on using the exact same hashing algorithm, everything might be fine.

But it’s often the case that you need to switch to a better hashing algorithm or one that ships with the framework you plan on using.

We don’t want to just wipe everyone’s password because forcing every user to reset their password would be annoying and it might lose you customers.

Instead we need a way to migrate the users from the old hashing algorithm to the new one in a seamless way.

In today’s tutorial I’m going to show you how to do that using a very simple authentication process.

How this process is going to work

Before I get into the code, first I will explain how this is going to work.

We need to migrate the user data to the new database table. This will include the current hashed password.

When a user attempts to sign in for the first time, we will check against the users table by hashing the given password with the new hashing algorithm.

This will fail because the user’s password is currently hashed using the old algorithm and so it won’t match the generated hash.

Next we will check using the old hashing algorithm.

If the credentials are successful, we can hash the given password using the new hashing algorithm and store it against the user’s record in the users table.

When the user attempts to sign in again, the first attempt to the users table will be successful if the password is correct.

This adds extra database queries whenever someone attempts to authenticate with an incorrect password, however it will mean that each user will be gradually migrated across as they log in for the first time.

Migrate the existing legacy credentials

So the first thing we need to do is to migrate the data across.

This is really going to depend on your current situation, so there’s nothing much for me to say.

Basically you just need to create a database table to hold the credentials of your existing users. This is probably going to be username, email or both as well as the hashed password.

Next, create a command line job that you can periodically run to transfer the data from the old database to the new one. This is just for convenience really, to make transferring the data easier.

Encapsulate the existing hashing algorithm

Next we need a way to use the existing algorithm in the new application. If you’re using a legacy application framework this will probably mean you just need to find the method that hashes the password and extract it into it’s own class.

For example, here I’m using the good old PBKDF2:

class PBKDF2
{
/** PBKDF2 Implementation (described in RFC 2898)
 *
 * @param string p password
 * @param string s salt
 * @param int c iteration count (use 1000 or higher)
 * @param int kl derived key length
 * @param string a hash algorithm
 *
 * @return string derived key
 */
public static function generate($p, $s, $c, $kl, $a = 'sha256')
{
    $hl = strlen(hash($a, null, true)); // Hash length
    $kb = ceil($kl / $hl); // Key blocks to compute
    $dk = ""; // Derived key
    // Create key
    for ($block = 1; $block <= $kb; $block++) {
        // Initial hash for this block
        $ib = $b = hash_hmac($a, $s . pack('N', $block), $p, true);
        // Perform block iterations
        for($i = 1; $i < $c; $i ++)
            // XOR each iterate
            $ib ^= ($b = hash_hmac($a, $b, $p, true));
            $dk .= $ib; // Append iterated block
        }
        // Return derived key of correct length
        return base64_encode(substr($dk, 0, $kl));
    }
}

Create the Migrator class

Next we can create the Migrator class. This class will be responsible for checking against the users table for an existing match.

In this example I’m using Laravel, so I can inject an instance of DatabaseManager to speak to the database:

class Migrator
{
    /**
     * @var DatabaseManager
     */
    private $db;

    /**
     * @param DatabaseManager $db
     * @return void
     */
    public function __construct(DatabaseManager $db)
    {
        $this->db = $db;
    }
}

We can also create a MigratorTest class to test this class as we’re writing it:

class MigratorTest extends \TestCase
{
    use DatabaseMigrations;

    /** @var Migrator */
    private $migrator;

    /** @return void */
    public function setUp()
    {
        parent::setUp();

        $this->migrator = new Migrator(app("db"));
    }
}

The first two tests I will write will be for finding a user by their username:

/** @test */
public function should_fail_to_find_user()
{
    $this->assertNull($this->migrator->find('philipbrown'));
}

/** @test */
public function should_find_user_by_username()
{
    $this->fixture('User', ['username' => 'philipbrown']);

    $this->assertInstanceOf('StdClass', $this->migrator->find('philipbrown'));
}

Note, I’m using a custom helper method to create the fixture, this isn’t the default Laravel functionality.

To make these two tests pass we can add the following method:

/**
 * Find the user by their legacy credentials
 *
 * @param string $username
 * @return bool
 */
public function find($identifier)
{
    return $this->db->table('users')
        ->where('username', $identifier)
        ->first();
}

Next I will add two tests for validating the password:

/** @test */
public function should_validate_password()
{
    $new = 'password';
    $existing = 'e/+7RSuHvTHideDuZkpvFXtq65+oHM9xONAsEVJaV6s=';
    $salt = 'abc';

    $this->assertTrue($this->migrator->validate($new, $existing, $salt));
}

/** @test */
public function should_fail_to_validate_password()
{
    $new = 'qwerty';
    $existing = 'e/+7RSuHvTHideDuZkpvFXtq65+oHM9xONAsEVJaV6s=';
    $salt = 'abc';

    $this->assertFalse($this->migrator->validate($new, $existing, $salt));
}

And the method to make these two tests pass:

/**
 * Validate the password against the legacy credentials
 *
 * @param string $new
 * @param string $existing
 @param string $salt
* @return bool
*/
public function validate($new, $existing, $salt)
{
    $hashed = PBKDF2::generate($new, $salt, 1000, 32);

    return $hashed === $existing;
}

Next we need a method to set the credentials on the users table. Here’s the test:

/** @test */
public function should_set_new_credentials()
{
    $user = $this->fixture('User', ['username' => 'philipbrown']);

    $this->migrator->set('philipbrown', 'password');

    $updated = DB::table('users')->where('username', 'philipbrown')->first();

    $this->assertTrue($user->password != $updated->password);
    $this->assertTrue(Hash::check('password', $updated->password));
}

And the method looks like this:

/**
 * Set the user's password
 *
 * @param string $identifier
 * @param string $password
 * @return void
 */
public function set($identifier, $password)
{
    $hashed = Hash::make($password);

    $this->db->table('users')
        ->where('username', $identifier)
        ->update(['password' => $hashed]);
}

Finally we can encapsulate this process in a method called attempt() which will be the public API for this class.

We can assert that the process is working correctly with the following three tests:

/** @test */
public function should_return_null_on_attempt_without_legacy_credentials()
{
    $this->fixture('User', ['username' => 'philipbrown']);

    $this->assertNull($this->migrator->attempt('philipbrown', 'password'));
}

/** @test */
public function should_return_null_on_attempt_with_invalid_password()
{
    $this->fixture('User', ['username' => 'philipbrown']);

    $this->assertNull($this->migrator->attempt('philipbrown', 'qwerty'));
}

/** @test */
public function should_set_password_and_remove_existing_legacy_credentials()
{
    $this->fixture('User', ['username' => 'philipbrown', 'password' => PBKDF2::generate('password')]);

    $this->assertInstanceOf('stdClass', $this->migrator->attempt('philipbrown', 'password'));

    $updated = DB::table('users')->where('username', 'philipbrown')->first();

    $this->assertTrue(Hash::check('password', $updated->password));
}

And the attempt() method looks like this:

/**
 * Attempt to authenticate the user
 *
 * @param string $identifier
 * @param string $password
 * @return User
 */
public function attempt($identifier, $password)
{
    $user = $this->find($identifier);

    if (is_null($user)) return;

    if (! $this->validate($password, $user->password)) return;

    $this->set($identifier, $password);

    return $user;
}

Create the Authenticator class

Next we need to create the Authenticator class. This class will be what you use in your application code to authenticate requests:

class Authenticator
{
    /**
     * @var DatabaseManager
     */
    private $db;

    /**
     * @var Migrator
     */
    private $migrator;

    /**
     * @var DatabaseManager $db
     * @var Migrator $migrator
     * @return void
     */
    public function __construct(DatabaseManager $db, Migrator $migrator)
    {
        $this->db = $db;
        $this->migrator = $migrator;
    }
}

This class should be injected with the DatabaseManager as well as the Migrator class from the last section. Once all of your users have been migrated to the new hashing algorithm you can simple drop this class dependency without having to touch your application authentication code.

At this point we can also create a test class:

class AuthenticatorTest extends \TestCase
{
    use DatabaseMigrations;

    /** @var Authenticator */
    private $authenticator;

    /** @return void */
    public function setUp()
    {
        parent::setUp();

        $this->authenticator = new Authenticator(
            app("db"),
            new Migrator(app("db"))
        );
    }
}

First we can assert that null is returned with invalid credentials:

/** @test */
public function should_return_null_with_invalid_credentials()
{
    $this->fixture('User', ['username' => 'philipbrown']);

    $this->assertNull($this->authenticator->attempt('philipbrown', 'password'));
}

Next we can write a test to ensure that the user is returned with valid new credentials:

/** @test */
public function should_return_user_with_valid_credentials()
{
    $this->fixture('User', ['username' => 'philipbrown', 'password' => Hash::make('password')]);

    $this->assertInstanceOf('stdClass', $this->authenticator->attempt('philipbrown', 'password'));
}

And finally we can write a test to ensure that a user is returned with valid legacy credentials:

/** @test */
public function should_return_user_with_valid_legacy_credentials()
{
    $this->fixture('User', ['username' => 'philipbrown', PBKDF2::generate('password')]);

    $this->assertInstanceOf('stdClass', $this->authenticator->attempt('philipbrown', 'password'));
}

Next we can create the methods on the Authenticator class to make these tests pass.

First we have the main public attempt() method:

/**
 * Attempt to authenticate
 *
 * @param string $identifier
 * @param string $password
 * @return User
 */
public function attempt($identifier, $password)
{
    if (! $this->isValid($identifier, $password)) return;

    $user = $this->users($identifier);

    if (is_null($user)) return;

    if ($this->isValidPassword($password, $user->password)) return $user;

    if ($this->isLegacyCredentials($identifier, $password)) return $user;
}

First up we have a method just to make sure the identifier and password aren’t empty:

/**
 * Check to see if the credentials is valid
 *
 * @param string $identifier
 * @param string $password
 * @return bool
 */
private function isValid($identifier, $password)
{
    return ! empty($identifier) && ! empty($password);
}

Next we have a method to search the users table for the user with the given identifier:

/**
 * Find a user by their identifier
 *
 * @param string $identifier
 * @return StdClass
 */
private function users($identifier)
{
    return $this->db->table('users')
        ->where('username', $identifier)
        ->first();
}

Next we have a method to check if the password is correct:

/**
 * Check to see if the password is valid
 *
 * @param string $password
 * @param string $hashed
 * @return bool
 */
private function isValidPassword($password, $hashed)
{
    return Hash::check($password, $hashed);
}

And finally, we have a method that delegates to the Migrator class to check for legacy credentials:

/**
 * Check to see if the legacy credentials are valid
 *
 * @param string $identifier
 * @param string $password
 * @return bool
 */
private function isLegacyCredentials($identifier, $password)
{
    return $this->migrator->attempt($identifier, $password);
}

Conclusion

Starting from a clean slate can be like a breath of fresh air. However, there are important things like migrating passwords that you need to consider.

In this tutorial we have encapsulated the process of migrating legacy passwords into it’s own class.

This class can then be injected into the main authentication class so that the responsibility for migrating passwords isn’t lumped on the authentication class.

By injecting the migrator class, we can remove this legacy cruft at some point in the future without having to touch the authentication code.

The technique of encapsulating a legacy process, and then injecting it into the new service can be used for a lot of different situations.

Hopefully this tutorial has given you inspiration for how to deal with the big rebuild and how to transfer to new processes.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.