PHPackages                             thecyrilcril/laravel-impersonate - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. thecyrilcril/laravel-impersonate

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

thecyrilcril/laravel-impersonate
================================

Modern user impersonation for Laravel — securely log in as another user for admin and support workflows.

v1.1.0(3w ago)147MITPHPPHP ^8.4CI passing

Since Jul 10Pushed 3w agoCompare

[ Source](https://github.com/thecyrilcril/laravel-impersonate)[ Packagist](https://packagist.org/packages/thecyrilcril/laravel-impersonate)[ RSS](/packages/thecyrilcril-laravel-impersonate/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)Dependencies (22)Versions (3)Used By (0)

Laravel Impersonate
===================

[](#laravel-impersonate)

User impersonation for Laravel. It lets an admin or support person "log in as" another user, fix a problem, and then switch back to their own account.

- Guard-aware: `take()` and `leave()` put the admin back on the guard they came from
- Blocks impersonating yourself, and blocks starting an impersonation inside another one
- Never changes anyone's `remember_token`, so nobody gets logged out of their other devices
- Fires events on take and leave, so you can build an audit trail
- Ships a middleware that keeps impersonators out of sensitive pages, and one that auto-ends stale impersonations
- Adds Blade directives for your views
- Needs nothing outside Laravel's own `illuminate/*` packages

Requirements
------------

[](#requirements)

- PHP 8.4 or newer
- Laravel 12 or 13

Installation
------------

[](#installation)

```
composer require thecyrilcril/laravel-impersonate
```

The service provider is found automatically. Publish the config if you want to change the defaults:

```
php artisan vendor:publish --tag=impersonate-config
```

This writes `config/impersonate.php`:

```
return [
    'session_key' => 'impersonate',
    'default_impersonator_guard' => config('auth.defaults.guard'),
    'leave_redirect_to' => '/',

    // Minutes before HandleImpersonationSession auto-ends an impersonation.
    // Zero (or a non-numeric value) turns expiry off.
    'ttl' => (int) env('IMPERSONATION_TTL_MINUTES', 30),
];
```

> **Session guards only.** Impersonation lives in the session. Both guards — the admin's and the target's — must be session guards. On a token or request guard (like Sanctum's `api` guard), `take()` does nothing and returns `false`. It never pretends to succeed.

Set up your User model
----------------------

[](#set-up-your-user-model)

Add the `ImpersonatesUsers` trait to your authenticatable model:

```
use Illuminate\Foundation\Auth\User as Authenticatable;
use Thecyrilcril\Impersonate\Concerns\ImpersonatesUsers;

final class User extends Authenticatable
{
    use ImpersonatesUsers;
}
```

The trait adds:

MethodWhat it does`impersonate(Authenticatable $target, ?string $guard = null): bool`Start impersonating `$target`. Returns `false` when not allowed, already impersonating, or the target is you.`leaveImpersonation(): bool`Stop impersonating and return to your own session.`isImpersonated(): bool`Is **this user** the one currently being impersonated? True for the target only — matched by class and id.`isImpersonator(): bool`Is **this user** the impersonator of the active impersonation? True for the admin only.`canImpersonate(): bool`May this user impersonate others? Defaults to `true` — override it.`canBeImpersonated(): bool`May others impersonate this user? Defaults to `true` — override it.**Mind the direction of the two state checks.** During impersonation the signed-in user *is* the target, so `Auth::user()->isImpersonated()` is true and `Auth::user()->isImpersonator()` is false. Neither method answers "is this session impersonating at all" — that is the manager's `isImpersonating()`. In particular, don't try to block impersonation chaining from inside `canImpersonate()` with `! $this->isImpersonated()`; that guard never fires for the actor starting a new impersonation. Gate chaining on the manager's own state instead:

```
if (app(\Thecyrilcril\Impersonate\Impersonate::class)->isImpersonating()) {
    abort(403, 'Cannot impersonate while impersonating.');
}
```

Override the two hooks to set your own rules:

```
public function canImpersonate(): bool
{
    return $this->hasRole('admin');
}

public function canBeImpersonated(): bool
{
    return ! $this->hasRole('super-admin');
}
```

> Don't add `#[\Override]` when overriding these hooks. They come from a trait, not a parent class, so PHP fatals at class-load time with "has #\[\\Override\] attribute, but no matching parent method exists".

> The `Impersonate` manager does **no** permission checks on its own. The trait's `impersonate()` checks `canImpersonate()` and `canBeImpersonated()`first, then hands off to the manager. If you call the manager directly, add your own checks (a policy, a gate, or the trait hooks).

Add your routes
---------------

[](#add-your-routes)

The package ships **no** routes on purpose — you wire them the way your app needs. Use POST (or DELETE) so the actions are CSRF-protected; never GET:

```
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;

Route::post('/impersonate/{user}', function (User $user) {
    Auth::user()->impersonate($user);

    return redirect('/');
})->middleware('auth')->name('impersonate.take');

Route::post('/impersonate/leave', function () {
    Auth::user()->leaveImpersonation();

    return redirect(config('impersonate.leave_redirect_to'));
})->middleware('auth')->name('impersonate.leave');
```

The manager — full API
----------------------

[](#the-manager--full-api)

You can also use the manager directly:

```
use Thecyrilcril\Impersonate\Impersonate;

$manager = app(Impersonate::class);

// Start and stop
$manager->take($admin, $target);            // uses the default guard
$manager->take($admin, $target, 'web');     // logs the target into a chosen guard
$manager->leave();                          // restores the admin

// State
$manager->isImpersonating();                // bool — impersonation keys exist in the session
$manager->isActive();                       // bool — impersonating AND not expired
$manager->getImpersonatorId();              // int|string|null — the admin's id
$manager->getImpersonatorType();            // ?string — the admin's model class, as captured at take()
$manager->getImpersonator();                // ?Authenticatable — the admin, or null if deleted
$manager->impersonatedUser();               // ?Authenticatable — the user being impersonated
$manager->impersonatingOnGuard();           // ?string — the guard the target is logged into
$manager->restoreGuard();                   // ?string — the guard the admin returns to on leave()

// Expiry (TTL)
$manager->startedAt();                      // ?int — unix time the impersonation began
$manager->hasExpired();                     // bool — true when it outlived the TTL
$manager->minutesRemaining();               // ?int — whole minutes left; null when expiry is off
```

Two things to know:

- `isImpersonating()` only says the session keys exist. An expired impersonation still reads as `true` until the session middleware cleans it up on the next request. **For any security decision, use `isActive()`.**
- `take()` returns `false` instead of throwing. That happens when the session is already impersonating (no nesting), the target is the same user, the admin's current guard cannot be safely identified, or either guard is not a session guard.

Multi-guard apps: after `leave()`, redirect the admin to a page protected by the guard `restoreGuard()` reports. A redirect to a page on a different guard bounces the just-restored admin to the login screen. Single-guard apps can ignore this.

Auto-ending stale impersonations (TTL)
--------------------------------------

[](#auto-ending-stale-impersonations-ttl)

The `ttl` config sets how many minutes an impersonation may live. **It only works when the `HandleImpersonationSession` middleware runs.** The package registers the alias `impersonate.session` but does not add the middleware to any group — do that yourself in `bootstrap/app.php`:

```
use Thecyrilcril\Impersonate\Http\Middleware\HandleImpersonationSession;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [
        HandleImpersonationSession::class,
    ]);
})
```

On every request, the middleware:

- ends the impersonation when it is older than the TTL,
- ends the impersonation when the impersonated user no longer exists.

Both cases redirect to `leave_redirect_to` and flash a reason into the session under the fixed key `impersonate.status`:

StatusMeaning`expired`The impersonation outlived the TTL.`target-missing`The impersonated user was deleted.Read that key in your layout to show a toast or banner. Set `ttl` to `0` to turn expiry off. Without the middleware, `ttl` has no effect and nothing expires.

Protecting sensitive routes
---------------------------

[](#protecting-sensitive-routes)

Add the `impersonate.protect` middleware to any route an impersonator must not reach — password changes, two-factor settings, account deletion. It returns `403` while impersonating:

```
Route::middleware(['auth', 'impersonate.protect'])->group(function () {
    Route::put('/settings/password', PasswordController::class);
    Route::post('/settings/two-factor', TwoFactorController::class);
});
```

Blade directives
----------------

[](#blade-directives)

```
@impersonating

        @csrf
        Leave impersonation

@endImpersonating

@impersonating('web')
    Impersonating on the web guard.
@endImpersonating

@canImpersonate
    Impersonate
@endCanImpersonate

@canImpersonate('admin')
    {{-- checks the user signed into the "admin" guard --}}
@endCanImpersonate

@canBeImpersonated($user)
    Log in as {{ $user->name }}
@endCanBeImpersonated
```

`@impersonating` and `@canImpersonate` take an optional guard name. `@canImpersonate` checks the signed-in user's `canImpersonate()` hook; `@canBeImpersonated($user)` checks the given user's `canBeImpersonated()` hook.

Events
------

[](#events)

Three events are fired. Every event carries an `occurredAt`(`DateTimeInterface`) stamped when the action happened — pass it to your audit log so a queued listener records the true time, not the time the job ran. All three use `SerializesModels`, so queued listeners are safe.

- `TakenImpersonation` — `impersonator`, `target`, `occurredAt`.
- `LeftImpersonation` — `impersonator`, `target`, `occurredAt`.
- `OrphanedImpersonationLeft` — fired instead of `LeftImpersonation` when the impersonator no longer exists (deleted mid-session), so the leave still reaches your audit log. Carries `impersonatorId` (int|string), a nullable `target`, and `occurredAt` — there is no impersonator model left to give you.

```
use Thecyrilcril\Impersonate\Events\OrphanedImpersonationLeft;
use Thecyrilcril\Impersonate\Events\TakenImpersonation;

Event::listen(TakenImpersonation::class, function (TakenImpersonation $event) {
    logger()->info('Impersonation started', [
        'impersonator' => $event->impersonator->getAuthIdentifier(),
        'target' => $event->target->getAuthIdentifier(),
        'at' => $event->occurredAt,
    ]);
});

Event::listen(OrphanedImpersonationLeft::class, function (OrphanedImpersonationLeft $event) {
    logger()->warning('Impersonation left; impersonator was deleted', [
        'impersonator_id' => $event->impersonatorId,
    ]);
});
```

Two integration notes:

- **The payloads are contracts, not models.** `impersonator` and `target` are typed as `Illuminate\Contracts\Auth\Authenticatable` because the package is guard-agnostic — a custom user provider doesn't have to return Eloquent models. Consumers that require a concrete `Model` (spatie/activitylog's `causedBy()` / `performedOn()`, for example) must narrow first:

    ```
    public function handle(TakenImpersonation $event): void
    {
        if (! $event->impersonator instanceof User || ! $event->target instanceof User) {
            return; // or log the unexpected Authenticatable
        }

        activity()->causedBy($event->impersonator)->performedOn($event->target)
            ->log('impersonation.taken');
    }
    ```
- **Don't register a discovered listener twice.** If your listener lives in `app/Listeners/`, Laravel discovers any public `handle*` method by its typed first parameter automatically — don't also wire it with `Event::listen()`, or every event fires it twice (two audit rows per impersonation). Check with `php artisan event:list`.

Guarding against reused ids (fingerprints)
------------------------------------------

[](#guarding-against-reused-ids-fingerprints)

Databases can hand an auto-increment id to a new row after the old one is deleted. If the admin's account is deleted mid-impersonation and a **new**user gets their old id, `leave()` could restore the wrong person. To close that door, give your model a stable fingerprint — a value that never moves to another human, like a UUID:

```
public function getImpersonationFingerprint(): string
{
    return $this->uuid;
}
```

The package captures the fingerprint at `take()` and checks it at `leave()`. A mismatch means a different human: the session is cleaned up instead of restored, and `OrphanedImpersonationLeft` is fired. This is opt-in — without the method, the package still requires the restored user's id **and** class to match what was captured.

How guard switching works
-------------------------

[](#how-guard-switching-works)

`take()` remembers the admin's id and guard, then swaps users quietly:

- No `Login`/`Logout` events fire, so the target's login history and "new device" emails stay clean.
- Remember tokens are never touched, so nobody is logged out of their other devices.
- The session id is regenerated, which blocks session fixation.

`leave()` reverses the swap: it logs out of the impersonation guard and logs the admin back into their original guard — again without touching remember tokens. Two overlapping take/leave calls on the same session (say, two browser tabs) are run one at a time when your cache store supports atomic locks.

Testing
-------

[](#testing)

```
composer test      # Pest
composer lint      # Pint
composer analyse   # PHPStan (larastan) level 7
```

License
-------

[](#license)

The MIT License (MIT). See [LICENSE.md](LICENSE.md).

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance95

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~26 days

Total

2

Last Release

23d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3ac5cd7ce72dc437b9ccc6111887fa9e174088c4c786fe9edc5c588b1f935db8?d=identicon)[thecyrilcril](/maintainers/thecyrilcril)

---

Top Contributors

[![thecyrilcril](https://avatars.githubusercontent.com/u/11295881?v=4)](https://github.com/thecyrilcril "thecyrilcril (17 commits)")

---

Tags

authenticationimpersonationlaravellaravelAuthenticationimpersonationimpersonatelogin-as

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/thecyrilcril-laravel-impersonate/health.svg)

```
[![Health](https://phpackages.com/badges/thecyrilcril-laravel-impersonate/health.svg)](https://phpackages.com/packages/thecyrilcril-laravel-impersonate)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

80427.1M249](/packages/laravel-mcp)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k31.8M163](/packages/laravel-cashier)[nuwave/lighthouse

A framework for serving GraphQL from Laravel

3.5k12.2M128](/packages/nuwave-lighthouse)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k16.3M159](/packages/laravel-pulse)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
