User switch with custom restrictions in Symfony
An example on how we added extra rules to the switch user functionality of the Symfony security component.
Recently we found ourselves in a situation where we needed to implement user switching/impersonating functionality. Which means a user can "log in" as another user.
This needed to be done by other (non-admin) users that have certain relations. A user can be linked to multiple other users under the relation managedUsers, which is a many-to-many relation. Users that are allowed to switch to a managed user have a specific role and can only switch to related users.
The Symfony security component supports this functionality, but only based on a certain role. Users with the role "ROLE_ALLOWED_TO_SWITCH" are allowed to switch users. This is great for admins for example, if the users that are granted this role are allowed to impersonate any user. This is definitely not the case in our situation.
So, how did we do this?
First of all, we made sure the users that are allowed to switch were granted the role "ROLE_ALLOWED_TO_SWITCH". Next, we quickly stumbled upon Symfony\Component\Security\Http\Firewall\SwitchUserListener, this is where the switching actually happens. Certain checks are made and a new token is built.
Methods attemptSwitchUser and attemptExitUser are the ones we need to try and hook into. We could override the SwitchUserListener, but if you look closely, an event is being dispatched at the end of each method containing the user we're switching to.
So we create a listener, or in this case, a subscriber:
We subscribe to the SecurityEvents::SWITCH_USER event which is thrown at the end of both methods.
Now we realise that we only have access to the target user. So we don't know anything about the context. We need the original user and the target user to be able to check the things we want. Luckily this is easily accomplished by injecting the token storage into our subscriber. And since we only need to throw an AccessDeniedException to stop the switching, this is all we need.
Add the subscriber to the services.yml:
Next we need to implement our logic, so we added the following methods to the subscriber:
Here we want to check if our user that is doing the impersonating is actually managing the user we're trying to impersonate. We check this for both directions, so even when trying to exit the impersonation. This way we're sure there will be no situation where the user gets access to the wrong account.
Let's put it all together:
The actual implementation is a bit different, these code snippets are purely for demonstration purposes. Feel free to post improvements or other/better ways to tackle this issue!