Decoupling your security user from your user model
This article shows an example of framework decoupling. You'll find a more elaborate discussion in my latest book, Recipes for Decoupling.
Why would it be nice to decouple your user model from the framework's security user or authentication model?
Reason 1: Hexagonal architecture
I like to use hexagonal architecture in my applications, which means among other things that the entities from my domain model stay behind a port. They are never exposed to, for instance, a controller, or a template. Whenever I want to show anything to the user, I create a dedicated view model for it.
We can take the hexagonal approach in any software application, including ones that use a web framework like Symfony or Laravel. However, these frameworks may sometimes require you to expose your internal entities or model objects. One example is the authentication layer. In order to authenticate as a user, the framework wants to know which users you have in your application, and what their hashed passwords are. With Symfony, the standard solution is to equate the security user with the
User entity and use the Entity user provider. If you use hexagonal architecture, this isn't great, since the application now has to expose its entities, even though it wanted to keep them inside.
Reason 2: CQRS
Another reason why you may not like to use your
User entity as the security user is that it's a write model. If you apply CQRS to your application, you don't want to use objects for both write and read use cases. The
User entity is a write model, but the security user required by Symfony is a read model; it won't be used to make changes to the user's state (like changing its password), it only exposes data like the user's email address, hashed password, and their roles. When using CQRS you're not supposed to reuse the same object for both of these use cases.
Reason 3: Different models
One last reason for not using your
User entity as a security user is that they really are a different model, in the sense that they have different properties, offer different behaviors, and just serve different purposes. Furthermore, I've found that in real-world applications has a mostly overlapping set of users and security users, but they aren't per definition the same. For example, you may want some kind of super-admin login, but you don't necessarily want to have an "admin"
User entity because then it also needs an email address, a phone number, a nickname, a public profile, and so on. This demonstrates a typical case of model incompatibility, where we try to fit one into another, and keep pushing until it somewhat works.
Solution: create your own user provider
We can deal with all these possible concerns at once by implementing our own user provider. Symfony has offered this option since the first release of version 2. I actually blogged about it in 2011 which is already 11 years ago. I also contributed to the original cookbook page on the topic, but the best way to find out how to do it today is to look at the current documentation page of course. I must say, it's very cool that Symfony has this option, because it allows us to keep the user entity behind the port (if we use hexagonal architecture), to return a read model instead of a write model as the security user (if we want to do CQRS), and to return different/fewer/more users than we have in our database. Powerful stuff!
Maybe you don't work with Symfony, so here's the outline of the general solution:
- Instead of passing your actual
Userentity or model to the framework, pass an object that matches the API that the framework expects (e.g. that implements a
SecurityUserinterface or offers the expected methods like
- Define your own implementation for the interface of the service that normally returns the security user to the framework (e.g.
SecurityUserRepository), and return your new custom security user object from it instead of the