Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Suplantación: switch_user

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

¿Alguna vez te has encontrado en una situación en la que estás ayudando a alguien en línea... y sería mucho más fácil si pudieras ver lo que está viendo en su pantalla... o, mejor, si pudieras hacerte cargo temporalmente y solucionar el problema tú mismo?

Sí, sólo tienes que hacer clic en el pequeño icono del clip para adjuntar el archivo. Debería estar como cerca de la parte inferior... un clip. ¿Qué es "adjuntar un archivo"? Oh... es... como enviar un "paquete"... pero en Internet.

Ah, los recuerdos. Symfony no puede ayudar a enseñar a tu familia cómo adjuntar archivos a un correo electrónico, pero sí puede ayudar a tu personal de atención al cliente a través de una función llamada suplantación de identidad. Muy sencillo: esto da a algunos usuarios el superpoder de iniciar sesión temporalmente como otra persona.

Activar el autentificador switch_user

Así es como se hace. En primer lugar, tenemos que habilitar la función. En security.yaml, bajo nuestro cortafuegos en algún lugar, añade switch_user: true:

security:
... lines 2 - 20
firewalls:
... lines 22 - 24
main:
... lines 26 - 46
switch_user: true
... lines 48 - 61

Esto activa un nuevo autentificador. Así que ahora tenemos nuestro CustomAuthenticator,form_login, remember_me y también switch_user.

¿Cómo funciona? Bien, ahora podemos "iniciar sesión" como cualquiera añadiendo ?_switch_user=a la URL y luego una dirección de correo electrónico. Vuelve al archivo de accesorios -src/Fixtures/AppFixtures.php - y desplázate hacia abajo. Tenemos otro usuario cuyo correo electrónico conocemos: es abraca_user@example.com:

... lines 1 - 15
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 20 - 51
UserFactory::createOne([
'email' => 'abraca_user@example.com',
]);
... lines 55 - 57
}
}

Cópialo, pégalo al final de la URL y...

Acceso denegado.

Por supuesto No podemos permitir que cualquiera haga esto. El autentificador sólo lo permitirá si tenemos un rol llamado ROLE_ALLOWED_TO_SWITCH. Vamos a dárselo a nuestros usuarios administradores. Podemos hacerlo a través de role_hierarchy. Aquí arriba, ROLE_ADMIN tieneROLE_COMMENT_ADMIN y ROLE_USER_ADMIN. Vamos a darles tambiénROLE_ALLOWED_TO_SWITCH:

security:
... lines 2 - 6
role_hierarchy:
ROLE_ADMIN: ['ROLE_COMMENT_ADMIN', 'ROLE_USER_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']
... lines 9 - 61

Y ahora... ¡vaya! ¡Hemos cambiado de usuario! ¡Es un icono de usuario diferente! Y lo más importante, abajo en la barra de herramientas de depuración de la web, vemos abraca_user@example.com... e incluso muestra quién es el usuario original.

Entre bastidores, cuando introdujimos la dirección de correo electrónico en la URL, el autentificador switch_userla cogió y luego aprovechó nuestro proveedor de usuarios para cargar ese objetoUser. Recuerda: tenemos un proveedor de usuarios que sabe cómo cargar usuarios de la base de datos consultando su propiedad email:

security:
... lines 2 - 13
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
... lines 21 - 61

Por eso usamos email en la URL.

Para "salir" y volver a nuestro usuario original, añade de nuevo ?_switch_user= con el especial _exit.

Cambios de estilo durante la suplantación

Pero antes de hacer eso, una vez que una persona del servicio de atención al cliente se ha cambiado a otra cuenta, queremos asegurarnos de que no olvida que se ha cambiado. Así que vamos a añadir un indicador muy obvio a nuestra página de que actualmente estamos "cambiados": hagamos este fondo de cabecera rojo.

Abre el diseño base: templates/base.html.twig. En la parte superior... busca el body y elnav... y lo dividiré en varias líneas. ¿Cómo podemos comprobar si estamos suplantando a alguien? Di is_granted() y pasa estoROLE_PREVIOUS_ADMIN. Si estás suplantando a alguien, tendrás este rol.

En ese caso, añade style="background-color: red"... con !important para anular el estilo nav:

... line 1
<html>
... lines 3 - 14
<body>
<nav
class="navbar navbar-expand-lg navbar-light bg-light px-1"
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }}
>
... lines 20 - 66
</nav>
... lines 68 - 72
</body>
</html>

