Digging A Little Deeper


It is not the end of the world if /profile returns one result for admin, and another for bob, and another again for dave. This route likely wouldn't be publicly available, so it's not that big of an issue, in my opinion.

However, generally, I prefer to have unique URIs, where possible.

In other words:

  • /profile/1 - admin
  • /profile/2 - bob
  • /profile/3 - dave

And so on.

Using the user's ID to make the URI unique.

How can we implement this with our setup?

We need to modify the ProfileController::getAction:

    /**
     * @Annotations\Get("/profile/{user}")
     * @ParamConverter("user", class="AppBundle:User")
     *
     * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
     */
    public function getAction(
        UserInterface $user
    )
    {
        if ($user === $this->getUser()) {
            return $user;
        }

        return new JsonResponse(
            'Access denied',
            JsonResponse::HTTP_FORBIDDEN
        );
    }

First up we've modified the route to require the {user} property.

This {user} property needs to be an ID.

Why?

Because we're using a ParamConvertor to convert the passed in value from user and converting it, behind the scenes, to the User object matching the given ID.

Whatever user ID we pass in will be looked up. Even if that's not us.

If we are user bob, and we have been added to the DB with an ID of 2, then we could still make a request to /profile/1, or /profile/3.

There's no way to stop this.

What we must protect against, therefore, is what if a user requests an ID that isn't their own?

We know that the ParamConvertor will have converted the given ID - 3 in this example - in to the User object representing the user with id: 3 - that's dave.

Fortunately, Symfony also offers us a convenience method from inside our controller actions:

$this->getUser().

As covered in the video, we can follow this through and figure out exactly how we get access to the real, currently logged in User.

We run a quick check here and say, hey, if we are logged in as bob, but we are accessing dave's profile, then Access Denied!

However, if we are logged in as bob, and accessing bob, then good times, let's return all the profile data we have for bob.

Who Are We?

We've covered how to add User Impersonation in stateless security setups in the previous two videos.

Symfony 3.4 makes this nice and easy for us.

Where things get interesting in a user impersonation situation is what value we get back from $this->getUser().

Let's log in as admin, and see what we get initially:

curl -X GET \
  http://127.0.0.1:81/app_dev.php/profile/1 \
  -H 'authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE1MDkxODkwNDEsImV4cCI6MTUwOTI3NTQ0MX0.oEZfqFCuaBp3inNLL8fyBIJ6fN_M0rP8vTJQycYNAivi_ufolM3nRxKjI8WO-Z_O9F46Qfoq2GnHhLfB3uON5ayRw-G5injGQbPisjlQFD_Rf5hPXQEgDUjkrjdply7yzEnPhXvk43b4IS6gB5l6b--SYaomtgQqptt8gyaCmu2NlUsBqf1WrhZaSjTCBRaQ9pU89EAC4-BbSMqXRspZfNDuVIYY_in2TxRlzXFia5KgQTSnLa_xprslpIuAFdeYLT-hJVAB7WXHLorhUQUM_UiLhGYjPQ30XBBHe5UMZhTDGFCE8I27JumAVEnTVhBnaB2XYq2JaT9DgVtHCWzQ2RJ_0W33fTU65A_ZEnxRWUf4XGJdPtaWL86CIwZXb79-XGxA53BrQ2hspMHLxeT_Q7LtlQxpwZK3EDdwFJvlRMDdsuIG_YHeSB1SpsDff90tpEXvORMGMISSSyPfE4QeDOvm84glEC-gSLDLqqbQ6QiE9RVsMSWCLQy-zMqUyxTPQ7sHehukkS_c1_T9r8d3sVfCHDhUADphECCpDHZ7COlx5zeY8M-PhjwDkAZzSxiXEMs0Eiaut_FMRuSv03RkCiDp_-Af6q1n89ri2qwFJ0fdJjfj5112J4pIFGEzAjwX0lcwHbv37TnoKD3jE_W_Lj2JR47t6b06vzY6_s5uAOU' \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \

We see our profile as expected.

Hitting either /profile/2, or /profile/3 rightly returns a 403, as we don't have the x-switch-user header, and those profiles aren't us.

