Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Impersonation (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

While we're inside security.yaml, I want to talk about another really cool feature called switch_user. Imagine you're an admin user and you're trying to debug an issue that a customer saw. But, dang it! The feature works perfectly for you! Is the customer wrong? Or is there something unique to their account? We'll never know! Time to find a different career! The end is nigh!

Suddenly, a super-hero swoops in to save the day! This hero's name? switch_user.

In security.yaml, under your firewall, activate our hero with a new key: switch_user set to true:

... lines 2 - 16
... lines 18 - 20
... lines 22 - 34
switch_user: true
... lines 36 - 57

As soon as you do this, you can go to any URL and add ?_switch_user= and the email address of a user that you want to impersonate. Let's try spacebar1@example.com.

And... access denied! Of course! To prevent any user from taking advantage of this little trick, the switch_user feature requires you to have a special role called ROLE_ALLOWED_TO_SWITCH. Go back to security.yaml and give ROLE_ADMIN users this new role under role_hierarchy:

... lines 2 - 13
... lines 16 - 57

Ok, watch closely: we still have the magic ?_switch_user= in the URL. Hit enter. That's gone, yea! I'm logged in as spacebar1@example.com! You can see this down in the web debug toolbar. Of course, this normal user can't access this page. But if you go back to the homepage, you can surf around as the spacebar1 user.

User Provider & _switch_user

Oh, by the way, the reason that we use the email address with _switch_user, and not some other field like the id, is due to the user provider. Remember, this is the code inside Symfony that helps reload the user from the session at the beginning of each request. But it is also used by a few other features to load the user, like remember_me and switch_user. If you're using the Doctrine user provider like we are, then this property key determines which field will be used for all of this:

... lines 2 - 6
# used to reload user from session & other features (e.g. switch_user)
... line 11
property: email
... lines 13 - 57

If you changed this to id, we would need to use the id with switch user.

Adding a Banner when you are Impersonating

Anyways, to exit and return to your normal identity, find a phone booth, close the door, and add ?_switch_user=_exit to any URL. And... we're back to being us!

Switch one more time back to spacebar1@example.com. One of the only issues with _switch_user is that it's not super obvious that we're switched! Yep, you might switch to a user, go check Facebook, then come back, forget that you're still switched to them, and start commenting on their behalf. What? No, I've definitely never done this... I'm just saying it's possible.

To prevent these... awkward situations, let's put a big banner on top when we're switched. Open base.html.twig and find the body tag. Here's the key: when we are switched to another user, Symfony gives us a special role called ROLE_PREVIOUS_ADMIN. We can use that to our advantage: if is_granted('ROLE_PREVIOUS_ADMIN'), then print an alert block. Inside, say:

You are currently switched to this user

... line 1
<html lang="en">
... lines 3 - 15
{% if is_granted('ROLE_PREVIOUS_ADMIN') %}
<div class="alert alert-warning" style="margin-bottom: 0;">
You are currently switched to this user.
... line 20
{% endif %}
... lines 23 - 80

And, to maximize our fanciness, let's add a link to exit. Use the path function to point to app_homepage. For the second argument, pass an array with the necessary _switch_user set to _exit. At the end, say "Exit Impersonation":

... line 1
<html lang="en">
... lines 3 - 15
{% if is_granted('ROLE_PREVIOUS_ADMIN') %}
<div class="alert alert-warning" style="margin-bottom: 0;">
You are currently switched to this user.
<a href="{{ path('app_homepage', {'_switch_user': '_exit'}) }}">Exit Impersonation</a>
{% endif %}
... lines 23 - 80

Adding Query Parameters with path()

Let's see how it looks! Move over and refresh! Nice! Even I won't forget when I'm impersonating. And, check out the URL on the link: it's perfect - ?_switch_user=_exit. But... wait... the way we just used the path() function was a bit weird.

Why? Open templates/article/homepage.html.twig and find the article list. You might remember that the second argument of the path() function is normally used to fill in the "wild card" values for a route:

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
... lines 6 - 8
<div class="col-sm-12 col-md-8">
... lines 10 - 21
<div class="article-container my-1">
<a href="{{ path('article_show', {slug: article.slug}) }}">
... lines 24 - 38
... line 40
... lines 42 - 61
{% endblock %}

Hold Command or Control and click article_show. Yep! This route has a {slug} wild card:

... lines 1 - 13
class ArticleController extends AbstractController
... lines 16 - 37
* @Route("/news/{slug}", name="article_show")
public function show(Article $article, SlackClient $slack)
... lines 43 - 49
... lines 51 - 63

And so, when we link to it, we need to pass a value for that slug wildcard via the 2nd argument to path().

We already knew that. And this is the normal purpose of the second argument to path(). However, if you pass a key to the second argument, and that route does not have a wildcard with that name, Symfony just adds it as a query parameter.

That is why we can click this link to exit impersonation.

Next - let's build an API endpoint with Symfony's serializer! That will be our first step towards API authentication.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4