Secure, elegant user impersonation for Laravel applications.
Features | Installation | Quick Start | Usage | Configuration | Security | Exceptions | Development
Mirror is a Laravel package for safely logging in as another user. It is designed for admin panels, support tooling, QA workflows, and production applications that need impersonation without handing route handling, redirects, or authorization policy decisions to a package.
Mirror stores a signed impersonation payload in the session, restores the original user when leaving, supports multiple session guards, exposes lifecycle events for audit logs, and keeps the application in control of the HTTP flow.
Important
Mirror only works with Laravel guards backed by the session driver. Token, API, and stateless guards are intentionally rejected.
- Signed session state: HMAC-SHA256 verification using your Laravel
app.key. - Multi-guard support: resolve the authenticated impersonator guard and infer or explicitly set the target guard.
- Explicit authorization hooks: require models to decide who can impersonate and who can be impersonated.
- Custom context: attach signed metadata such as support reasons, ticket IDs, or workflow sources.
- TTL checks: detect expired impersonation sessions while letting your app decide the response.
- Blade directives: render UI based on active impersonation and model capabilities.
- Lifecycle events: audit
ImpersonationStartedandImpersonationStoppedevents.
- PHP
8.2or higher - Laravel
11,12, or13
Install the package with Composer:
composer require franbarbalopez/mirrorLaravel auto-discovers the service provider and facade alias. If you want to customize the TTL or session namespace, publish the configuration file:
php artisan vendor:publish --tag=mirrorThe published file is available at config/mirror.php.
Every model that can start or receive impersonation must implement Mirror\Contracts\Impersonatable.
use Illuminate\Foundation\Auth\User as Authenticatable;
use Mirror\Contracts\Impersonatable;
class User extends Authenticatable implements Impersonatable
{
public function canImpersonate(): bool
{
return $this->hasRole('admin');
}
public function canBeImpersonated(): bool
{
return ! $this->hasRole('super-admin');
}
}Note
Mirror does not prescribe your authorization model. Use roles, policies, permissions, feature flags, or any business rule that fits your application.
use App\Models\User;
use Mirror\Facades\Mirror;
public function impersonate(User $user)
{
Mirror::impersonate(
target: $user,
context: [
'reason' => request('reason'),
'ticket_id' => request('ticket_id'),
],
);
return redirect()->route('dashboard');
}use Mirror\Facades\Mirror;
public function leave()
{
$context = Mirror::leave();
audit('Impersonation ended', $context);
return redirect()->route('admin.users.index');
}Use the facade to impersonate a target user. Mirror resolves the current authenticated session guard as the impersonator guard.
Mirror::impersonate($user);Pass a guard when the target user should be authenticated through a specific guard:
Mirror::impersonate($user, guard: 'web');Attach signed context when you want to carry audit metadata across the impersonation lifecycle:
Mirror::impersonate(
target: $user,
guard: 'web',
context: [
'reason' => 'Support request',
'ticket_id' => 123,
'source' => 'admin-panel',
],
);Mirror prevents nested impersonation and throws ImpersonationAlreadyActive if the current session is already impersonating another user.
Mirror resolves guards in this order:
| Guard | Resolution |
|---|---|
| Impersonator guard | First authenticated Laravel guard using the session driver. |
| Target guard | Explicit guard argument, when provided. |
| Target guard | Target model guardName() method, guard_name attribute, or guard_name default property. |
| Target guard | First matching session guard whose provider model matches the target model. |
If multiple target guards match the same model, Mirror uses the first matching guard. Pass guard: explicitly when the choice matters.
Mirror::active(); // bool
Mirror::expired(); // bool
Mirror::impersonator(); // ?Authenticatable
Mirror::impersonated(); // ?Authenticatable
Mirror::context(); // arrayUse these methods to drive banners, route middleware, audit logs, or support tooling.
if (Mirror::active()) {
$impersonator = Mirror::impersonator();
$impersonated = Mirror::impersonated();
}The mirror.ttl value controls whether Mirror::expired() returns true. The default is 1800 seconds.
if (Mirror::active() && Mirror::expired()) {
$context = Mirror::leave();
return redirect()
->route('admin.users.index')
->with('warning', __('Impersonation expired.'));
}Tip
Mirror reports expiration, but does not force logout, redirect, or abort a request. Put your preferred behavior in middleware or controller code.
Set the TTL to null to disable expiration checks:
'ttl' => null,Mirror registers Blade condition directives for common UI checks.
@impersonating
<div class="alert">
You are impersonating {{ auth()->user()->name }}.
<a href="{{ route('impersonation.leave') }}">Exit</a>
</div>
@endimpersonating
@notImpersonating
<span>Normal session</span>
@endnotImpersonatingGuard-specific checks are supported:
@impersonating('web')
<span>Impersonating through the web guard</span>
@endimpersonatingCapability directives call the Impersonatable contract methods:
@canImpersonate
<a href="{{ route('admin.users.index') }}">Manage users</a>
@endcanImpersonate
@canBeImpersonated($user)
<form method="POST" action="{{ route('impersonation.start', $user) }}">
@csrf
<button type="submit">Impersonate</button>
</form>
@endcanBeImpersonatedMirror dispatches two events:
| Event | When |
|---|---|
Mirror\Events\ImpersonationStarted |
After the target user is logged in. |
Mirror\Events\ImpersonationStopped |
After the original impersonator is restored. |
Both events expose $impersonator, $impersonated, and $context.
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Mirror\Events\ImpersonationStarted;
Event::listen(ImpersonationStarted::class, function (ImpersonationStarted $event): void {
Log::info('User impersonation started', [
'impersonator_id' => $event->impersonator->getAuthIdentifier(),
'impersonated_id' => $event->impersonated->getAuthIdentifier(),
'context' => $event->context,
]);
});The default configuration is intentionally small:
return [
'ttl' => 1800,
'session' => [
'key' => env('MIRROR_SESSION_KEY', 'mirror.impersonation'),
],
];| Option | Default | Description |
|---|---|---|
ttl |
1800 |
Maximum age, in seconds, used by Mirror::expired(). Set to null to disable expiration checks. |
session.key |
mirror.impersonation |
Session namespace used to store the signed payload and signature. |
Mirror stores the impersonator ID, impersonator guard, target ID, target guard, start timestamp, and context in the session. That payload is signed with HMAC-SHA256 using config('app.key').
When Mirror reads impersonation state, it verifies the signature. If the payload or signature is missing or tampered with, Mirror clears the stored impersonation state and throws an exception.
Security behavior to be aware of:
- Both users must implement
Impersonatable. - The impersonator must return
truefromcanImpersonate(). - The target must return
truefromcanBeImpersonated(). - Only session-backed guards are accepted.
- Nested impersonation is rejected.
- Expiration is reported by
Mirror::expired(); your application decides how to enforce it.
Warning
Keep your Laravel APP_KEY private and stable. Rotating it invalidates existing Mirror signatures, which is normally desirable for security but can interrupt active impersonation sessions.
All package exceptions extend Mirror\Exceptions\MirrorException. For application code, the phase interfaces are usually the best catch points.
use Mirror\Exceptions\CannotLeaveImpersonation;
use Mirror\Exceptions\CannotStartImpersonation;
use Mirror\Facades\Mirror;
try {
Mirror::impersonate($user);
} catch (CannotStartImpersonation $exception) {
report($exception);
}
try {
Mirror::leave();
} catch (CannotLeaveImpersonation $exception) {
report($exception);
}| Phase interface | Raised by | Typical cause |
|---|---|---|
CannotStartImpersonation |
impersonate() |
Authorization failed, no authenticated session guard exists, target guard cannot be inferred, or impersonation is already active. |
CannotLeaveImpersonation |
leave() |
No active impersonation exists or stored state is invalid. |
CannotReadImpersonationState |
active(), expired(), impersonator(), impersonated(), context() |
Stored impersonation state cannot be trusted. |
Common concrete exceptions include CanNotImpersonate, CanNotBeImpersonated, CannotInferTargetGuard, GuardDoesNotUseSessionDriver, ImpersonationAlreadyActive, ImpersonationNotActive, InvalidImpersonationSignature, MissingAuthenticatedSessionGuard, and MissingImpersonationSignature.
Install dependencies:
composer installRun the full quality suite:
composer testUseful scripts:
| Command | Description |
|---|---|
composer test:lint |
Check formatting with Laravel Pint. |
composer lint |
Fix formatting with Laravel Pint. |
composer test:analyse |
Run PHPStan/Larastan. |
composer test:refactor |
Run Rector in dry-run mode. |
composer test:coverage |
Run Pest with exactly 100% coverage. |
composer fix |
Run Rector and Pint fixes. |
composer serve |
Build and serve the Orchestra Testbench workbench app. |
The test suite uses Pest and Orchestra Testbench to validate Mirror inside a Laravel application context.