Hashing Plain Passwords & PasswordCredentials
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe process of saving a user's password always looks like this: start with a plain-text password, hash that, then save the hashed version onto the User. This is something we're going to do in the fixtures... but we'll also do this on a registration form later... and you would also need it on a change password form.
Adding a plainPassword Field
To make this easier, I'm going to do something optional. In User, up on top, add a new private $plainPassword property:
| // ... lines 1 - 12 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 15 - 41 | |
| private $plainPassword; | |
| // ... lines 43 - 154 | |
| } |
The key thing is that this property will not be persisted to the database: it's just a temporary property that we can use during, for example, registration, to store the plain password.
Below, I'll go to "Code"->"Generate" - or Command+N on a Mac - to generate the getter and setter for this. The getter will return a nullable string:
| // ... lines 1 - 12 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 15 - 143 | |
| public function getPlainPassword(): ?string | |
| { | |
| return $this->plainPassword; | |
| } | |
| public function setPlainPassword(string $plainPassword): self | |
| { | |
| $this->plainPassword = $plainPassword; | |
| return $this; | |
| } | |
| } |
Now, if you do have a plainPassword property, you'll want to find eraseCredentials() and set $this->plainPassword to null:
| // ... lines 1 - 12 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 15 - 118 | |
| public function eraseCredentials() | |
| { | |
| // If you store any temporary, sensitive data on the user, clear it here | |
| $this->plainPassword = null; | |
| } | |
| // ... lines 124 - 154 | |
| } |
This... is not really that important. After authentication is successful, Symfony calls eraseCredentials(). It's... just a way for you to "clear out any sensitive information" on your User object once authentication is done. Technically we will never set plainPassword during authentication... so it doesn't matter. But, again, it's a safe thing to do.
Hashing the Password in the Fixtures
Back inside UserFactory, instead of setting the password property, set plainPassword to "tada":
| // ... lines 1 - 28 | |
| final class UserFactory extends ModelFactory | |
| { | |
| // ... lines 31 - 37 | |
| protected function getDefaults(): array | |
| { | |
| return [ | |
| // ... lines 41 - 42 | |
| 'plainPassword' => 'tada', | |
| ]; | |
| } | |
| // ... lines 46 - 58 | |
| } |
If we just stopped now, it would set this property... but then the password property would stay null... and it would explode in the database because that column is required.
So after Foundry has finished instantiating the object, we need to run some extra code that reads the plainPassword and hashes it. We can do that down here in the initialize() method... via an "after instantiation" hook:
| // ... lines 1 - 28 | |
| final class UserFactory extends ModelFactory | |
| { | |
| // ... lines 31 - 46 | |
| protected function initialize(): self | |
| { | |
| // see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization | |
| return $this | |
| // ->afterInstantiate(function(User $user) {}) | |
| ; | |
| } | |
| // ... lines 54 - 58 | |
| } |
This is pretty cool: call $this->afterInstantiate(), pass it a callback and, inside say if $user->getPlainPassword() - just in case we override that to null - then $user->setPassword(). Generate the hash with $this->passwordHasher->hashPassword() passing the user that we're trying to hash - so $user - and then whatever the plain password is: $user->getPlainPassword():
| // ... lines 1 - 29 | |
| final class UserFactory extends ModelFactory | |
| { | |
| // ... lines 32 - 49 | |
| protected function initialize(): self | |
| { | |
| // see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization | |
| return $this | |
| ->afterInstantiate(function(User $user) { | |
| if ($user->getPlainPassword()) { | |
| $user->setPassword( | |
| $this->passwordHasher->hashPassword($user, $user->getPlainPassword()) | |
| ); | |
| } | |
| }) | |
| ; | |
| } | |
| // ... lines 63 - 67 | |
| } |
Done! Let's try this. Find your terminal and run:
symfony console doctrine:fixtures:load
This will take a bit longer than before because hashing passwords is actually CPU intensive. But... it works! Check the user table:
symfony console doctrine:query:sql 'SELECT * FROM user'
And... got it! Every user has a hashed version of the password!
Validating the Password: PasswordCredentials
Finally we're ready to check the user's password inside our authenticator. To do this, we need to hash the submitted plain password then safely compare that with the hash in the database.
Well we don't need to do this... because Symfony is going to do it automatically. Check it out: replace CustomCredentials with a new PasswordCredentials and pass it the plain-text submitted password:
| // ... lines 1 - 17 | |
| use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | |
| // ... lines 19 - 21 | |
| class LoginFormAuthenticator extends AbstractAuthenticator | |
| { | |
| // ... lines 24 - 37 | |
| public function authenticate(Request $request): PassportInterface | |
| { | |
| // ... lines 40 - 42 | |
| return new Passport( | |
| // ... lines 44 - 53 | |
| new PasswordCredentials($password) | |
| ); | |
| } | |
| // ... lines 57 - 83 | |
| } |
That's it! Try it. Log in using our real user - abraca_admin@example.com - I'll copy that, then some wrong password. Nice! Invalid password! Now enter the real password tada. It works!
That's awesome! When you put a PasswordCredentials inside your Passport, Symfony automatically uses that to compare the submitted password to the hashed password of the user in the database. I love that.
This is all possible thanks to a powerful event listener system inside of security. Let's learn more about that next and see how we can leverage it to add CSRF protection to our login form... with about two lines of code.
21 Comments
I am refreshing my konwledge and when run fixture load it shows errror:
No password hasher has been configured for account "App\Entity\User".In scurity.yaml when I added
App\Entity\User: 'auto'in password_hashers section before
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'It works but with code in tutorial it works without App\Entity\User: 'auto'
Hey @kakhaber
What Symfony version are you using? Does your user entity implement the
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterfaceinterface?Cheers!
Hey there, The course is just magnificent.
Just one question in mind, why are we starting from login and going to register, were not it be easy to start from register and after login from the register credentials.
Thanks!
Hey Arman,
I'm not sure I completely understand you here. If you ask why we show the login feature first and after the registration feature instead of showing it in reverse order - well, that's not very important actually, but the login feature is easier because we use some data from the fixtures. Registration is a bit more complex, and it will require knowing how the login feature works eventually, so once again it's a good idea to meet with the login feature first. Last but not least, actually, the registration feature is not required for some websites, I mean, having a user registration on your website isn't a rule but is mostly an optional feature. There might be some websites where users just access the content without being able to authorize themselves because it's not necessary.
But your question is also valid, and showing it in the reverse order is kinda OK too. But as I said, it's not that important, and it was already recorded this way by the course author. I hope this answers your question :)
Cheers!
Yeah you got my question right, and now I understand you perfectly.
Thanks very much.
I am kind of frustrated because with this lesson I am not quite sure of what to do when processing an actual password that is submitted in a registration form.
Assuming that
$plaintextPasswordis the plain password string, can I use the password setter in theUserobject$useras follows:(this is from the doc). I assume, that, from the security yaml file,
hashPasswordknows it must use the email in$user; right?Hey @Francois
My apologies for the bad feeling, that's not the experience we want our users to have. After hashing the user password you can set it on the object, basically as you please, usually via a setter method.
About the hasher using the email as part of the password. It does not do that, the only field it gets from the user object is the
saltin case your app supports it, but it is not recommended when using modern hashing algorithmsI hope I managed to clarify things. Cheers!
Hi All,
I followed the tutorial but I can't connect via my email / tada
I'm on Symfony 5.3
LoginFormAuthenticator.php on line 83:<br />Symfony\Component\Security\Core\Exception\BadCredentialsException {#589 â–¼<br /> #message: "The presented password is invalid."<br /> #code: 0<br /> #file: "/home/amine/Projets/Symfony/Symfony6/symfony-doctrine-formation/vendor/symfony/security-http/EventListener/CheckCredentialsListener.php"<br /> #line: 74<br /> -token: nullBest,
Amine
I foun the prob.
My methode getPAssword() in USer entity return null
Good night :)
Hey Amine,
I'm happy to hear you were able to find the problem yourself, well done! And thanks for sharing your solution with others ;)
Cheers!
Another question =) I thought the new hashing system always used sodium, and those hashed passwords always started with "$argon". If I use the
symfony console security:encode-passwordindeed passwords do, but when I use the password hasherI get a completely different string.
Both passwords work, just curious =)
Hey @MattWelander!
Hmm, that's interesting! The point of the "auto" is that you no longer need to really care what algorithm is being used, because it'll choose whatever the latest and greatest is. That being said, I would definitely expect that
security:encode-passwordand using theUserPasswordHasherInterfaceservice in PHP would use the same algorithm. The algorithm is chosen, iirc, entirely based on theUserclass (well, theUserclass is used to look up your hasher config - so basically, it's chosen based on your hasher config, which does not change between php and that console command). So I also would expect to see$argonstyle hashed passwords in both cases. When I just checked Symfonycasts, indeed, that IS what I see: both ways give me$argon2style hashed password.So... that's a mystery why doing that in PHP would result in a non-argon hasher being used - I can't explain that...
Cheers!
Hi!
Prior to sym4 (I believe) the config security - firewalls - main - form_login would make it so that any request to a page that required authentication automatically redirected to the login page.
I find that in sym5 instead the user gets an access denied exception. What would be the equivalent config to auto-redirect to the login page?
Nevermind - that question is answered a few lessons down the line =) https://symfonycasts.com/screencast/symfony-security/entry-point
This command does not work with PostgeSQL
symfony console doctrine:query:sql "SELECT * FROM user"returned just
`
user
postgres
`
Solved - need to add -->"
symfony console doctrine:query:sql 'SELECT * FROM "user"'<br />Hey Maxim,
Yeah, user might be a reserved keyword, usually we wrap table names with tick. Thank you for sharing your solution!
Cheers!
Hi, if this is correct way to set password without Factory ?
Hey Mepcuk!
Yes! But we can even do a bit less work:
A) You can skip setting the plain password and just do
$this->passwordHasher->hashPassword($user, 'tada')B) You only need one flush(). Remove the first one and just flush/save once after you set the real password. Also, you can then remove the
$user->setPassword('tada'). I'm guessing you had that just so that your database didn't throw an error during the first flush ;).Cheers!
"Houston: no signs of life"
Start the conversation!