How to Migrate a Password Hash

5.0 version

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 the system, the user's password should be rehashed and stored. Symfony provides this functionality when a user is successfully authenticated.

To enable this, make sure you apply the following steps to your application:

  1. Configure a new Encoder Using "migrate_from"
  2. Upgrade the Password
  3. Optionally, Trigger Password Migration From a Custom Encoder

Configure a new Encoder Using "migrate_from"

When configuring a new encoder, you can specify a list of legacy encoders by using the migrate_from option:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    # config/packages/security.yaml
    security:
        # ...
    
        encoders:
            legacy:
                algorithm: sha256
                encode_as_base64: false
                iterations: 1
    
            App\Entity\User:
                # the new encoder, along with its options
                algorithm: sodium
                migrate_from:
                    - bcrypt # uses the "bcrypt" encoder with the default options
                    - legacy # uses the "legacy" encoder 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
    <!-- 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">
    
        <security:config>
            <!-- ... -->
    
            <security:encoder class="legacy"
                algorithm="sha256"
                encode-as-base64="false"
                iterations="1"
            />
    
            <!-- algorithm: the new encoder, along with its options -->
            <security:encoder class="App\Entity\User"
                algorithm="sodium"
            >
                <!-- uses the bcrypt encoder with the default options -->
                <security:migrate-from>bcrypt</security:migrate-from>
    
                <!-- uses the legacy encoder configured above -->
                <security:migrate-from>legacy</security:migrate-from>
            </security:encoder>
        </security:config>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // config/packages/security.php
    $container->loadFromExtension('security', [
        // ...
    
        'encoders' => [
            'legacy' => [
                'algorithm' => 'sha256',
                'encode_as_base64' => false,
                'iterations' => 1,
            ],
    
            'App\Entity\User' => [
                // the new encoder, along with its options
                'algorithm' => 'sodium',
                'migrate_from' => [
                    'bcrypt', // uses the "bcrypt" encoder with the default options
                    'legacy', // uses the "legacy" encoder configured above
                ],
            ],
        ],
    ]);
    

Tip

The auto, native, bcrypt and argon encoders 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 algorithm. It is recommended to use migrate_from instead of hash_algorithm, unless the auto encoder 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. You can enable this 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!

Upgrade the Password when using Doctrine

When using the entity user provider, implement 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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 $newEncodedPassword): void
    {
        // set the new encoded password on the User object
        $user->setPassword($newEncodedPassword);

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

Upgrade the Password when using a Custom User Provider

If you're using a custom user provider, implement the PasswordUpgraderInterface in the user provider:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 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 $newEncodedPassword): void
    {
        // set the new encoded password on the User object
        $user->setPassword($newEncodedPassword);

        // ... store the new password
    }
}

Trigger Password Migration From a Custom Encoder

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/Security/UserProvider.php
namespace App\Security;

// ...
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;

class CustomPasswordEncoder implements PasswordEncoderInterface
{
    // ...

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

        return $hashIsOutdated;
    }
}

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