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.
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(),
];
}
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',
]);
}