Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/FilamentImpersonateServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public function registeringPackage(): void

Event::listen(EnterImpersonation::class, fn () => $this->clearAuthHashes());
Event::listen(LeaveImpersonation::class, fn () => $this->clearAuthHashes());
Event::listen(Login::class, fn () => Impersonation::clear());
Event::listen(Logout::class, fn () => Impersonation::clear());
Event::listen(Login::class, fn (Login $event) => $this->clearImpersonationForGuard($event->guard));
Event::listen(Logout::class, fn (Logout $event) => $this->clearImpersonationForGuard($event->guard));

$this->registerIcon();
}
Expand All @@ -53,6 +53,35 @@ public function bootingPackage(): void
$this->loadViewsFrom(__DIR__.'/../resources/views', 'impersonate');
}

/**
* Clear impersonation in response to an auth event, but only when the event
* belongs to a guard involved in the active impersonation.
*
* Apps that share a single session across multiple guards (e.g. an admin
* guard alongside a separate customer/storefront guard) would otherwise have
* an active impersonation silently torn down when an unrelated guard fires
* Login/Logout — a customer logging in on the storefront, say. The
* impersonator's own session key is untouched, so they remain authenticated
* as the impersonated user but isImpersonating() returns false, leaving them
* with no banner and no way to leave. Only the impersonator's guard chain may
* end the impersonation.
*/
protected function clearImpersonationForGuard(?string $guard): void
{
if (Impersonation::isImpersonating()) {
$impersonationGuards = array_filter([
Impersonation::getImpersonatorGuardName(),
Impersonation::getImpersonatorGuardUsingName(),
]);

if ($guard !== null && $impersonationGuards !== [] && ! in_array($guard, $impersonationGuards, true)) {
return;
}
}

Impersonation::clear();
}

protected function clearAuthHashes(): void
{
$guards = collect([
Expand Down
70 changes: 70 additions & 0 deletions tests/ImpersonationClearOnAuthEventTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use STS\FilamentImpersonate\Facades\Impersonation;
use STS\FilamentImpersonate\Tests\User;

beforeEach(function () {
$this->admin = User::create([
'name' => 'Admin',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
]);

$this->targetUser = User::create([
'name' => 'Target User',
'email' => 'target@example.com',
'password' => bcrypt('password'),
]);

$this->actingAs($this->admin);
});

afterEach(function () {
if (Impersonation::isImpersonating()) {
Impersonation::leave();
}
});

it('keeps the impersonation when a login fires on an unrelated guard', function () {
Impersonation::enter($this->admin, $this->targetUser, 'web');

// A different guard sharing the same session (e.g. a customer/storefront
// guard) authenticates. This must not tear down the admin impersonation.
event(new Login('customer', $this->targetUser, false));

expect(Impersonation::isImpersonating())->toBeTrue();
});

it('keeps the impersonation when a logout fires on an unrelated guard', function () {
Impersonation::enter($this->admin, $this->targetUser, 'web');

event(new Logout('customer', $this->targetUser));

expect(Impersonation::isImpersonating())->toBeTrue();
});

it('clears the impersonation when a fresh login fires on the impersonator guard', function () {
Impersonation::enter($this->admin, $this->targetUser, 'web');

event(new Login('web', $this->admin, false));

expect(Impersonation::isImpersonating())->toBeFalse();
});

it('clears the impersonation when a logout fires on the impersonator guard', function () {
Impersonation::enter($this->admin, $this->targetUser, 'web');

event(new Logout('web', $this->targetUser));

expect(Impersonation::isImpersonating())->toBeFalse();
});

it('still clears on login when not impersonating', function () {
expect(Impersonation::isImpersonating())->toBeFalse();

event(new Login('customer', $this->targetUser, false));

expect(Impersonation::isImpersonating())->toBeFalse();
});
Loading