It becomes difficult to use the dump command when using FOS REST Bundle, so instead I will make use of a more abrupt dump command provided by Doctrine:

    /**
     * @Annotations\Get("/profile/{user}")
     * @ParamConverter("user", class="AppBundle:User")
     *
     * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
     */
    public function getAction(
        UserInterface $user
    )
    {
        exit(\Doctrine\Common\Util\Debug::dump($user));
    }

Hitting the endpoint now dumps out the requested $user:

object(stdClass)#427 (15) {
  ["__CLASS__"]=>
  string(21) "AppBundle\Entity\User"
  ["id:protected"]=>
  int(1)
  ["username:protected"]=>
  string(5) "admin"
  ["usernameCanonical:protected"]=>
  string(5) "admin"
  ["email:protected"]=>
  string(17) "admin@example.com"
  ["emailCanonical:protected"]=>
  string(17) "admin@example.com"
  ["enabled:protected"]=>
  bool(true)
  ["salt:protected"]=>
  NULL
  ["password:protected"]=>
  string(60) "$2y$13$G0vtjZQ/QlKdOeF3uE3ve.UvDbj0md.OZJEW/uHWphhABGVIdo83e"
  ["plainPassword:protected"]=>
  NULL
  ["lastLogin:protected"]=>
  object(stdClass)#424 (3) {
    ["__CLASS__"]=>
    string(8) "DateTime"
    ["date"]=>
    string(25) "2017-10-28T13:14:53+01:00"
    ["timezone"]=>
    string(13) "Europe/London"
  }
  ["confirmationToken:protected"]=>
  NULL
  ["passwordRequestedAt:protected"]=>
  NULL
  ["groups:protected"]=>
  array(0) {
  }
  ["roles:protected"]=>
  array(1) {
    [0]=>
    string(16) "ROLE_SUPER_ADMIN"
  }
}

Ok, that's our user.

And if we hit /profile/2:

object(stdClass)#425 (15) {
  ["__CLASS__"]=>
  string(21) "AppBundle\Entity\User"
  ["id:protected"]=>
  int(2)
  ["username:protected"]=>
  string(3) "bob"
  ["usernameCanonical:protected"]=>
  string(3) "bob"
  ["email:protected"]=>
  string(15) "bob@example.com"
  ["emailCanonical:protected"]=>
  string(15) "bob@example.com"
  ["enabled:protected"]=>
  bool(true)
  ["salt:protected"]=>
  NULL
  ["password:protected"]=>
  string(60) "$2y$13$mNmI7PqRJjBzFlHLyv6nvuqaGLtCnFqu.qbQpzuLbN5kBeqM4KHcG"
  ["plainPassword:protected"]=>
  NULL
  ["lastLogin:protected"]=>
  NULL
  ["confirmationToken:protected"]=>
  NULL
  ["passwordRequestedAt:protected"]=>
  NULL
  ["groups:protected"]=>
  NULL
  ["roles:protected"]=>
  array(0) {
  }
}

As expected, the ParamConverter has converted the given id: 2 into the corresponding user with the id of 2: bob.

What about $this->getUser()?

    /**
     * @Annotations\Get("/profile/{user}")
     * @ParamConverter("user", class="AppBundle:User")
     *
     * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
     */
    public function getAction(
        UserInterface $user
    )
    {
        exit(\Doctrine\Common\Util\Debug::dump(
            $this->getUser()
        ));
    }

We can hit /profile/1:

object(stdClass)#427 (15) {
  ["__CLASS__"]=>
  string(21) "AppBundle\Entity\User"
  ["id:protected"]=>
  int(1)
  ["username:protected"]=>
  string(5) "admin"
  ["usernameCanonical:protected"]=>
  string(5) "admin"
  ["email:protected"]=>
  string(17) "admin@example.com"
  ["emailCanonical:protected"]=>
  string(17) "admin@example.com"
  ["enabled:protected"]=>
  bool(true)
  ["salt:protected"]=>
  NULL
  ["password:protected"]=>
  string(60) "$2y$13$G0vtjZQ/QlKdOeF3uE3ve.UvDbj0md.OZJEW/uHWphhABGVIdo83e"
  ["plainPassword:protected"]=>
  NULL
  ["lastLogin:protected"]=>
  object(stdClass)#424 (3) {
    ["__CLASS__"]=>
    string(8) "DateTime"
    ["date"]=>
    string(25) "2017-10-28T13:17:01+01:00"
    ["timezone"]=>
    string(13) "Europe/London"
  }
  ["confirmationToken:protected"]=>
  NULL
  ["passwordRequestedAt:protected"]=>
  NULL
  ["groups:protected"]=>
  array(0) {
  }
  ["roles:protected"]=>
  array(1) {
    [0]=>
    string(16) "ROLE_SUPER_ADMIN"
  }
}

