State Processors: Hashing the User Password
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 SubscribeWhen an API client creates a user, they send a password field, which gets set onto the plainPassword property. Now, we need to hash that password before the User is saved to the database. Like we showed when working with Foundry, hashing a password is simple: grab the UserPasswordHasherInterface service then call a method on it:
| // ... lines 1 - 6 | |
| use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
| // ... lines 8 - 30 | |
| final class UserFactory extends ModelFactory | |
| { | |
| // ... lines 33 - 47 | |
| public function __construct( | |
| private UserPasswordHasherInterface $passwordHasher | |
| ) | |
| { | |
| // ... line 52 | |
| } | |
| // ... lines 54 - 81 | |
| protected function initialize(): self | |
| { | |
| return $this | |
| ->afterInstantiate(function(User $user): void { | |
| $user->setPassword($this->passwordHasher->hashPassword( | |
| $user, | |
| $user->getPassword() | |
| )); | |
| }) | |
| ; | |
| } | |
| // ... lines 93 - 97 | |
| } |
But to pull this off, we need a "hook" in API platform: we need some way to run code after our data is deserialized onto the User object, but before it's saved.
In our tutorial about API platform 2, we used a Doctrine listener for this, which would still work. Though, it does some negatives, like being super magical - it's hard to debug if it doesn't work - and you need to do some weird stuff to make sure it runs when editing a user's password.
Hello State Processors
Fortunately, In API platform 3, we have a shiny new tool that we can leverage. It's called a state processor. And actually, our User class is already using a state processor!
Find the API Platform 2 to 3 upgrade guide... and search for processor. Let's see... here we go. It has a section called providers and processors. We'll talk about providers later.
According to this, if you have an ApiResource class that is an entity - like in our app - then, for example, your Put operation already uses a state processor called PersistProcessor! The Post operation also uses that, and Delete has one called RemoveProcessor.
State processors are cool. After the sent data is deserialized onto the object, we... need to do something! Most of the time, that "something" is: save the object to the database. And that's precisely what PersistProcessor does! Yea, our entity changes are saved to the database entirely thanks to that built-in state processor!
Creating the Custom State Processor
So here's the plan: we're going to hook into the state processor system and add our own. Step one, run a new command from API Platform:
php ./bin/console make:state-processor
Let's call it UserHashPasswordProcessor. Perfect.
Spin over, go into src/, open the new State/ directory and check out UserHashPasswordStateProcessor:
| // ... lines 1 - 2 | |
| namespace App\State; | |
| use ApiPlatform\Metadata\Operation; | |
| use ApiPlatform\State\ProcessorInterface; | |
| class UserHashPasswordStateProcessor implements ProcessorInterface | |
| { | |
| public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
| { | |
| // Handle the state | |
| } | |
| } |
It's delightfully simple: API platform will call this method, pass us data, tell us which operation is happening... and a few other things. Then... we just do whatever we want. Send emails, save things to the database, or RickRoll someone watching a screencast!
Activating this processor is simple in theory. We could go to the Post operation, add a processor option and set it to our service id: UserHashPasswordStateProcessor::class.
Unfortunately... if we did that, it would replace the PersistProcessor that it's using now. And... we don't want that: we want our new processor to run... and then also the existing PersistProcessor. But... each operation can only have one processor.
Setting up Decoration
No worries! We can do this by decorating PersistProcessor. Decoration always follows the same pattern. First, add a constructor that accept an argument with the same interface as our class: private ProcessorInterface and I'll call it $innerProcessor:
| // ... lines 1 - 5 | |
| use ApiPlatform\State\ProcessorInterface; | |
| // ... lines 7 - 9 | |
| class UserHashPasswordStateProcessor implements ProcessorInterface | |
| { | |
| public function __construct(private ProcessorInterface $innerProcessor) | |
| { | |
| } | |
| // ... lines 15 - 21 | |
| } |
After I add a dump() to see if this is working, we'll do step 2: call the decorated service method: $this->innerProcessor->process() passing $data, $operation, $uriVariables and... yes, $context:
Tip
In API Platform 3.2 and higher, you should return $this->innerProcessor->process(). This
is also a safe thing to do in 3.0 & 3.1.
| // ... lines 1 - 9 | |
| class UserHashPasswordStateProcessor implements ProcessorInterface | |
| { | |
| // ... lines 12 - 15 | |
| public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
| { | |
| dump('ALIVE!'); | |
| $this->innerProcessor->process($data, $operation, $uriVariables, $context); | |
| } | |
| } |
Love it: our class is set up for decoration. Now we need to tell Symfony to use it. Internally, PersistProcessor from API Platform is a service. We're going to tell Symfony that whenever anything needs that PersistProcessor service, it should be passed our service instead... but also that Symfony should pass us the original PersistProcessor.
To do that, add #[AsDecorator()] and pass the id of the service. You can usually find this in the documentation, or you can use the debug:container command to search for it. The docs say it's api_platform.doctrine.orm.state.persist_processor:
| // ... lines 1 - 6 | |
| use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |
| ('api_platform.doctrine.orm.state.persist_processor') | |
| class UserHashPasswordStateProcessor implements ProcessorInterface | |
| { | |
| // ... lines 12 - 21 | |
| } |
Decoration done! We're not doing anything yet, but let's see if it hits our dump! Run the test:
symfony php bin/phpunit tests/Functional/UserResourceTest.php
And... there it is! It's still a 500, but it is using our processor!
Adding the Hashing Logic
Now we can get to work. Because of how we did the service decoration, our new processor will be called whenever any entity is processed... whether it's a User, DragonTreasure or something else. So, start by checking if $data is an instanceof User... and if $data->getPlainPassword()... because if we're editing a user, and no password is sent, no need for us to do anything:
| // ... lines 1 - 11 | |
| class UserHashPasswordStateProcessor implements ProcessorInterface | |
| { | |
| // ... lines 14 - 17 | |
| public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
| { | |
| if ($data instanceof User && $data->getPlainPassword()) { | |
| // ... line 21 | |
| } | |
| $this->innerProcessor->process($data, $operation, $uriVariables, $context); | |
| } | |
| } |
By the way, the official documentation for decorating state processors is slightly different. It looks more complex to me, but the end result is a processor that's only called for one entity, not all of them.
To hash the password, add a second argument to the constructor: private UserPasswordHasherInterface called $userPasswordHasher:
| // ... lines 1 - 8 | |
| use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
| // ... lines 10 - 11 | |
| class UserHashPasswordStateProcessor implements ProcessorInterface | |
| { | |
| public function __construct(private ProcessorInterface $innerProcessor, private UserPasswordHasherInterface $userPasswordHasher) | |
| { | |
| } | |
| // ... lines 17 - 25 | |
| } |
Below, say $data->setPassword() set to $this->userPasswordHasher->hashPassword() passing it the User, which is $data and the plain password: $data->getPlainPassword():
| // ... lines 1 - 11 | |
| class UserHashPasswordStateProcessor implements ProcessorInterface | |
| { | |
| // ... lines 14 - 17 | |
| public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
| { | |
| if ($data instanceof User && $data->getPlainPassword()) { | |
| $data->setPassword($this->userPasswordHasher->hashPassword($data, $data->getPlainPassword())); | |
| } | |
| $this->innerProcessor->process($data, $operation, $uriVariables, $context); | |
| } | |
| } |
And this all happens before we call the inner processor that actually saves the object.
Let's try this thing! Run that test:
symfony php bin/phpunit tests/Functional/UserResourceTest.php
Victory! After creating a user in our API, we can then log in as that user.
User.eraseCredentials()
Oh, and it's minor, but once you have a plainPassword property, inside of User, there's a method called eraseCredentials(). Uncomment $this->plainPassword = null:
| // ... lines 1 - 67 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 70 - 186 | |
| public function eraseCredentials() | |
| { | |
| // If you store any temporary, sensitive data on the user, clear it here | |
| $this->plainPassword = null; | |
| } | |
| // ... lines 192 - 292 | |
| } |
This makes sure that if the object is serialized into the session, the sensitive plainPassword is cleared first.
Next: let's fix some validation issues via validationGroups and discover something special about the Patch operation.
18 Comments
Hi ! when I use the process method I need to return the result of the process method of the decorated class. else my POST call return an empty string.
Hey @Hugues!
Hmm, interesting! Let's think about this. If you don't return something from your processor, then API Platform will ultimately serialize the
$dataargument that you passed in. In your case, unless something else is going on, the inner processor, after saving, returns$dataright back to you. So... there shouldn't be any difference: if you return nothing, then$data(your entity) is serialized. But by returning$this->innerProcessor->process()... that should result in returning the same object! If you dump$dataand$this->innerProcessor->process(), they must be different. Are they? If so, what are the differences? I would be interested to know - it smells like a mystery :).Cheers!
Hi @weaverryan,
Thank you for your answer !
You're totally right
$dataand$this->innerProcessor->process()return the same entity.I use Symfony 6.4 with api-platform/core v3.2.7.
If I debug my
UserHashPasswordProcessormethod I have this call stack :if I've not returned anything from my processor
$datawill be null in the process method of SerializeProcessor.perhaps the code has changed in the meantime?
have a good weekend and I hope the conferences in Brussels were fascinating...
Hey @Hugues!
Hmm, indeed! The
WriteProcessorstuff is new in API Platform 3.2... and while its addition shouldn't have changed anything, by looking at your debugging, it seems like you're right. If you don't return anything, nothing gets serialized. Well, actually, this is likely because you're using the new "mode" of API Platform without event listeners - https://api-platform.com/docs/core/upgrade-guide/#event-listeners - I'm guessing the "old" mode still works. But really, returning something from your processor is more correct anyway. I'll add a note to the tutorial to help others.It was GREAT. Many friends, many great conversations and ideas. Excited about the future :)
Cheers!
Hi @weaverryan,
I suspect a regression in API Platform 3.3.
If I run the tests in the "finish" folder after upgrading Symfony to 6.4 and API Platform to 3.3, then the
patch()calls returnnullagain.If I revert only API Platform to 3.2 and keep Symfony to 6.4, the tests pass flawlessly.
In 3.2,
event_listeners_backward_compatibility_layer: falsebreaks the tests, while when set totruethey pass.In 3.3, either value breaks the tests, so I guess the issue comes from somewhere else.
1) App\Tests\Functional\DragonTreasureResourceTest::testPostToCreateTreasure<br />Expected "(null)" to be the same as "A shiny thing".2) App\Tests\Functional\DragonTreasureResourceTest::testPatchToUpdateTreasure<br />Expected "(null)" to be the same as "12345".... 3 more errors like these. Do you have any suggestion to debug the source of the issue ? Thanks!
EDIT : fixed
The issue was that I added the return of
$this->innerProcessor->process()only to one of the twoStateProcessors, adding thereturnto all of them, changing the method return type tomixedsolves the bug.event_listeners_backward_compatibility_layeris not concerned.Is there a way to process additional fields from the request?
For example, for the registration, I send,
email,passwordandfirstname.emailandpasswordgoes to theUserentity and thefirstnamein theProfileentity (that's created in aStateProcessor, but there, I don't have access to the original POST request/dataHey @Sebastian-K!
Hmmm. How have you set things up so that the "registration" endpoint has a
firstNamefield but withoutUserhaving afirstNameproperty? Usually I WOULD have this as a property onUser, or I might make a DTO for this specific operation if you've got things split up.But anyway, this is an interesting problem! The JSON is ready from the request and passed directly to the serializer here: https://github.com/api-platform/core/blob/main/src/Symfony/EventListener/DeserializeListener.php#L98-L101
The problem is that, if your "resource class" for this operation is
Userand it doesn't have afirstNameproperty, then that field from the JSON is simply ignored. I think the only way to get thefirstNamefield would be to grab the$request->getContent()andjson_decode()it manually. But... I really hope we can find a better way :).Cheers!
Does it really make sense to set up decoration for the UserHashPasswordProcessor via #[AsDecorator()]? As I understand it the decorating service is then involved in every call of the PersistProcessor?
In the API Platform docs they use a "bind" in the services to bind the $persistProcessor as an argument to the "UserPasswordHasher".
This way I guess it is only decorating the service when it is used (e.g. defining the "processor" on operation level)...
Hey @Tobias-B!
Your thinking on this is absolutely correct. For me, it was a trade-off between complexity (the API Platform official way is more complex) vs potential performance problems. So, the final decision is subjective, but since
PersistProcessoris only called during POST/PUT/PATCH operations and it will only be called once (I would be more concerned ifPersistProessorwere called many times during a single request) and the logic inside ofUserHashPasswordProcessoris really simple in those cases (if notUser, it exits immediately), I think the performance issue is non-material. So, I went for simplicity :). But I think the other approach is 110% valid - so you can choose your favorite.Cheers!
I'm building this tutorial not in the project but in a custom bundle.
Since it took me some time to find the solution for my case, I'd like to post it here for others that might struggle with that.
I did NOT set the
#[AsDecorator(PersistProcessor::class)]attribute inUserHashPasswordProcessorIn the User Entity I added
processor: UserHashPasswordProcessor::classto Put, Post and Patch.Example:
In the bundles services.xml I added:
Since this Processor is called from the
UserEntity now, I also modified theprocessmethod inUserHashPasswordProcessora litte:If this isn't smart, please correct me :-)
Hi @weaverryan,
When using your method of declaring the state processor on the post operation, I get an error :
Processor \"App\\State\\UserHashPasswordStateProcessor\" not found on operation \"_api_/users{._format}_post\"Don't know why it happens, I just figured out I'd let you know, and maybe you could help me understand
To solve the issue, I use the documentation's method, binding the persist processor to my own using the service.yaml config file, and it works fine.
https://api-platform.com/docs/core/user/
Hey @Frederic
How did you set up the
UserHashPasswordStateProcessor? What Ryan did was to decorate the main "persist" processor service, which would surprise me to throw an error. But anyway, the docs way seems better to me because you don't want to check the object's type every timeCheers!
I can't confirm I did exactly the same as Ryan, I followed the script (no video, because not paid membership (yet...)), maybe I missed something in the video that isn't shown in the script and I might also just have missed something obvious on my part.
I manage to make it work the Ryan's way. Don't know what went wrong the first time
But I noticed something else, I let the
dump('ALIVE')statement at the beginning of myprocess()method, and I discovered it behaves differently between both ways of wiring :using
AsDecorator,"ALIVE"gets dump to the console during test,using the service config file, it doesn't.
Everything else happens the same (post, patch, hash, persist)
I see, if it is working now it's very likely that you missed something.
The difference between the config file and using PHP attributes is that attributes are not environment-specific, they will work on any your app is running, but config files are bound to an environment (dev, test, prod). If you configure the decorator service in a
config/services_test.yamlfile, does it work?Cheers!
Hi Ryan,
Thnaks for your amazing work.
I follow exactly the tutorial.
When I use
#[AsDecorator('api_platform.doctrine.orm.state.persist_processor')]it's working.But if I try (as you wrote in the tip) with
I get an error when launching tests
Any Idea ? Thanks
Hey @Sylvain-B!
Hmm, yea, I think I know what's going on - and my tip was misleading! If you wanted to simply inject/autowire the
api_platform.doctrine.orm.state.persist_processorinto some other service, usingPersistProcessor::class(which is a service alias) would work great. But with decoration, it doesn't - and that's my fault. Explanation:api_platform.doctrine.orm.state.persist_processor. Other services specifically reference this id to have this service injected into them.api_platform.doctrine.orm.state.persist_processor, you effectively "replace" this service with your own. So anyone that was previously asking for this service id to be passed to them, will now be passed your service.PersistProcessor::class, you are decorating a service alias. So IF anyone asked forPersistProcessor::classto be passed to them, they would receive your service. But in reality, everyone asks forapi_platform.doctrine.orm.state.persist_processor... which remains the original, core service.Thanks for mentioning this - I wanted people to be aware of the new, shorter way of referencing the service - but in decoration, it's incorrect! I'm going to remove the note.
Cheers!
Hi team, my question is: if I use Symfony with Api Platform and Easy Admin, where I have to write "unified" logic to hash the password for both applications?
I think that in that case a listener/subscriber is better, right?
Hey @Fedale!
That's a great question. I can think of 2 options, and they're both totally fine imo:
persistEntity()in your controller. Duplication sounds lame... but password hashing logic is already SO simple (it's just 1 line basically) that you are not really duplicating much.Cheers!
"Houston: no signs of life"
Start the conversation!