How to Migrate a Password Hash

5.3 version
Symfony 5.3 is backed by JoliCode.

How to Migrate a Password Hash

In order to protect passwords, it is recommended to store them using the latest hash algorithms. This means that if a better hash algorithm is supported on your system, the user’s password should be rehashed using the newer algorithm and stored. That’s possible with the migrate_from option:

  1. Configure a new Hasher Using “migrate_from”
  2. Upgrade the Password
  3. Optionally, Trigger Password Migration From a Custom Hasher

Configure a new Hasher Using “migrate_from”

When a better hashing algorithm becomes available, you should keep the existing hasher(s), rename it, and then define the new one. Set the migrate_from option on the new hasher to point to the old, legacy hasher(s):

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    # config/packages/security.yaml
    security:
        # ...
    
        password_hashers:
            # a hasher used in the past for some users
            legacy:
                algorithm: sha256
                encode_as_base64: false
                iterations: 1
    
            App\Entity\User:
                # the new hasher, along with its options
                algorithm: sodium
                migrate_from:
                    - bcrypt # uses the "bcrypt" hasher with the default options
                    - legacy # uses the "legacy" hasher configured above
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:security="http://symfony.com/schema/dic/security"
        xsi:schemaLocation="http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd
            http://symfony.com/schema/dic/security
            https://symfony.com/schema/dic/security/security-1.0.xsd">
    
        <security:config>
            <!-- ... -->
    
            <security:password-hasher class="legacy"
                algorithm="sha256"
                encode-as-base64="false"
                iterations="1"
            />
    
            <!-- algorithm: the new hasher, along with its options -->
            <security:password-hasher class="App\Entity\User"
                algorithm="sodium"
            >
                <!-- uses the bcrypt hasher with the default options -->
                <security:migrate-from>bcrypt</security:migrate-from>
    
                <!-- uses the legacy hasher configured above -->
                <security:migrate-from>legacy</security:migrate-from>
            </security:password-hasher>
        </security:config>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // config/packages/security.php
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        // ...
        $security->passwordHasher('legacy')
            ->algorithm('sha256')
            ->encodeAsBase64(true)
            ->iterations(1)
        ;
    
        $security->passwordHasher('App\Entity\User')
            // the new hasher, along with its options
            ->algorithm('sodium')
            ->migrateFrom([
                'bcrypt', // uses the "bcrypt" hasher with the default options
                'legacy', // uses the "legacy" hasher configured above
            ])
        ;
    };
    

With this setup:

  • New users will be hashed with the new algorithm;
  • Whenever a user logs in whose password is still stored using the old algorithm, Symfony will verify the password with the old algorithm and then rehash and update the password using the new algorithm.

Tip

The auto, native, bcrypt and argon hashers automatically enable password migration using the following list of migrate_from algorithms:

  1. PBKDF2 (which uses hash_pbkdf2);
  2. Message digest (which uses hash)

Both use the hash_algorithm setting as the algorithm. It is recommended to use migrate_from instead of hash_algorithm, unless the auto hasher is used.

Upgrade the Password

Upon successful login, the Security system checks whether a better algorithm is available to hash the user’s password. If it is, it’ll hash the correct password using the new hash. If you use a Guard authenticator, you first need to provide the original password to the Security system.

You can enable the upgrade behavior by implementing how this newly hashed password should be stored:

After this, you’re done and passwords are always hashed as secure as possible!

Provide the Password when using Guard

When you’re using a custom guard authenticator, you need to implement Symfony\Component\Security\Guard\PasswordAuthenticatedInterface. This interface defines a getPassword() method that returns the password for this login request. This password is used in the migration process:

// src/Security/CustomAuthenticator.php
namespace App\Security;

use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
// ...

class CustomAuthenticator extends AbstractGuardAuthenticator implements PasswordAuthenticatedInterface
{
    // ...

    public function getPassword($credentials): ?string
    {
        return $credentials['password'];
    }
}

Upgrade the Password when using Doctrine

When using the entity user provider, implement Symfony\Component\Security\Core\User\PasswordUpgraderInterface in the UserRepository (see the Doctrine docs for information on how to create this class if it’s not already created). This interface implements storing the newly created password hash:

// src/Repository/UserRepository.php
namespace App\Repository;

// ...
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

class UserRepository extends EntityRepository implements PasswordUpgraderInterface
{
    // ...

    public function upgradePassword(UserInterface $user, string $newHashedPassword): void
    {
        // set the new hashed password on the User object
        $user->setPassword($newHashedPassword);

        // execute the queries on the database
        $this->getEntityManager()->flush();
    }
}

Upgrade the Password when using a Custom User Provider

If you’re using a custom user provider, implement the Symfony\Component\Security\Core\User\PasswordUpgraderInterface in the user provider:

// src/Security/UserProvider.php
namespace App\Security;

// ...
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
    // ...

    public function upgradePassword(UserInterface $user, string $newHashedPassword): void
    {
        // set the new hashed password on the User object
        $user->setPassword($newHashedPassword);

        // ... store the new password
    }
}

Trigger Password Migration From a Custom Hasher

If you’re using a custom password hasher, you can trigger the password migration by returning true in the needsRehash() method:

// src/Security/CustomPasswordHasher.php
namespace App\Security;

// ...
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class CustomPasswordHasher implements UserPasswordHasherInterface
{
    // ...

    public function needsRehash(string $hashed): bool
    {
        // check whether the current password is hashed using an outdated hasher
        $hashIsOutdated = ...;

        return $hashIsOutdated;
    }
}

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.