We can see that $this->getUser() is our logged in user: admin.

To prove this, we can hit /profile/2, or /profile/3, and the dumped value is always admin.

Makes sense, right? That's the credentials we are providing in our JWT / JSON Web Token, which Symfony then uses to figure out who we are.

How Does User Impersonation Impact This Process?

Let's now introduce the impersonation header.

curl -X GET \
  http://127.0.0.1:81/app_dev.php/profile/1 \
  -H 'authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE1MDkxODkwNDEsImV4cCI6MTUwOTI3NTQ0MX0.oEZfqFCuaBp3inNLL8fyBIJ6fN_M0rP8vTJQycYNAivi_ufolM3nRxKjI8WO-Z_O9F46Qfoq2GnHhLfB3uON5ayRw-G5injGQbPisjlQFD_Rf5hPXQEgDUjkrjdply7yzEnPhXvk43b4IS6gB5l6b--SYaomtgQqptt8gyaCmu2NlUsBqf1WrhZaSjTCBRaQ9pU89EAC4-BbSMqXRspZfNDuVIYY_in2TxRlzXFia5KgQTSnLa_xprslpIuAFdeYLT-hJVAB7WXHLorhUQUM_UiLhGYjPQ30XBBHe5UMZhTDGFCE8I27JumAVEnTVhBnaB2XYq2JaT9DgVtHCWzQ2RJ_0W33fTU65A_ZEnxRWUf4XGJdPtaWL86CIwZXb79-XGxA53BrQ2hspMHLxeT_Q7LtlQxpwZK3EDdwFJvlRMDdsuIG_YHeSB1SpsDff90tpEXvORMGMISSSyPfE4QeDOvm84glEC-gSLDLqqbQ6QiE9RVsMSWCLQy-zMqUyxTPQ7sHehukkS_c1_T9r8d3sVfCHDhUADphECCpDHZ7COlx5zeY8M-PhjwDkAZzSxiXEMs0Eiaut_FMRuSv03RkCiDp_-Af6q1n89ri2qwFJ0fdJjfj5112J4pIFGEzAjwX0lcwHbv37TnoKD3jE_W_Lj2JR47t6b06vzY6_s5uAOU' \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -H 'x-switch-user: dave'

For clarity, that's the admin's JWT.

We want to switch to become dave.

In our controller action we have:

    /**
     * @Annotations\Get("/profile/{user}")
     * @ParamConverter("user", class="AppBundle:User")
     *
     * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
     */
    public function getAction(
        UserInterface $user
    )
    {
        exit(\Doctrine\Common\Util\Debug::dump($user));
    }

If we hit /profile/1, the ParamConverter still does the same thing. It converts the requested ID into the matched user:

object(stdClass)#539 (15) {
  ["__CLASS__"]=>
  string(21) "AppBundle\Entity\User"
  ["id:protected"]=>
  int(1)
  ["username:protected"]=>
  string(5) "admin"
  ...
}

And likewise, hitting /profile/2, or /profile/3 gets back the requested user by ID:

object(stdClass)#416 (15) {
  ["__CLASS__"]=>
  string(21) "AppBundle\Entity\User"
  ["id:protected"]=>
  int(2)
  ["username:protected"]=>
  string(3) "bob"
  ...

What happens when we dump $this->getUser():

    /**
     * @Annotations\Get("/profile/{user}")
     * @ParamConverter("user", class="AppBundle:User")
     *
     * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
     */
    public function getAction(
        UserInterface $user
    )
    {
        exit(\Doctrine\Common\Util\Debug::dump(
            $this->getUser()
        ));
    }

