I'm implementing a simple impersonation system for Laravel with Sanctum. Earlier I used Tymondesigns/jwt-auth with Rickycezar/laravel-jwt-impersonate, but we recently abandoned Tymon JWT for Sanctum.
I had no luck implementing laravel-jwt-impersonate with Sanctum, or the original 404labfr/laravel-impersonate that it was forked from. So... I decided to try and implement a really simple impersonation system myself.
This is what I'm trying to do now:
When the admin calls the impersonate()
function I create a token for the user that is being impersonated. This token is returned to Frontend and used as bearer token. It seems to work well and after doing this the app acts as if I'm the impersonated user (since I'm actually "logged in" as that user).
public function impersonate($user)
{
$user = User::find($user);
return $user->createToken('IMPERSONATE token');
}
The next step is what I'm wondering about. How to end the impersonation. If I log out, the created impersonation token is removed, so all good there... But that means that the admin is now logged out and has to log in again.
I would like to log the admin back in with the earlier token. But I don't know how to return the token that admin was logged in with.
So my questions:
- How would you go about solving this?
- What obvious security risks do you see (impersonation is only allowed from the admin account and only non admins can be impersonated)?
CodePudding user response:
You're almost there! We've an app which is also using Sanctum, but using session instead of token. Before we to impersonate the user, we put the admin/actual user's id into a session:
$request->session()->put('impersonate', true); // if you need to check if session is impersonated or not
$request->session()->put('impersonate_admin_id', Auth::id());
Auth::login($user); // then impersonate
So we can log back into admin on logout/exit impersonation using the user id:
Auth::loginUsingId($request->session()->get('impersonate_admin_id'));
Although my example was session based, you get the gist. Since yours is token based, and you can't store this on a session or a cookie, I'd suggest either DB or Redis/Cache.
CodePudding user response:
This is the solution I went for. Turned out a little bit bigger than first anticipated. It seems to work just as fine as when we used Tymondesigns/jwt-auth with Rickycezar/laravel-jwt-impersonate. I use the same structure for the responses so Front End didn't need to make any changes.
Migration (never mind the mixed types for the id tables. It's because of our old database)
public function up()
{
Schema::create('impersonations', function (Blueprint $table) {
$table->id();
$table->bigInteger('personal_access_token_id')->unsigned();
$table->integer('user_id')->unsigned();
$table->timestamps();
$table->foreign('personal_access_token_id')->references('id')->on('personal_access_tokens')->cascadeOnDelete();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
});
}
User model gets these two functions
public function canImpersonate()
{
return $this->is_superadmin;
}
public function canBeImpersonated()
{
return !$this->is_superadmin;
}
Impersonate function
public function impersonate($userId)
{
$impersonator = auth()->user();
$persona = User::find($userId);
// Check if persona user exists, can be impersonated and if the impersonator has the right to do so.
if (!$persona || !$persona->canBeImpersonated() || !$impersonator->canImpersonate()) {
return false;
}
// Create new token for persona
$personaToken = $persona->createToken('IMPERSONATION token');
// Save impersonator and persona token references
$impersonation = new Impersonation();
$impersonation->user_id = $impersonator->id;
$impersonation->personal_access_token_id = $personaToken->accessToken->id;
$impersonation->save();
// Log out impersonator
$impersonator->currentAccessToken()->delete();
$response = [
"requested_id" => $userId,
"persona" => $persona,
"impersonator" => $impersonator,
"token" => $personaToken->plainTextToken
];
return response()->json(['data' => $response], 200);
}
Leave impersonation
public function leaveImpersonate()
{
// Get impersonated user
$impersonatedUser = auth()->user();
// Find the impersonating user
$currentAccessToken = $impersonatedUser->currentAccessToken();
$impersonation = Impersonation::where('personal_access_token_id', $currentAccessToken->id)->first();
$impersonator = User::find($impersonation->user_id);
$impersonatorToken = $impersonator->createToken('API token')->plainTextToken;
// Logout impersonated user
$impersonatedUser->currentAccessToken()->delete();
$response = [
"requested_id" => $impersonator->id,
"persona" => $impersonator,
"token" => $impersonatorToken,
];
return response()->json(['data' => $response], 200);
}