Decoupling your security user from your user model

Posted on by Matthias Noback

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 User entity or model to the framework, pass an object that matches the API that the framework expects (e.g. that implements a SecurityUser interface or offers the expected methods like getPassword() etc.)
  • 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 User entity/model.
PHP decoupling hexagonal architecture CQRS Symfony user provider
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Pieter Jordaan

Another good usecase I had was related to a project we were working on. You are not logged in as a user, but you are logged in as user with a specific digital book selected. Similar situation would be that a user can switch company context.

If we would have used the default User entity as the logged in user, then the entire frontend needs to filter by book on every ajax call it does. I did have to convince my entire team it was the better solution, but it did make the frontend much easier to work with and less likely they would make mistakes.