And we send a request:

curl -X GET \
  http://127.0.0.1:81/app_dev.php/profile/1 \
  -H 'authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE1MDkxODkwNDEsImV4cCI6MTUwOTI3NTQ0MX0.oEZfqFCuaBp3inNLL8fyBIJ6fN_M0rP8vTJQycYNAivi_ufolM3nRxKjI8WO-Z_O9F46Qfoq2GnHhLfB3uON5ayRw-G5injGQbPisjlQFD_Rf5hPXQEgDUjkrjdply7yzEnPhXvk43b4IS6gB5l6b--SYaomtgQqptt8gyaCmu2NlUsBqf1WrhZaSjTCBRaQ9pU89EAC4-BbSMqXRspZfNDuVIYY_in2TxRlzXFia5KgQTSnLa_xprslpIuAFdeYLT-hJVAB7WXHLorhUQUM_UiLhGYjPQ30XBBHe5UMZhTDGFCE8I27JumAVEnTVhBnaB2XYq2JaT9DgVtHCWzQ2RJ_0W33fTU65A_ZEnxRWUf4XGJdPtaWL86CIwZXb79-XGxA53BrQ2hspMHLxeT_Q7LtlQxpwZK3EDdwFJvlRMDdsuIG_YHeSB1SpsDff90tpEXvORMGMISSSyPfE4QeDOvm84glEC-gSLDLqqbQ6QiE9RVsMSWCLQy-zMqUyxTPQ7sHehukkS_c1_T9r8d3sVfCHDhUADphECCpDHZ7COlx5zeY8M-PhjwDkAZzSxiXEMs0Eiaut_FMRuSv03RkCiDp_-Af6q1n89ri2qwFJ0fdJjfj5112J4pIFGEzAjwX0lcwHbv37TnoKD3jE_W_Lj2JR47t6b06vzY6_s5uAOU' \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -H 'x-switch-user: dave'

Again, that's our admin's JWT, and user dave has id: 3:

object(stdClass)#539 (15) {
  ["__CLASS__"]=>
  string(21) "AppBundle\Entity\User"
  ["id:protected"]=>
  int(3)
  ["username:protected"]=>
  string(4) "dave"
  ["usernameCanonical:protected"]=>
  string(4) "dave"
  ["email:protected"]=>
  string(16) "dave@example.com"
  ["emailCanonical:protected"]=>
  string(16) "dave@example.com"
  ["enabled:protected"]=>
  bool(true)
  ["salt:protected"]=>
  NULL
  ["password:protected"]=>
  string(60) "$2y$13$OhGgsFSJynybdV//KAiglegca8Wl7Nr10ia4xe1cI/thMClHMm5MG"
  ["plainPassword:protected"]=>
  NULL
  ["lastLogin:protected"]=>
  NULL
  ["confirmationToken:protected"]=>
  NULL
  ["passwordRequestedAt:protected"]=>
  NULL
  ["groups:protected"]=>
  array(0) {
  }
  ["roles:protected"]=>
  array(0) {
  }
}

So now, Symfony believes we are dave, even though we gave admin's JWT.

If we take out the dump statements and revert to the original logic:

    /**
     * @Annotations\Get("/profile/{user}")
     * @ParamConverter("user", class="AppBundle:User")
     *
     * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
     */
    public function getAction(
        UserInterface $user
    )
    {
        if ($user === $this->getUser()) {
            return $user;
        }

        return new JsonResponse(
            'Access denied',
            JsonResponse::HTTP_FORBIDDEN
        );
    }

Then we can deduce that if we:

  • set the admin's JWT
  • set the x-switch-user to dave
  • send in a request for /profile/3

We should see Dave's profile, right?

