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.
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!