¡Vamos a verlo! Actualiza y... ¡ja! Es una pista muy obvia de que estamos suplantando.

Ayudar al usuario a acabar con la suplantación

Para ayudar al usuario a acabar con la suplantación, vamos a añadir un enlace. Baja al menú desplegable. Una vez más, comprueba si is_granted('ROLE_PREVIOUS_ADMIN'). Copia el enlace de abajo... pégalo... y envía al usuario a - app_homepage pero pasa un parámetro extra_switch_user establecido en _exit.

Si pasas algo al segundo argumento de path() que no sea un comodín en la ruta, Symfony lo establecerá como parámetro de consulta. Así que esto debería darnos exactamente lo que queremos. Para el texto, di "Salir de la suplantación":

... line 1
<html>
... lines 3 - 14
<body>
<nav
class="navbar navbar-expand-lg navbar-light bg-light px-1"
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }}
>
<div class="container-fluid">
... lines 21 - 29
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 31 - 41
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
... lines 44 - 54
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
{% if is_granted('ROLE_PREVIOUS_ADMIN') %}
<li>
<a class="dropdown-item" href="{{ path('app_homepage', {
'_switch_user': '_exit'
}) }}">Exit Impersonation</a>
</li>
{% endif %}
... lines 63 - 65
</ul>
</div>
{% else %}
... lines 69 - 70
{% endif %}
</div>
</div>
</nav>
... lines 75 - 79
</body>
</html>

¡Inténtalo! Refresca. Es obvio que estamos suplantando... pulsa "Salir de la suplantación" y... volvemos a ser abraca_admin@example.com. ¡Qué bien!

Por cierto, si necesitas más control sobre los usuarios a los que se puede cambiar, puedes escuchar el evento SwitchUserEvent. Para evitar el cambio, lanza un eventoAuthenticationException. Más adelante hablaremos de los escuchadores de eventos.

A continuación: hagamos un breve descanso para hacer algo totalmente divertido, pero... no relacionado con la seguridad: construir una ruta de usuario de la API.

Leave a comment!

33
Login or Register to join the conversation
Rufnex Avatar

Hey, how i have to extend this to allow the user identificatioin with more fields. For e.g. email and company_id.

Reply

Hey Rufnex,

Could you elaborate a bit more on your question? I don't fully understand what you need. If you're talking about impersonation, any user that has the role ROLE_ALLOWED_TO_SWITCH will be able to impersonate anybody

Cheers!

Reply
Rufnex Avatar

Hi MolloKhan,

in the user provider this e.g. uses property: email as identifier. I need a second field (company_id) to identify the user. Can you give me an example for that?

(select .. from users where email = '...' and company_id = 123)

Reply

Ok, in that case you'll need to load your users manually by sending a callback as second argument to the UserBadge, and compose the userIdentifier by concatenating the email + companyId. Something like this:

    public function authenticate(Request $request): PassportInterface
    {
        $email = $request->request->get('email');
        $company = $request->request->get('company');
        $userIdentifier = sprintf('%s:$s', $email, $company);
        
        return new Passport(
            new UserBadge($email, function($userIdentifier) {
                [$email, $company] = explode(':', $userIdentifier);
                $user = $this->userRepository->findOneBy(['email' => $email, 'company' => $company]);

                if (!$user) {
                    throw new UserNotFoundException();
                }

                return $user;
            }),
            new PasswordCredentials($password),
        );
    }

I didn't try that code but it should give you a good idea of what to do :)

Cheers!

Reply
Rufnex Avatar

Thank you, thats so far clear. But what i mean is how to do this with the ?_switch_user .. for that i have to set also the company_id .. is this possible?

In my database, an email can occur more than once. This is made unique in combination via the company_id.

Reply

Ohh I get it now. Symfony, internally uses the UserProvider to load the impersonated user object, it calls the loadUserByIdentifier(), so, what you can do is create a custom UserProvider implementing that method, and the $userIdentifier argument will be (as I mentioned above) a combination of your email + companyId. Does it makes sense?
You can check this class if you want to get deeper on how your users are loaded Symfony\Component\Security\Http\Firewall\SwitchUserListener

Reply
Abraham-O Avatar
Abraham-O Avatar Abraham-O | posted hace 10 meses

any docs on how to set up in-memory users

Reply

Hey Abraham,