curl -X GET \
  http://127.0.0.1:81/app_dev.php/profile/3 \
  -H 'authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE1MDkxODkwNDEsImV4cCI6MTUwOTI3NTQ0MX0.oEZfqFCuaBp3inNLL8fyBIJ6fN_M0rP8vTJQycYNAivi_ufolM3nRxKjI8WO-Z_O9F46Qfoq2GnHhLfB3uON5ayRw-G5injGQbPisjlQFD_Rf5hPXQEgDUjkrjdply7yzEnPhXvk43b4IS6gB5l6b--SYaomtgQqptt8gyaCmu2NlUsBqf1WrhZaSjTCBRaQ9pU89EAC4-BbSMqXRspZfNDuVIYY_in2TxRlzXFia5KgQTSnLa_xprslpIuAFdeYLT-hJVAB7WXHLorhUQUM_UiLhGYjPQ30XBBHe5UMZhTDGFCE8I27JumAVEnTVhBnaB2XYq2JaT9DgVtHCWzQ2RJ_0W33fTU65A_ZEnxRWUf4XGJdPtaWL86CIwZXb79-XGxA53BrQ2hspMHLxeT_Q7LtlQxpwZK3EDdwFJvlRMDdsuIG_YHeSB1SpsDff90tpEXvORMGMISSSyPfE4QeDOvm84glEC-gSLDLqqbQ6QiE9RVsMSWCLQy-zMqUyxTPQ7sHehukkS_c1_T9r8d3sVfCHDhUADphECCpDHZ7COlx5zeY8M-PhjwDkAZzSxiXEMs0Eiaut_FMRuSv03RkCiDp_-Af6q1n89ri2qwFJ0fdJjfj5112J4pIFGEzAjwX0lcwHbv37TnoKD3jE_W_Lj2JR47t6b06vzY6_s5uAOU' \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -H 'x-switch-user: dave'

And:

{
    "id": 3,
    "username": "dave",
    "usernameCanonical": "dave",
    "salt": null,
    "email": "dave@example.com",
    "emailCanonical": "dave@example.com",
    "password": "$2y$13$OhGgsFSJynybdV//KAiglegca8Wl7Nr10ia4xe1cI/thMClHMm5MG",
    "plainPassword": null,
    "lastLogin": null,
    "confirmationToken": null,
    "roles": [
        "ROLE_USER"
    ],
    "accountNonExpired": true,
    "accountNonLocked": true,
    "credentialsNonExpired": true,
    "enabled": true,
    "superAdmin": false,
    "passwordRequestedAt": null,
    "groups": [],
    "groupNames": []
}

Symfony now believes we are definitely dave.

So much so, in fact, that if we try to access our own profile, we are not allowed!

curl -X GET \
  http://127.0.0.1:81/app_dev.php/profile/1 \
  -H 'authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE1MDkxODkwNDEsImV4cCI6MTUwOTI3NTQ0MX0.oEZfqFCuaBp3inNLL8fyBIJ6fN_M0rP8vTJQycYNAivi_ufolM3nRxKjI8WO-Z_O9F46Qfoq2GnHhLfB3uON5ayRw-G5injGQbPisjlQFD_Rf5hPXQEgDUjkrjdply7yzEnPhXvk43b4IS6gB5l6b--SYaomtgQqptt8gyaCmu2NlUsBqf1WrhZaSjTCBRaQ9pU89EAC4-BbSMqXRspZfNDuVIYY_in2TxRlzXFia5KgQTSnLa_xprslpIuAFdeYLT-hJVAB7WXHLorhUQUM_UiLhGYjPQ30XBBHe5UMZhTDGFCE8I27JumAVEnTVhBnaB2XYq2JaT9DgVtHCWzQ2RJ_0W33fTU65A_ZEnxRWUf4XGJdPtaWL86CIwZXb79-XGxA53BrQ2hspMHLxeT_Q7LtlQxpwZK3EDdwFJvlRMDdsuIG_YHeSB1SpsDff90tpEXvORMGMISSSyPfE4QeDOvm84glEC-gSLDLqqbQ6QiE9RVsMSWCLQy-zMqUyxTPQ7sHehukkS_c1_T9r8d3sVfCHDhUADphECCpDHZ7COlx5zeY8M-PhjwDkAZzSxiXEMs0Eiaut_FMRuSv03RkCiDp_-Af6q1n89ri2qwFJ0fdJjfj5112J4pIFGEzAjwX0lcwHbv37TnoKD3jE_W_Lj2JR47t6b06vzY6_s5uAOU' \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -H 'x-switch-user: dave'

