Decoupling your application User entity from Symfony’s security

This post is about my experience in decoupling the Security component User from my application User, as described on Iltar van der Berg’s post at https://stovepipe.systems/post/decoupling-your-security-user

I thought I would write this article to document my takeaways from Iltar’s blog post, and maybe help others with implementation by giving more code examples.


Why separate them?

To me, the main goal is to help prevent accidental security holes.

In my example, I’m using API Platform, and I’m using the User entity for authentication with Symfony’s security component. In other words, my User entity is stored inside the security token object, which is then serialized and stored in a session. (Yep, I’m using session cookies instead of JWT here.)

So, let’s pretend I’m editing my own account and Sally removes my administrator role before I click “Save”. My form data contains administrator = 1. The way API Platform’s request listeners work in this scenario is:

  1. ReadListener loads the User object that the PUT operation will affect.
  2. DeserializeListener applies the PUT data to the User object.
  3. DenyAccessListener checks to see if the request is authorized by running my custom security voter.

What’s important to know here is that my personal user entity has already been loaded/refreshed by Symfony’s security component before API Platform’s ReadListener is executed. And, that Doctrine maintains an identity map so that it doesn’t have to fetch & hydrate an entity multiple times in the same request. Therefore, when ReadListener asks Doctrine to load the User entity (me) that’s being edited (by me), it’s getting back the same instance that is also being stored in the Symfony security token. When DeserializeListener applies the PUT data to that entity, the entity in the security token is also affected.

By the time DenyAccessListener executes and my security voter is called, it appears to the voter that I am indeed an admin (I’m not) and that it’s OK to authorize the action.

So, I can solve this problem (and potentially others both now and in the future) by using a completely separate security user class for Symfony’s security component.


How to separate them

This is really the main reason I wrote this article. Iltar’s blog post gives some code but it doesn’t fully connect the dots. I’d like to show how I did it. Please note that this is just one possible way and isn’t a one-size-fits-all solution.

#1 — Create a separate security user class to be used by the Security component. SecurityUser.php below

#2 — Create a security user provider to load a security user by email. SecurityUserProvider.php below. (This is optional when you’re using a Guard class because you could just load the user in Guard’s getUser method.)

#3 — Configure Symfony to use the new security user and the provider. security.yaml below

#4 — (optional) Create a user provider to fetch a User entity based on the security user. CurrentUserProvider.php below. I inject this service into my app where needed instead of the security token storage. Also, there are other ways, e.g. like Iltar said, for controllers you could create an ArgumentValueResolver.

Code:

(public props & methods clipped for brevity)

This is working out pretty well for me so far. Let me know what you think or if you spot any bugs!