Here are the docs to configure in-memory users, however it's not recommended for production
https://symfony.com/doc/current/security/user_providers.html#memory-user-provider

Cheers!

Reply
Mathew Avatar

Hey Symfony Cast Team. I never leave a comment, but I am at wits end. I took on the task of updating a Symfony 3.4 application recently. I have it up to 5.4.6 currently and all is working fine. Although, the impersonate user ability seems to be broken. When I use the _switch_user=username parameter, it switches me over to the user. But then as soon as I click on anything else, it boots me back to the login page with a 403 http status in the web profiler. It redirected me to the security controller::loginAction.

Here are some other things that may help you answer this.
Symfony 3.4 upgraded to 5.4. Using OAuth (HWI/OauthBundle) with Azure,

What more information would you need to help out with this issue?

Reply
Mathew Avatar

Reading some of the items that were already posted in a little more depth, I am using access control.

Reply

Hey @Mathew!

Welcome to the comments - I hope we can help :).

Are you able to repeat this locally? If so, turn intercept_redirects to true: https://github.com/symfony/recipes/blob/2d1ba2cf32556d9c01ff7bd05bb02ec9bfb44e5c/symfony/web-profiler-bundle/5.3/config/packages/web_profiler.yaml#L4

This will "stop" redirects. Basically, each time it is about to redirect you, you will instead see a page saying "you're about to be redirected" with a link to the redirect. The reason this is useful is that this page will have a web debug toolbar at the bottom. On that, you'll be able to see if you're still logged in and who you are logged in as. I'm particularly interested in exactly when you are logged out. For example, here is a potential flow that you could confirm or tell me which part is wrong:

A) You add _switch_user=username
B) This is "read" then you are redirected back to the same URL but without the _switch_user=username (because, now, in theory, it has been processed). At this moment, you ARE logged in as the impersonated user.
C) You click a link. You are suddenly redirected to /logout. On this page's web debug toolbar, you can see that you are NOT logged out.

Pay VERY careful attention to where you are or aren't redirected and who you are logged in as in each step. What flow exactly are you seeing? And, when you finally identify the page (well, it will be a redirect to /login probably) where you are mysteriously not logged in anymore, if you click into the profiler, do you see any interesting logs for this page?

Overall, my initial instinct is that you're losing authentication because it appears that your user is "changing". I can go into more detail about that... once if confirm that it is or isn't the problem. A super fast way to check if this is the issue is to:

1) Add EquatableInterface to your User clas
2) In the isEqualTo method (which the interface will force you to have), simply return true.

If the problem goes away, then we've nailed down the cause and we can then debug (pretty easily) why this is happening and a proper solution (the above is NOT a proper solution).,

Lemme know!

Cheers!

Reply
Mathew Avatar

Thank you so much for your quick reply!
I have turned on the intercept redirects (which is very helpful) and tried the impersonate process again.. So far, what is happening.
I receive the redirect notice for the following.
- Clicking login (Anon)
- Redirected to Microsoft for credentials (which never displays due to remember me (Anon)
- Redirected to our app's main dashboard (AdminUser)
- Redirected to the admin dashboard once it realizes what roles I have (AdminUser
- From there I click on a link in an impersonation GUI (AdminUser)
- Redirected to the standard non admin dashboard (BasicUser)
- Click on any link or refresh, instantly sent to the login screen with no redirect notice (Anon)

I then tried your second recommendation or implementing EquatableInterface and adding the required isEqualTo method which returns true.

IT WORKED!

I very much appreciate the work that you and your team do. While it may be my first time in the conversation tab, I use your lessons quite a bit. Your lessons are excellent. I wish I had more time to watch them all!

Reply

Hey @Mathew!

Woohoo! I'm glad we figured it out! Ok, but before you celebrate TOO much, we should get that isEqualTo() method into a condition where it works AND is "secure". Quick explanation:

A) At the end of each request, the User object is serialized into the session. If you have a _serialize() method or implement SerializableInterface, then those methods will be called to do this. Otherwise, iirc, every property is serialized.

B) At the start of the next request, that User object is deserialized from the session.

C) THEN, the security system queries (assuming your User class is an entity - on a technical level, refreshUser() is called on your user provider) for a "fresh" user. It basically looks at the "id" of your deserialized User object and uses it to fetch a fresh one from the database.