Results in a 403:

"Access denied"

Oh my.

Likewise, if we tried to access /profile/2, then we are also denied. That one, I guess, is a little more expected.

Is this a big deal?

It depends :)

Super Switch User

Before we go further with this, I want to state that I do not recommend implementing this in a real world project. This process is for exploration purposes.

Maybe you want the x-switch-user header to be a little more Super Administrative.

Maybe if you have the x-switch-user header, and the right roles, then you can switch to anyone without first updating your x-switch-user header value.

use Symfony\Component\Security\Core\Role\SwitchUserRole;

// ...

    /**
     * @Annotations\Get("/profile/{user}")
     * @ParamConverter("user", class="AppBundle:User")
     *
     * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
     */
    public function getAction(
        AuthorizationCheckerInterface $authorizationChecker,
        TokenStorageInterface $tokenStorage,
        UserInterface $user
    )
    {
        // new code
        if ($authorizationChecker->isGranted('ROLE_PREVIOUS_ADMIN')) {
            foreach ($tokenStorage->getToken()->getRoles() as $role) {
                if ($role instanceof SwitchUserRole) {
                    return $user;
                }
            }
        }

        if ($user === $this->getUser()) {
            return $user;
        }

        return new JsonResponse(
            'Access denied',
            JsonResponse::HTTP_FORBIDDEN
        );
    }

I'm not going to claim credit for this. It's an adaption of [this approach][3] from the official docs.

As per the official docs, we will need both the Authorization Checker, and Token Storage services. Using Symfony 3.3 autowiring, we can inject both of these directly into our controller action.

With this extra setup out of the way, we can get on with the task at hand:

if ($authorizationChecker->isGranted('ROLE_PREVIOUS_ADMIN')) {
    foreach ($tokenStorage->getToken()->getRoles() as $role) {
        if ($role instanceof SwitchUserRole) {
            return $user;
        }
    }
}

As soon as we switch users, our currently impersonated user gains the extra role of ROLE_PREVIOUS_ADMIN.

This would be that if we were impersonating dave, then dave would gain the extra role of ROLE_PREVIOUS_ADMIN.

The string of ROLE_PREVIOUS_ADMIN maps to an object of type SwitchUserRole. Notice here the extra use statement at the top of the above PHP code.

We can see this in better detail by dumping:

if ($authorizationChecker->isGranted('ROLE_PREVIOUS_ADMIN')) {

    exit(\Doctrine\Common\Util\Debug::dump($tokenStorage->getToken()->getRoles()));

    foreach ($tokenStorage->getToken()->getRoles() as $role) {
        if ($role instanceof SwitchUserRole) {
            return $user;
        }
    }
}

And sending through a request now dumps out:

array(2) {
  [0]=>
  object(stdClass)#535 (2) {
    ["__CLASS__"]=>
    string(41) "Symfony\Component\Security\Core\Role\Role"
    ["role:Symfony\Component\Security\Core\Role\Role:private"]=>
    string(9) "ROLE_USER"
  }
  [1]=>
  object(stdClass)#533 (3) {
    ["__CLASS__"]=>
    string(51) "Symfony\Component\Security\Core\Role\SwitchUserRole"
    ["source:Symfony\Component\Security\Core\Role\SwitchUserRole:private"]=>
    string(79) "Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken"
    ["role:Symfony\Component\Security\Core\Role\Role:private"]=>
    string(19) "ROLE_PREVIOUS_ADMIN"
  }
}

Remember here that $tokenStorage->getToken() represents the User object of whatever name we gave in the x-switch-user header value.

But now that header value isn't super important. The key is ... key. As long as we have the right key (x-switch-user), and whatever user supplies the original JWT has enough privileges to switch roles (e.g. admin), then we can quite happily view any profile we like - including our own :)

It's important to understand that this second approach has greater security implications than the original.

My approach is to use the first method, choosing to 'exit' impersonation (unset the x-switch-user header) and then switch to a different user as needed. It's really not much effort, and this second approach can lead to false positives, in so much as if impersonating bob then you shouldn't have access to dave.

Just because you can, doesn't mean that you should.

Code For This Course

Get the code for this course.

Episodes