Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Password encoders -> password_hashers & debug:firewall

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

By converting to the new security system, our deprecations just went way down. If you look at what's left, one of them says:

The child node "encoders" at path "security" is deprecated, use "password_hashers" instead.

This is another change that we saw when upgrading the security-bundle recipe. Originally, we had encoders. This tells Symfony which algorithm to use to hash passwords. This has been renamed to password_hashers. And instead of needing our custom class, we can always just use this config. This says:

Any class that that implements PasswordAuthenticatedUserInterface should use the auto algorithm.

security:
... lines 2 - 11
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
... lines 14 - 63

And since... every user class with a password needs to implement this - including our class - that covers us.

Oh, but if you had a different algorithm before, move that down to this line. We don't want to change the algorithm: we just want to delete encoders in favor of password_hashers.

Now, on the homepage... we have even less deprecations! Two left! Let's try to log in. Ah! I think I missed some conflicts in my base layout earlier.

Let's swing over and fix these. In templates/base.html.twig... yep. When we upgraded the twig-bundle recipe, this conflicted and I didn't even notice! Shame on me!

Now... much better. Let's log in: we have a user called abraca_admin@example.com with password tada. Sign in and... it's alive!

The debug:firewall Command

Speaking of "security" and "firewalls" and other nerdery, Symfony ships with a new command to help debug and visualize your firewall. It's called, appropriately, debug:firewall. If you run it with no arguments:

php bin/console debug:firewall

It'll tell you your firewall names: dev and main. Re-run this with main:

php bin/console debug:firewall main

Here we go! This tells us what authenticators this firewall has, which user provider it's using - though our app usually only has one - and also the entry point, which is something we talk about in our Security tutorial.

Ok, put a big ol' check mark next to "Upgrade Security". Next, let's crush the last few deprecations and learn how we can be sure that we didn't miss any.

Leave a comment!

6
Login or Register to join the conversation
Artur-M Avatar
Artur-M Avatar Artur-M | posted 1 month ago | edited

Hello,
I upgraded my project to Symfony 6 and new authentication system using this course. For sure It works, but I have only one concern and cannot make it. In my old guard authenticator system (coming from Symfony 4.3) password are hashed using argon2id (those start with eg.: $argon2id$v=19$m=65536,t=4,p=1$pXf....) I am not sure if that hashing algorithm is good to use now, but I understood from your course and Symfony manual pages that current bcrypt (so 'auto' here) is better. So I tried to make it automatically migrated.
To my User class I added implementing PasswordUpgraderInterface and upgrade password method:

    public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
    {
        $this->setPassword($newHashedPassword);
    }

I set in security.yaml (I am totally not sure which one I should set in migrate_from key, so I set many :-) - but also tried each single):

enable_authenticator_manager: true
    password_hashers:
        # auto hasher with custom options for all PasswordAuthenticatedUserInterface instances
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
            algorithm: 'auto'
            cost: 16
            migrate_from:
                - sodium
                - argon2
                - argon2i
                - argon2d
                - argon2id

Anyway, it does not seem to work. Newly created accounts have new password hashes (eg. starting with: $2y$13$T7h6DMcZsEyE2LAuK3l...), but when logging onto account which was created before this change password hash is not upgraded to new one. Although I tried to use manual from: https://symfony.com/doc/current/security/passwords.html#upgrade-the-password, I clearly see I messed something :-).
Would be great if you could hint me something.
Best
Artur

Reply

Hey Artur!

Sorry for the slow reply! Yes, this is complex stuff! The first question I'm wondering is (and I'm not sure of the answer): is bcrypt actually considered that needs to be rehashed or not? It may not be the preferred hashing algorithm, but it's quite likely it's still considered good enough (that may change in the future at some point, but then Symfony WOULD start re-hashing it automatically at that time).

If I'm reading the code correctly, here is how to check:

When you log in via a user that users bcrypt, this method should be called: https://github.com/symfony/symfony/blob/ddaedd28bd2e4fc22b64567753cf934fe9d68c4c/src/Symfony/Component/PasswordHasher/Hasher/NativePasswordHasher.php#L113-L116 - I would add a dd(password_needs_rehash($hashedPassword, $this->algorithm, $this->options);) before the return statement. Then, log in, and see if it hits. And if so, does it return true or not? My guess is that it will return false meaning that the passwords are fine and don't need to be rehashed.

Let me know if you find anything out!

Cheers!

Reply
Artur-M Avatar
Artur-M Avatar Artur-M | weaverryan | posted 28 days ago | edited

Many thanks for your response.
Unfortunately:

NativePasswordHasher.php on line 118:
true

I am not sure if I understand PasswordUpgraderInterface correctly - and what work/logic should be done in this method (upgradePassword). Because I put it in User entity used for security system I thought I can just set newHashedPassword to user entity, I also tried to

