Advanced user switching
Tagged with: [ PHP ] [ security ] [ switch user ] [ symfony2 ]
A really neat trick in the Symfony Security component is the fact that you can impersonate or “switch” users. This allows you to login as another user, without supplying their password. Suppose a client of your application has a problem at a certain page which you want to investigate. Sometimes this is not possible under your own account, as you don’t have the same data as the user, so the issue might not even occur in your account. Instead of asking the password from the user itself, which is cumbersome, and not a very safe thing to begin with, you can use the switch-user feature.
Obviously, switching users is not something you like to enable for all users. By default this feature is disabled and
you must explicitly turn it on by adding switch_user: true
in your firewall configuration. From that point on, you can
switch to another user by supplying the following in your URLs: http://myapp.com/foo/bar?_switch_user=johndoe
. Symfony
will run in the context for the “johndoe” until you to log out.
Again, it’s not a feature you want to enable for everybody, so as a secondary barrier, it will only work when the user
who wants to make the switch has the ROLE_ALLOWED_TO_SWITCH
role.
One of the issues with this system, is that there is no way of restricting to which users somebody can switch to. If you are allowed to switch users, you are allowed to switch to everybody. This is not always a good thing. For instance, let’s assume that you have three types of users in your application: users, which are your normal (paid) clients. *support, which are users working for the company that deal with issues from users, and **admins, which are users that administer and maintain the application itself. They for instance can change or add support users, while support users can change or add only regular users.
Having a switch-user feature can really be a major benefit in such an application. Whenever users are in trouble, a support user can login as that user and fix the problem. However, when a support user is allowed to switch, they are also allowed to switch to administrators accounts. In this case, we only want support users to be able to switch to regular users, but not administrators (and even other support users).
The new listener
We solve this problem by creating a new switch user listener. While normally it would be easy enough to extend functionality, we cannot do so in this case, because the functionality we need to adapt is stored inside a private method. There is no way we could extend it, or even create a decent composite class from it. We are forced to copy/paste the class in order to add a few single lines. If there are other ways of doing this, please let me know!
The listener that deals with switching users is the SwitchUserListener
. This listener checks if you have specified the
_switch_user
option in your querylist, and if so, it will call the attemptSwitchUser()
method. This method does all
the work: it checks if the user exists, if you have the ROLE_ALLOWED_TO_SWITCH
role and finally dispatches a
SWITCH_USER
event.
Unfortunately, we cannot add our functionality inside the SWITCH_USER
event, as it will not allow us to change the
outcome of the result (it’s just for notification purposes). We cannot create a decorator class, that will call
attemptSwitchUser()
and add functionality before or after, because the SWITCH_USER
will be called, even when we do
not have enough rights to switch. We only want this event to be dispatched when we are sure we can switch the user.
Our only option is to copy/paste the whole attemptSwitchUser()
method into extended class, and add a bit of checking.
But, we will be changing the constructor a bit too. So all in all, i’ve decided to copy/paste the WHOLE class instead :(
At the end of the argument list of our constructor, we add a $switchableRole
parameter. This is the name of the role a
user must have in order to be able to switch to. By default this will be ROLE_SWITCHABLE
. If a user does not have this
role, you cannot switch to it. You don’t need to add this role to every user, you can use role inheritance for this
purpose to make it easier to maintain.
The only other change is inside the attemptSwitchUser()
method. Most of this class is regular copy/paste:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private function attemptSwitchUser(Request $request)
{
$token = $this->securityContext->getToken();
$originalToken = $this->getOriginalToken($token);
if (false !== $originalToken) {
if ($token->getUsername() === $request->get($this->usernameParameter)) {
return $token;
} else {
throw new \LogicException(
sprintf('You are already switched to "%s" user.', $token->getUsername())
);
}
}
if (false === $this->accessDecisionManager->decide($token, array($this->role))) {
throw new AccessDeniedException();
}
$username = $request->get($this->usernameParameter);
if (null !== $this->logger) {
$this->logger->info(sprintf('Attempt to switch to user "%s"', $username));
}
$user = $this->provider->loadUserByUsername($username);
$this->userChecker->checkPostAuth($user);
$roles = $user->getRoles();
$roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $this->securityContext->getToken());
$token = new UsernamePasswordToken(
$user,
$user->getPassword(),
$this->providerKey,
$roles
);
// START: THIS IS WHAT WE NEED TO ADD
// Do not allow access when the user we switch to has not the switchable role
if (false === $this->accessDecisionManager->decide($token, array($this->switchableRole))) {
throw new AccessDeniedException();
}
// END: THIS IS WHAT WE NEED TO ADD
if (null !== $this->dispatcher) {
$switchEvent = new SwitchUserEvent($request, $token->getUser());
$this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
}
return $token;
}
The only check we add, just before we call the dispatcher, is to see if the user we are switching to, has the switchable
role. If not, we throw an accessDeniedExecption
.
All that remains is tying this code into your application. In Symfony2 this means adding it to your services.yml
:
Make sure the name of the service is security.authentication.switchuser_listener
. This is because it will override the
default switch user listener. That way, it will be used by default by the security component without additional work.
Inside the arguments there are some empty arguments, and that is ok: it will be filled in by Symfony during the creation
of the firewalls. This class is only used as a “template” for the real switch user listeners.
And that’s it! Whenever you have the ROLE_IS_ALLOWED_TO_SWITCH
, you can actually change to any user, provided they
have the ROLE_SWITCHABLE
. It’s easy to configure and even extend this functionality. For instance, have users with
ROLE_ADMIN be able to switch to any user regardless of the switchable role.
You can find the complete class and service definition in this gist: https://gist.github.com/jaytaph/c75ea98dbefea9e83790
Check out the security edition of the Symfony Rainbow Deepdive series! It contains an in-depth explanation of the Security component, the different bundles and bridges and detailed explaination of all configuration options.
The book, and free sample book are available as PDF, EPUB and MOBI on Leanpub: https://leanpub.com/symfonyframeworkdeepdive-security.