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.
php1// src/Doctrine/UserListener.php23namespace App\Doctrine;45use 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;1011class UserListener {1213 /** @var UserPasswordHasherInterface */14 private $hasher;1516 /**17 * @param UserPasswordHasherInterface $encoder18 */19 public function __construct(UserPasswordHasherInterface $hasher) {20 $this->hasher = $hasher;21 }2223 /** @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 }2930 /** @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 }3839 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.
yaml1# config/services.yaml23services:4 # ...56 # Tag just the UserListener:78 App\Doctrine\UserListener:9 tags: ['doctrine.orm.entity_listener']1011 # OR tag an entire directory:1213 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.
yaml1# config/packages/security.yaml23security:4 # ...56 password_hashers:7 # use your user class name here8 App\Entity\User:9 algorithm: auto10
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.
php1// src/Entity/User.php23namespace App\Entity;45use Doctrine\ORM\Mapping as Orm;6use Symfony\Component\Security\Core\User\UserInterface;7use App\Doctrine\UserListener;89/**10 * A user entity.11 *12 * @Orm\Entity()13 * @Orm\EntityListeners({UserListener::class})14 */15class User extends BaseEntity implements UserInterface {1617 //..1819}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
The Interface of the PasswordHasherInterface
Lastly, for reference, here is the interface of the PasswordHasherInterface
.
php1<?php23namespace Symfony\Component\PasswordHasher;45interface PasswordHasherInterface {67 public function hash(string $plainPassword): string;89 public function verify(string $hashedPassword, string $plainPassword): bool;1011 public function needsRehash(string $hashedPassword): bool;1213}14