public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
    $user->setPassword($newHashedPassword); // instead of $this->setPassword($newHashedPassword);
}

but it does nothing as this method is not called anyway during login.
I only consider how to persist this object as this is already entity. Maybe I should create separate class for PasswordUpgradeInterface implementation?
From what I see in method description:
**

 * Upgrades the hashed password of a user, typically for using a better hash algorithm.
 *
 * This method should persist the new password in the user storage and update the $user object accordingly.
 * Because you don't want your users not being able to log in, this method should be opportunistic:
 * it's fine if it does nothing or if it fails without throwing any exception.
 */
Reply

Hey @Artur-M!

Ok, this gets us much closer then :). So it seems that the problem now is that we know the password should be upgraded, but that the upgradePassword() is not being called. And, I'm sorry, I should have caught this detail earlier, but this method (and the PasswordUpgraderInterface) should not live on the User class but on UserRepository: https://symfony.com/doc/5.2/security/password_migration.html#upgrade-the-password-when-using-doctrine - try moving it there and don't forget to include the ->flush() call.

In case you're curious, or it's helpful, here is the logic that's called during login to trigger this entire flow - https://github.com/symfony/symfony/blob/683eeb3890676e617cb1b5819757b9cc8e6b4871/src/Symfony/Component/Security/Http/EventListener/PasswordMigratingListener.php#L57-L86 - you can add debugging statements there if needed to see what's not working :).

Cheers!

Reply
Artur-M Avatar
Artur-M Avatar Artur-M | weaverryan | posted 25 days ago | edited

Yep, I already spotted that it should live in UserRepository - because after trying many things in User.php class I noted I already have it there, using this course as tutor :-) Anyway, I see this method is not called (when I try to dd or dump inside). So now I tried to check what happens in PasswordUpgradeListener.php and it seems $userLoader and badge here does not have passwordUpgrader properly set so it returns without upgrading password in line 77. First checked with dd that $userLoader is not array, it is Closure so after line 71 I set:
dd($userLoader instanceof \Closure, ((new \ReflectionFunction($userLoader))->getClosureThis()) instanceof PasswordUpgraderInterface);
and get true and false, so it return too quickly...
When I checked that $userLoader is Closure with class LoginFormAuthenticator it seems this should contain upgradePassword method implemented, not in Doctrine Repository. And after implementing it in CustomFormAuthenticator it started work eventually!
Many thanks for help and directing me! I always afraid of touching/modifying source code, but anyway I really never would be able to find this listener and do that investigation without your replies.

Reply

Hey @Artur-M!

GREAT debugging! I'm so happy you got it figured out and working!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.6", // v3.6.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.5
        "doctrine/annotations": "^1.13", // 1.13.2
        "doctrine/dbal": "^3.3", // 3.3.5
        "doctrine/doctrine-bundle": "^2.0", // 2.6.2
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.0", // 2.11.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.10.0
        "knplabs/knp-time-bundle": "^1.18", // v1.18.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.6
        "sentry/sentry-symfony": "^4.0", // 4.2.8
        "stof/doctrine-extensions-bundle": "^1.5", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.7
        "symfony/console": "6.0.*", // v6.0.7
        "symfony/dotenv": "6.0.*", // v6.0.5
        "symfony/flex": "^2.1", // v2.1.7
        "symfony/form": "6.0.*", // v6.0.7
        "symfony/framework-bundle": "6.0.*", // v6.0.7
        "symfony/mailer": "6.0.*", // v6.0.5
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/property-access": "6.0.*", // v6.0.7
        "symfony/property-info": "6.0.*", // v6.0.7
        "symfony/proxy-manager-bridge": "6.0.*", // v6.0.6
        "symfony/routing": "6.0.*", // v6.0.5
        "symfony/runtime": "6.0.*", // v6.0.7
        "symfony/security-bundle": "6.0.*", // v6.0.5
        "symfony/serializer": "6.0.*", // v6.0.7
        "symfony/stopwatch": "6.0.*", // v6.0.5
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-chartjs": "^2.0", // v2.1.0
        "symfony/validator": "6.0.*", // v6.0.7
        "symfony/webpack-encore-bundle": "^1.7", // v1.14.0
        "symfony/yaml": "6.0.*", // v6.0.3
        "symfonycasts/verify-email-bundle": "^1.7", // v1.10.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/string-extra": "^3.3", // v3.3.5
        "twig/twig": "^2.12|^3.0" // v3.3.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.1
        "phpunit/phpunit": "^9.5", // 9.5.20
        "rector/rector": "^0.12.17", // 0.12.20
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/maker-bundle": "^1.15", // v1.38.0
        "symfony/var-dumper": "6.0.*", // v6.0.6
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.6
        "zenstruck/foundry": "^1.16" // v1.18.0
    }
}