lab404/laravel-impersonate
Laravel Impersonate lets administrators log in as any user for support and debugging. It offers Blade directives, middleware, and guard helpers to safely start and stop impersonation without passwords, supporting multiple guards while keeping the original session recoverable.
Install and rely on package discovery (Laravel 5.5+); there is nothing to register manually:
composer require lab404/laravel-impersonate
php artisan vendor:publish --provider="Lab404\Impersonate\ImpersonateServiceProvider" --tag=config
Publish the config early — it gives you session_key (default impersonated_by), take_redirect_to, leave_redirect_to, and the default guard. Then add the trait to your authenticatable model and lock it down immediately, because both policy methods return true by default:
// app/Models/User.php
use Lab404\Impersonate\Models\Impersonate;
class User extends Authenticatable
{
use Impersonate;
public function canImpersonate(): bool
{
return $this->hasRole('support') || $this->hasRole('admin');
}
public function canBeImpersonated(): bool
{
return ! $this->hasAnyRole(['admin', 'billing_owner']) && ! $this->mfa_enrolled;
}
}
The first real use case is a support console button. Two POST routes — take and leave — plus a persistent banner, is the whole happy path:
Route::middleware('auth')->group(function () {
Route::post('/impersonate/{user}', [ImpersonationController::class, 'take'])->name('impersonate.take');
Route::post('/impersonate/leave', [ImpersonationController::class, 'leave'])->name('impersonate.leave');
});
public function take(Request $request, User $user)
{
abort_unless($request->user()->canImpersonate(), 403);
abort_unless($user->canBeImpersonated(), 403);
abort_if($request->user()->is($user), 403);
$request->user()->impersonate($user);
return redirect()->route('dashboard');
}
public function leave(Request $request)
{
$request->user()->leaveImpersonate();
return redirect()->route('admin.users.index');
}
After that, the two things you'll reach for daily are is_impersonating() / Impersonate::isImpersonating() and Impersonate::getImpersonator().
Guard the route, not just the model. canImpersonate() is a model method — pair it with a gate or middleware on the route so a misconfigured model can't expose the endpoint:
Route::post('/impersonate/{user}', ...)->middleware('can:impersonate-users');
Always ship a banner. The package deliberately provides no UI. A layout-level block is the cheapest safety net you'll build:
@impersonating
<div class="impersonation-banner">
Impersonating <strong>{{ auth()->user()->name }}</strong>.
<form method="POST" action="{{ route('impersonate.leave') }}">
@csrf
<button>Leave</button>
</form>
</div>
@endImpersonating
Directives available: @impersonating / @endImpersonating, @canImpersonate, @canBeImpersonated (each accepts an optional guard name), plus the helpers is_impersonating(), can_impersonate(), can_be_impersonated($user).
Log via events, not controllers. Lab404\Impersonate\Events\TakeImpersonation and LeaveImpersonation both carry $impersonator and $impersonated as Authenticatable instances. Put the audit write in a listener so it fires no matter which entry point triggers impersonation:
Event::listen(TakeImpersonation::class, function (TakeImpersonation $e) {
ImpersonationLog::create([
'impersonator_id' => $e->impersonator->getAuthIdentifier(),
'impersonated_id' => $e->impersonated->getAuthIdentifier(),
'ip' => request()->ip(),
'created_at' => now(),
]);
});
Block sensitive routes with the shipped middleware. impersonate.protect (aliased by the service provider to Lab404\Impersonate\Middleware\ProtectAgainstImpersonation) aborts when impersonating. Apply it to password change, email change, MFA enrolment, billing, and account deletion — never globally:
Route::middleware('impersonate.protect')->group(function () {
Route::get('/settings/security', ...);
Route::post('/billing/payment-method', ...);
});
Multi-guard is the differentiator — use it deliberately. Pass the guard explicitly and keep both models trait-equipped:
auth('admin')->user()->impersonate($customer, 'web');
Impersonate::getImpersonator(); // resolved back through the original guard
@impersonating('web') ... @endImpersonating
Note the guard-name handling in getImpersonator() and in "can't leave" was fixed only in 1.7.2 — pin at least that.
Feature-test the switch. Asserting on the session key and the authenticated user catches almost every wiring mistake:
$this->actingAs($support)->post(route('impersonate.take', $customer))->assertRedirect();
$this->assertAuthenticatedAs($customer);
$this->assertTrue(session()->has(config('impersonate.session_key')));
canImpersonate() and canBeImpersonated() return true out of the box. Overriding them is step one, not a later hardening task.impersonator_id into your log context on every request while is_impersonating() is true.Auth::onceUsingId()-style auth won't participate. If your app is API-first, this is the wrong tool.leaveImpersonate() or session expiry — an abandoned tab stays impersonated for hours. Consider a timestamp in the session checked by middleware, or a short session lifetime for support sessions.password.confirm leakage. Laravel's recent-password-confirmation flag lives in the session, so an impersonated session can inherit the admin's confirmation window. Keep impersonate.protect on anything behind password.confirm.auth()->user() later.redirect() yourself, take_redirect_to / leave_redirect_to never apply. 1.7.6 added dynamic redirection on leave — check which path you're actually on before debugging a redirect that "doesn't respect config."MissingUserProvider / InvalidUserProvider (1.7.0+) mean the guard in config/auth.php has no provider or a non-Eloquent one. It fails loudly instead of silently — good, but it will surface at impersonation time, not boot time, so smoke-test each guard in staging.getAuthIdentifier(), so UUID/ULID models work — but only past 1.5.1/1.7.0. Older versions assumed id, including in can_be_impersonated().session_key to something app-specific or you'll get cross-app impersonation reads.canImpersonate / canBeImpersonated overrides and in event listeners around the trait — the package is intentionally a primitive, not a governance layer. Don't fork it to add approval workflows; wrap it.How can I help you explore Laravel packages today?