Now, here is where things get interesting. You now have 2 User objects floating around: the deserialized one and the fresh one from the database. At this point, Symfony "compares" these to see if they are "different". And if they ARE different, you are logged out. This is what was happening to you. Why does Symfony compare the objects? It's trying to see if some sensitive data has changed in the database since the user logged in. Here is the most concrete example: suppose a bad user logs in as me on some browser. I find out, log in, and change my password. At this point, our system needs to be smart enough to deauthenticate the "bad user". This "user comparison" process does that.

Normally, to compare the User objects, Symfony uses a built-in algorithm: https://github.com/symfony/symfony/blob/ed9f973de5ed3c950cc0769557919f6ad7a60210/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php#L292-L331 (the logic for this has moved in 6.0, but is still the same).

You can see that it compares several properties on the deserialized User with the fresh User, including password, roles and user identifier. For some reason, once you switched users (and so, the "switched" user would be the one that is now deserialized), this check began to fail. By implementing EquatableInterface, you "took over" and fixed this.

This is a long way of saying that you have 2 options:

A) Figure out why the original logic was failing and fix it. Maybe you are not serializing the roles property so it's missing when you do the comparison? It's a bit weird that you have this problem when switching... but not under normal situations.

B) Keep isEqualTo() but implement your own checks. Ask yourself this about each property on User: if this value changes, should the user be deauthenticated on all other browsers? If yes, then add that check.

Cheers!

Reply
Mathew Avatar
Mathew Avatar Mathew | weaverryan | posted hace 1 año | edited

I THINK im picking up what you're putting down.. I wrote this up, let me know if this is what you were describing.
I am assuming that you meant to check the userInterface brought in to the isEqualTo method against the user object itself?


public function isEqualTo(UserInterface $user)
    {
        $userObjectHasRole = false;
        $userInterfaceHasRole = false;
        if ($this->hasRole('ROLE_USER'))
            $userObjectHasRole = true;
        $roles = $user->getRoles();
        foreach ($roles as $role) {
            if ($role == 'ROLE_USER')
                $userInterfaceHasRole = true;
        }

        $userInterfaceActive = false;
        $userObjectActive = false;

        if ($user->getStatus())
            $userInterfaceActive = true;
        if ($this->getStatus())
            $userObjectActive = true;

        if ($userObjectHasRole and $userInterfaceHasRole and $userObjectActive and $userInterfaceActive)
            return true;
        else
            return false;
    }
Reply

Hey @Mathew!

Apologies for the slow reply! Yes, this is exactly what I had in mind. I would add just a few notes:

A) You could probably simplify by "exiting earlier" instead of storing things in variables. For example:


if ($this->hasRole('ROLE_USER') && !$user->hasRole('ROLE_USER') {
    return false;
}

if ($this->getStatus() != $user->getStatus()) {
    return false;
}

return true;

B) I would also add checks to compare the password of the user... and maybe also the email. You have to ask yourself (for example): is it feasible that the user would ever need to change their email to "regain" for a security reason and want all other browsers currently logged in to be deauthenticated? I think for "password", this is definitely a yes. For email, I'm less sure, but by default, the email address is a field that is normally "compared" in the core algorithm.

Cheers!

Reply
Ruslan Avatar

Hi.
Is it possible to use both(one of params) parameters "email" or "id"?
Some time for users support convenient to use "email" , another time "id".

Thank you.

Reply

Hey Ruslan

That's a good question and it's not actually documented. The anwers relies on a UserProvider. What you need to do is to add a custom user provider for your application, and implement the loadUserByIdentifier() method, that method should be able to find users by email or by id (or whatever other field you may use to identify a user).

Cheers!

Reply
Ruslan Avatar

Do you mean UserInterface::getUserIdentifier() ? (I fond it in S5.4)

Thank you.

Reply
Matteo S. Avatar
Matteo S. Avatar Matteo S. | posted hace 1 año

