Tips & Tidbits

Impersonating users in an SPA with Laravel Passport

Impersonating users is an awesome feature, particularly when investigating issues your users might have. In a standard Laravel application there's plenty of packages out there providing this functionality and it's pretty easy to write your own as well.

In a Single Page Application, it's a bit more difficult due to the lack of state - we can't just store the information in the session, we need to keep that logic on the front end.

Adding Impersonation to Laravel Passport

To allow users to "impersonate" other users in a SPA, we need to generate a new access token for that user, which the front end will use to call the APIs in place of the normal logged in user's token.

I've added a simple impersonate method on my AuthController that generates a token for the target user as long as the logged in user has permission to become the target user.

public function impersonate(User $target)
{
    // Make sure the logged in user is able to impersonate the target user.
    // In this case, i'm using a model policy, but you could do any checks required here.
    $this->authorize('impersonate', $target);
    
    // Generate a new personal access token for the user
    $token = $target->createToken(sprintf('impersonate-%s-%s', Auth::user()->email, now()->getTimestamp()));
    
    // Return the token for the front end to use.
    return [
        'token' => $token->accessToken,
        'expires_at' => $token->token->expires_at->toIso8601String(),
    ];
}

Using the token in the SPA (Single Page Application)

Now we've got a route to generate a token, we can setup a method to swap all requests to use the provided token. Here's some psudeocode for how you could swap the tokens out, but this will depend on the front end tech you are using.

function impersonate(userId) {
  const res = await window.axiois.post(`/api/impersonate/${userId}`);

  // Set the `impersonating` property to the token received from the API.
  store.auth.impersonating = res.data;

  // Trigger a refresh of all the data in the store, so it's fetched using the impersonated user's token
  store.refreshAll();
}

You'll also need to add some logic to your API client instructing it to use the impersonate token if it exists. Something like this:

function getRequestOptions() {
  config.headers['X-CSRF-TOKEN'] = window.csrfToken;
  config.headers['X-Requested-With'] = 'XMLHttpRequest';
  config.headers['X-Client'] = 'XYZ';
  
  if (store.auth && store.auth.impersonating) {
    config.headers['Authorization'] = 'Bearer ' + store.auth.impersonating.token
  } else if (store.auth) {
      config.headers['Authorization'] = 'Bearer ' + store.auth.token
  }

  return config;
}

You'll also probably want to display some kind of UI indication that impersonation is active, with a button to return to the previously logged in user. In our example, this as as simple as setting store.auth.impersonating = null.

While it's not required, it's probably a good idea to revoke the access token once the impersonation finishes. You could either just to make a request to your /logout endpoint, or add a dedicated route to revoke the tokens.

public function logout(Request $request)
{
    $request->user()->token()->revoke();

    return response()->json([
        'message' => 'Successfully revoked token',
    ]);
}