franbarbalopez / mirror
Mirror is a Laravel package that handles user impersonation.
Requires
- php: ^8.2
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.10.0
- laravel/pint: ^1.29.1
- orchestra/testbench: ^9.15.0|^10.9.0|^11.1
- pestphp/pest: ^3.8.4|^4.7.2
- pestphp/pest-plugin-laravel: ^3.2.0|^4.1
- pestphp/pest-plugin-type-coverage: ^3.6.1|^4.0.4
- rector/rector: ^2.4.5
README
Mirror
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.
Features
- 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.
Requirements
- PHP
8.2or higher - Laravel
11,12, or13
Installation
Install the package with Composer:
composer require franbarbalopez/mirror
Laravel 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=mirror
The published file is available at config/mirror.php.
Quick Start
1. Implement the Contract
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.
2. Start Impersonating
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'); }
3. Leave Impersonation
use Mirror\Facades\Mirror; public function leave() { $context = Mirror::leave(); audit('Impersonation ended', $context); return redirect()->route('admin.users.index'); }
Usage
Starting Impersonation
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.
Guard Resolution
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.
Reading State
Mirror::active(); // bool Mirror::expired(); // bool Mirror::impersonator(); // ?Authenticatable Mirror::impersonated(); // ?Authenticatable Mirror::context(); // array
Use these methods to drive banners, route middleware, audit logs, or support tooling.
if (Mirror::active()) { $impersonator = Mirror::impersonator(); $impersonated = Mirror::impersonated(); }
Expiration
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,
Blade Directives
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> @endnotImpersonating
Guard-specific checks are supported:
@impersonating('web') <span>Impersonating through the web guard</span> @endimpersonating
Capability 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> @endcanBeImpersonated
Events
Mirror 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, ]); });
Configuration
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. |
Security
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.
Exceptions
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.
Development
Install dependencies:
composer install
Run the full quality suite:
composer test
Useful 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.