I have another question.
I'd like to define a section of the website (e.g. all the routes starting with /admin) where impersonation is ignored/skipped, so when you access those pages, you are not impersonating anyone and you are just logged in with your original user, but when you are visiting any other url, you keep impersonating.
So, for example. Say I have an admin user admin@example.com with ROLE_ADMIN, and a regular user user@example.com with no role (other than ROLE_USER). I log in as admin@example.com, then I switch user (i.e. start impersonating) to user@example.com. When I visit any non-admin url, i.e. any url that does not start with /admin, I am impersonating: I see everything as if logged in as user@, except for the red navbar that reminds me that I am impersonating. But when I go to /admin/whatever, I am not impersonating, so instead of getting an access denied error, I can simply access the page and I am logged in as my real user, admin@.
How can I do that? Can I attach a listener to some event where I can prevent impersonation from happening, but not by denying access, but rather cause the impersonation to be "skipped"?
Typical use case: I am an admin, I go to the admin panel, manage a user, I want to check something, so I start impersonating the user, then I go back to the admin panel and fix/edit something about that user's profile, then go back to the impersonation to check that everything is as expected from his point of view, and so on back and forth, without having to exit and enter impersonation over and over again (and I can even keep two separate browser tabs one with the admin panel and the other with the impersonated user - but that's irrelevant).
VBulletin 3.7 did this circa 2004.

Reply
Matteo S. Avatar

By looking further into it, I think what I'm looking for is not to "prevent" impersonation from happening or "skip" it, because that happens at the moment of switching, whereas I'm looking for something that happens at every request. It's more like: when the session is loaded, I want to somehow retrieve the original user and replace it into the current user... something like that

Reply

Hey Matteo S. !

Hmm. This is really interesting... and it's not a problem I've ever thought of before. And once again, it comes back to the "token" object. We know that after we switch users, our token is a SwitchUserToken. And we also know that we can get the original token by saying $token->getOriginalToken().

Using these two things, I think you could trick the system. You would do it by:

A) Register a listener to the RequestEvent (the one that happens early in Symfony). Use a priority of 7 - you can see an example of priorities here: https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber - the reason I'm using 7 is that the security system is initialized with a listener at priority 8... and we want our listener called right after that.

B) In this listener, inject the Security service and use $this->security->getToken() to get the token object. If this is NOT an instance of SwitchUserToken, just return and do nothing. Or, if the URL does NOT start with /admin, return and do nothing (so, do nothing if the current URL is one where you want the normal switch user behavior to happen).

C) If it IS an instance of SwitchUserToken, then get the token and call $originalToken = $token->getOriginalToken(). store the SwitchUserToken on some property on your subscriber for later - e.g. $this->switchUserToken = $token;

D) Now we are going to take that $originalToken and tell Symfony that we are authenticated using it. Do that by injecting the TokenStorageInterface service, and then call $this->tokenStorage->setToken($originalToken).

E) And... you're done! You should now be logged in as whatever the original user & token was. But, before the request finishes, you need to "restore" the SwitchUserToken, otherwise you'll "lose" the impersonation permanently. To do that, use the same class you created in step (A) and make it listen to ResponseEvent with a priority of 1 (so that you are called before ContextListener, which has a priority of 0). Very simply, if ($this->switchUserToken) then $this->tokenStorage->setToken($this->switchUserToken).

The tk;dr is that you "change" the token to the original right after security starts and change it BACK to the "switch user" token right before security finishes. I can't think of any problems this would cause... it's kind of a cool idea ;).

Let me know how it goes!

Cheers!

1 Reply
Matteo S. Avatar

So I'm trying this, but I think I've run into a problem. When my listener handles the RequestEvent, and the requested url is an /admin one, it seems that an AccessDeniedException has already been thrown, so apparently it's too late to switch tokens. This is the output of dd($event->getRequest()->attributes) inside my listener:

Symfony\Component\HttpFoundation\ParameterBag {#759 ▼
#parameters: array:4 [▼
"_controller" => "error_controller"
"exception" => Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException {#579 ▶}
"logger" => Symfony\Bridge\Monolog\Logger {#49 …6}
"_stopwatch_token" => "b8c8c0"
]
}
I'm using a priority of 7 for my listener as you said.

Reply

Hey Matteo S.!

Ah, darn. I bet I know the issue: part of the Firewall listener (the one that we are trying to execute right *after*) is the enforcement of access_control. So the Firewall listener initializes security (yay!) but then immediately uses the current user to enforce access_control (boo!).

If i'm right, the simplest solution would be to stop using access_control, which may be no big deal... or a huge bummer. If you NEED access_control, then... hmm. Here is another solution that *may* work. In the listener that handles RequestEvent, you're going to convert this into an event subscriber AND a security authenticator. The ResponseEvent logic will stay the same, but the RequestEvent logic get worked into the custom authenticator methods. We're basically going to do the RequestEvent logic but from inside of the authenticator methods, which should allow it to run during the security initialization but after access_control. In supports(), you would check to see if $this->security->getToken() is the SwitchUserToken. If it is, return true. In authenticate(), you would return a SelfValidatingPassport() containing the original user. And you would set the original SwitchUserToken on a property: the same "flow" as before. Then, in the ResponseEvent listener, you would replace the token at the end of the request lke normal.

I'm still making some assumptions here (I'm assuming the logic that loads the SwitchToken from the session and puts that into the security system occurs before your authenticators are called)... so it may not work. Also, this will make it look like your user is being authenticated on every request. What that means in practical terms is simply that events like LoginSuccessEvent will trigger on every request. So if you're relying on this to run code after a user *really* logs in, that'll mess things up. As a work around, you could do everything I said above about the authenticator except that you (A) return false ALWAYS in supports and (B) right before that return statement, you do all the logic of setting the token into the token storage. Basically, you "abuse" the authenticator system: you take advantage of the fact that supports() will be called at the correct time, and then do all your logic there, even though that's not what it's meant for ;).

