hapidjus/laravel-impersonate-ui
Laravel Impersonate UI adds a simple floating menu for selecting users to impersonate with 404labfr/laravel-impersonate. Configure allowed emails, icon position, auto-submit, global include, and redirects; publish config and Blade views.
Prerequisites. This package is a UI layer, not an impersonation engine. It wraps 404labfr/laravel-impersonate, so Composer will pull that in as a dependency and that is what actually performs the session swap. If you already run a first-party impersonation primitive, stop here — you'd be adding a second mechanism, not simplifying.
First five minutes:
composer require hapidjus/laravel-impersonate-ui
php artisan vendor:publish --provider="Hapidjus\ImpersonateUI\ImpersonateUiServiceProvider"
Then add the trait to your authenticatable model:
use Lab404\Impersonate\Models\Impersonate;
class User extends Authenticatable
{
use Notifiable, Impersonate;
}
--tag=config and --tag=view publish the config and Blade templates independently. Publish both — you will almost certainly edit the view.
Where to look first, in order:
config/impersonate-ui.php — nine keys, all legible. Read it before anything else; the defaults encode the package's opinions and two of them are wrong for production.resources/views/vendor/impersonate-ui/impersonate-ui.blade.php — this is the whole UI: a floating icon, a <select> of users, and a form posting to the base library's take route.config/impersonate.php (from the base library, if published) — session key, redirect targets, middleware. The two configs overlap on redirects; know which one wins.First real use case: a staging environment where support/QA can pick a user from a dropdown and see the app exactly as they do. Concretely:
// .env.staging
IMPERSONATE_UI_ENABLED=true
APP_DEBUG=false
// config/impersonate-ui.php — the two changes to make before first use
'enabled' => env('IMPERSONATE_UI_ENABLED', false),
'global_include' => false,
Then drop the partial into your admin layout only:
{{-- resources/views/layouts/admin.blade.php --}}
@can('impersonate')
@include('impersonate-ui::impersonate-ui')
@endcan
That single @can is the difference between "operator ergonomics" and "an unauthenticated privilege escalation on every page." Define the gate yourself; the package's email allowlist is a filter, not an authorization system.
Gate-first, widget-second. Treat the package's users_allowed_to_impersonate as a redundant second filter and put a real policy in front:
// AuthServiceProvider::boot()
Gate::define('impersonate', fn (User $user) => $user->hasRole('support'));
@can('impersonate')
@include('impersonate-ui::impersonate-ui')
@endcan
If you use global_include => true, the package injects the widget without any gate check. That's the default. Almost nobody should keep it.
Bypass the widget, keep the mechanics. The floating icon is a novelty; the useful part is the <select> + form. If you already have an admin user table, render your own and post to the base library's route:
<form method="POST" action="{{ route('impersonate.take') }}">
@csrf
<input type="hidden" name="id" value="{{ $user->id }}">
<button>Impersonate</button>
</form>
You lose the package entirely and keep identical behavior — worth doing once your user list exceeds a few dozen rows. Also consider a leave affordance visible while impersonating, since the widget's own state on that page depends on global_include:
@if (session()->has('impersonated_by'))
<form method="POST" action="{{ route('impersonate.leave') }}">
@csrf
<button>Return to {{ auth()->user()->name }}'s account</button>
</form>
@endif
Audit logging is yours to build. The package emits nothing. Hook the base library's events and make the listener a first-class ticket, not a follow-on:
// EventServiceProvider
protected $listen = [
\Lab404\Impersonate\Events\TakeImpersonation::class => [LogImpersonationStarted::class],
\Lab404\Impersonate\Events\LeaveImpersonation::class => [LogImpersonationEnded::class],
];
Capture actor ID, target ID, timestamp, source IP, and a required reason string. Without this, you cannot answer "who looked at this customer's account" — which is the first question any review will ask.
Scope the picker to the tenant. The package ships users_only (ID allowlist) and users_exclude (ID denylist), but both are static config arrays — useless for dynamic scoping. Use a policy instead so a support agent can only reach users in their assigned accounts:
Gate::define('impersonate-target', fn (User $actor, User $target) =>
$actor->can('impersonate') && $target->tenant_id === $actor->tenant_id
);
Then render your own filtered list rather than the package's full-table select.
Redirect targets are per-environment, not per-scenario. take_redirect_to and leave_redirect_to accept 'back' or a URI. 'back' is convenient in staging and traps operators in production — you impersonate someone, land on their dashboard, and the leave button is on a page they can't reach. Prefer an explicit route:
'take_redirect_to' => '/admin/support',
'leave_redirect_to' => '/admin/support',
Verify session semantics against your guards. The mechanism is a session swap. Exercise it explicitly in a feature test:
public function test_leave_restores_original_actor(): void
{
$admin = User::factory()->admin()->create();
$user = User::factory()->create();
$this->actingAs($admin)
->post(route('impersonate.take'), ['id' => $user->id])
->assertRedirect();
$this->assertAuthenticatedAs($user);
$this->post(route('impersonate.leave'))->assertRedirect();
$this->assertAuthenticatedAs($admin);
}
Add cases for: impersonation does not survive logout, A→B→A loops are blocked, and token-based guards (Sanctum) behave sanely — they typically don't participate in the swap at all.
Restyle before shipping. Publish the view and rebuild it. The default markup carries its own inline styling and JS that will collide with your design system and, depending on the version, with a strict CSP.
The README's last line is "Do not use in production!" That is a maintainer telling you the defaults are unsafe. Whether you ship it is your call, but you now own the hardening scope: RBAC gate, audit trail, production-safe enabled, CSP verification, CSS.
enabled defaults to env('APP_DEBUG', false). A single APP_DEBUG=true on a production box mounts an impersonation widget for everyone matching the allowlist. Decouple it immediately:
'enabled' => env('IMPERSONATE_UI_ENABLED', false),
global_include defaults to true. This injects the partial into every rendered page in the layout stack — including customer-facing ones — whenever enabled is on. Force it to false and inject deliberately into the admin layout.
The allowlist is email-keyed. users_allowed_to_impersonate matches literal addresses. Staff email changes silently grant or revoke impersonation rights; there's no group or role semantics. Never make this the primary control.
show_button => false auto-submits on selection. One click, no confirmation, and the interaction depends on inline JS. Under a restrictive CSP it fails silently — the dropdown appears to do nothing. Test with your real CSP headers during the spike; it's cheap now and expensive in a pilot. Keep show_button => true.
'back' redirects can strand you. See above. If you keep them, make sure a leave affordance is reachable from anywhere a target user can land.
The picker loads every user. There is no search, no pagination, no AJAX. Past a few hundred rows the select becomes a wall of names and the HTML payload gets ugly. Either scope users_only tightly or replace the widget.
Config and view caching bite on the first deploy. After publishing or editing:
php artisan config:clear && php artisan view:clear
config:cache will lock in whatever the env looked like at cache time — a classic way to ship enabled => false to staging and wonder why the icon is missing.
Version drift is real. 37 stars, zero dependents, a single maintainer, and a release tagged with a future date. Pin the exact version, diff the tag against master before trusting it, and assign a named code owner. Treat the stated floors (Laravel ≥7.0, PHP ≥7.1; Laravel 11/12 in 1.8) as claims, not guarantees — run composer why-not against your pinned framework and PHP pair before you commit.
Two configs, overlapping keys. config/impersonate-ui.php and the base library's config/impersonate.php both express redirect targets. If both are published and set, you'll get confusing behavior. Pick one source of truth and document it.
Extension points are narrow by design: publish and own the view, publish and edit the config, listen to the base library's take/leave events, and gate the @include. There is no service class to decorate or driver to swap. Plan your customization around those four levers.
Before you invest further, answer honestly: fork-and-harden this UI, or write the forty lines of Blade that post to impersonate.take yourself? The package's value is the picker markup and the config surface. The session mechanics, the security model, and the audit trail all live in the dependency — and the audit trail is the expensive part either way.
How can I help you explore Laravel packages today?