Automatic Password Hashing in Symfony 5

Automatic Password Hashing in Symfony 5

Implement automatic password encryption in Symfony using the new Symfony PasswordHasher component with Doctrine Entity Listeners.

How It's Done

Introducing the PasswordHasher Component

Utilizing the UserPasswordHasher from the new Symfony PasswordHasher component, we can easily hash our Users password.

Typically we hash user passwords at the initial persist, and when passwords are modified. So, assuming those requirements, an efficient way to put this new component to work is in combination with a Doctrine Entity Listener.


The Steps

Theres 4 steps involved. 1st we'll need to create the UserListener class. This will be a Doctrine Entity Listener that listens for the persist and update Doctrine events on the User entity.

2nd, we'll need to tell Doctrine about this Listener, by tagging the class with doctrine.orm.entity_listener.

3rd, we'll need to configure the PasswordHasher within our security config.

And finnaly, we'll hook our User entity up by pointing to the new listener as the argument to a Doctrine\ORM\Mapping\UserListener annotation.


Let me show you how its done and how to wire it up in your config.


Creating the Doctrine Entity Listener

Take a look at the UserListener class below. I inject an instance of the UserPasswordHasherInterface in the constructor which allows it to be used within the private encodePassword() method.

The real work though, happens in the prePersist() and preUpdate() methods. These methods are called automatically by Doctrine when a user is being persisted, and being updated.

That being said, users are often updated for reasons besides changing their password. So within the preUpdate() method we must check if the users password has changed. If it was, then we hash and change it. Otherwise we can just return.

php
1// src/Doctrine/UserListener.php
2
3namespace App\Doctrine;
4
5use Doctrine\ORM\Mapping as Orm;
6use Doctrine\ORM\Event\LifecycleEventArgs;
7use Doctrine\ORM\Event\PreUpdateEventArgs;
8use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
9use App\Entity\User;
10
11class UserListener {
12
13 /** @var UserPasswordHasherInterface */
14 private $hasher;
15
16 /**
17 * @param UserPasswordHasherInterface $encoder
18 */
19 public function __construct(UserPasswordHasherInterface $hasher) {
20 $this->hasher = $hasher;
21 }
22
23 /** @Orm\PrePersist */
24 public function prePersist(User $user, LifecycleEventArgs $args) {
25 $password = $user->getPassword();
26 $password = $this->encodePassword($user, $password);
27 $user->setPassword($password);
28 }
29
30 /** @Orm\PreUpdate */
31 public function preUpdate(User $user, PreUpdateEventArgs $args) {
32 if ($args->hasChangedField('password')) {
33 $password = $args->getNewValue('password');
34 $password = $this->encodePassword($user, $password);
35 $user->setPassword($password);
36 }
37 }
38
39 private function encodePassword(User $user, string $password) {
40 return $this->hasher->hashPassword($user, $password);
41 }
42}
43

Tag the Entity Listener

Within your /config/services.yaml you'll need to tell doctrine about the Entity Listener we just created. You do so by tagging the class with doctrine.orm.entity_listener.


Tagging an Individual Class vs. an Entire Directory

You can choose to either tag just the individual class, or tag the entire directory. If you plan on creating additional Doctrine Entity Listeners (and you probably will), then I recommend the latter. Otherwise you'll need to add service configuration for each class. Besides Doctrine Entity Listeners are so useful, you're bound to create more of them.

yaml
1# config/services.yaml
2
3services:
4 # ...
5
6 # Tag just the UserListener:
7
8 App\Doctrine\UserListener:
9 tags: ['doctrine.orm.entity_listener']
10
11 # OR tag an entire directory:
12
13 App\Doctrine\:
14 resource: '../src/Doctrine/'
15 tags: ['doctrine.orm.entity_listener']
16

Configure the Symfony PasswordHasher component

Next up we'll need to configure the PasswordHasher. So while you're still in your config directory, open up /config/packages/security.yaml and add the following snippet.


A Note On the Encoders Key

Symfony has depricated much of the old security system, including the old password encoders. If you've already configured an encoders key, simply replace it with the following password_hashers key which is intended to replace it.

yaml
1# config/packages/security.yaml
2
3security:
4 # ...
5
6 password_hashers:
7 # use your user class name here
8 App\Entity\User:
9 algorithm: auto
10

Tell Doctrine which Entity to Listen On.

You've created an Entity Listener, told Doctrine about it, and configured password hashing.

The final step is to tell Doctrine which Entity to associate with the listener. In otherwords, which Entity should it listen for events on.

php
1// src/Entity/User.php
2
3namespace App\Entity;
4
5use Doctrine\ORM\Mapping as Orm;
6use Symfony\Component\Security\Core\User\UserInterface;
7use App\Doctrine\UserListener;
8
9/**
10 * A user entity.
11 *
12 * @Orm\Entity()
13 * @Orm\EntityListeners({UserListener::class})
14 */
15class User extends BaseEntity implements UserInterface {
16
17 //..
18
19}
20

Give it a Try

And thats that. Go ahead and try persisting a new user. When you look at the result the password should be an unrecognizable mess of characters. If so, great job! If not, you may have missed a step, or misconfigured a small detail.


Learn More

If you'd like to learn more about Doctrine Entity Listeners, or the new Symfony PasswordHasher component, I encourage you to check out the following documentation.

Symfony PasswordHasher Component

Doctrine ORM Entity Listeners


The Interface of the PasswordHasherInterface

Lastly, for reference, here is the interface of the PasswordHasherInterface.

php
1<?php
2
3namespace Symfony\Component\PasswordHasher;
4
5interface PasswordHasherInterface {
6
7 public function hash(string $plainPassword): string;
8
9 public function verify(string $hashedPassword, string $plainPassword): bool;
10
11 public function needsRehash(string $hashedPassword): bool;
12
13}
14

- Comments -