tl;dr the access_control muddies things a lot. Doing this may still be possible, but it might not (depends on if my assumptions are correct).

Let me know if you get it working!

Cheers!

1 Reply
Matteo S. Avatar

EDIT: BOGUS COMMENT my bad (I'll leave this to avoid causing confusion if you have aready received a notification of it)

[NOT TRUE:] I have tried the "Stop using access_control" approach. I control access with /** @IsGranted */ annotations instead. Now the flow goes as expected, my listener with priority 7 gets called when I go to /admin too, I can dd() stuff as expected, but even though I switch the token with $this->tokenStorage->setToken($token->getOriginalToken()), I get "access denied" regardless (that's because I wasn't properly checking for the controller being called).

Reply

Wooo! I was reading your comment in my inbox thinking "darn, I really thought that would work!". Haha, really happy it did - a nice system!

1 Reply
Matteo S. Avatar

Thank you soooooooooooo much!! I'll try and let you know. You are the author and voice of the tutorials, right? I ask because I'm reading your replies with your voice in my head and if it's not you I need to stop doing that - lol.

Reply

Hey PhpFan,

> I'm reading your replies with your voice in my head and if it's not you I need to stop doing that - lol.

Hahaha, that's funny! :D And yes, you're right! Ryan is the author and the voice of this tutorial ;)

Cheers!

1 Reply
Matteo S. Avatar
Matteo S. Avatar Matteo S. | posted hace 1 año

Also, how do you access the original user (i.e. the impersonator) from twig? I want to show the username or email of the original user next to the "exit impersonation" link.

Reply

Hey Matteo S.!

Another good question! The answer goes back to the "security token" thing - a topic I don't talk about much in this tutorial because it's rarely useful (but in a few cases, it's very useful). After switching, your "security token" is an instance of SwitchUserToken: https://github.com/symfony/.... You can access this in Twig via app.token. So, app.token.originalToken will give you the "original" token object (probably an instance of UsernamePasswordToken... but it doesn't really matter, since most tokens have all the same methods except for a few special things like SwitchUserToken). To get the original user, it would be app.token.originalToken.user.

I hope that helps :).

Cheers!

1 Reply
Matteo S. Avatar
Matteo S. Avatar Matteo S. | posted hace 1 año

What's the difference between using is_granted('ROLE_PREVIOUS_ADMIN') and is_granted('IS_IMPERSONATOR')?

Reply

Hey Matteo S.!

Effectively nothing. I don't know why both exist. On a technical level:

A) IS_IMPERSONATOR looks for your security "token" to be an instance of SwitchUserToken, which is the token that switch_user gives you.
B) ROLE_PREVIOUS_ADMIN obviously looks for that role... and that extra role is added always when you use the switch_user system.

I had never really thought about it before, but those are two different ways to check for the exact same thing. ROLE_PREVIOUS_ADMIN predates the other... so we may have added IS_IMPERSONATOR to be more clear, but never removed the other way.

Cheers!

1 Reply
Matteo S. Avatar

Thank you!!! Now with what you said I just grepped (could have done it before actually) and found this:
symfony/security-core/Authorization/Voter/RoleVoter.php: trigger_deprecation('symfony/security-core', '5.1', 'The ROLE_PREVIOUS_ADMIN role is deprecated and will be removed in version 6.0, use the IS_IMPERSONATOR attribute instead.');

1 Reply

Ah, I didn't even realize that! Thanks for the tip!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}
userVoice