From 4341fc5d08291783a0b9fb913d55d8887fe3a3fb Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 3 Mar 2026 12:27:30 +0100 Subject: [PATCH 1/9] Instead of setting the 'tenancy_impersonating' session variable, store auth guard in 'tenancy_impersonation_guard' Also make `stopImpersonating()` able to keep the user logged in. --- src/Features/UserImpersonation.php | 16 ++++++++++------ tests/TenantUserImpersonationTest.php | 8 ++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index d286b8ba..91bb6789 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -61,9 +61,9 @@ class UserImpersonation implements Feature Auth::guard($token->auth_guard)->loginUsingId($token->user_id, $token->remember); - $token->delete(); + session()->put('tenancy_impersonation_guard', $token->auth_guard); - session()->put('tenancy_impersonating', true); + $token->delete(); return redirect($token->redirect_url); } @@ -76,16 +76,20 @@ class UserImpersonation implements Feature public static function isImpersonating(): bool { - return session()->has('tenancy_impersonating'); + return session()->has('tenancy_impersonation_guard'); } /** * Logout from the current domain and forget impersonation session. */ - public static function stopImpersonating(): void + public static function stopImpersonating(bool $logout = true): void { - auth()->logout(); + if ($logout) { + $guard = session()->get('tenancy_impersonation_guard'); - session()->forget('tenancy_impersonating'); + auth($guard)->logout(); + } + + session()->forget('tenancy_impersonation_guard'); } } diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index ea679357..e252ddc7 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -89,13 +89,13 @@ test('tenant user can be impersonated on a tenant domain', function () { ->assertSee('You are logged in as Joe'); expect(UserImpersonation::isImpersonating())->toBeTrue(); - expect(session('tenancy_impersonating'))->toBeTrue(); + expect(session('tenancy_impersonation_guard'))->toBe($token->auth_guard); // Leave impersonation UserImpersonation::stopImpersonating(); expect(UserImpersonation::isImpersonating())->toBeFalse(); - expect(session('tenancy_impersonating'))->toBeNull(); + expect(session('tenancy_impersonation_guard'))->toBeNull(); // Assert can't access the tenant dashboard pest()->get('http://foo.localhost/dashboard') @@ -135,13 +135,13 @@ test('tenant user can be impersonated on a tenant path', function () { ->assertSee('You are logged in as Joe'); expect(UserImpersonation::isImpersonating())->toBeTrue(); - expect(session('tenancy_impersonating'))->toBeTrue(); + expect(session('tenancy_impersonation_guard'))->toBe($token->auth_guard); // Leave impersonation UserImpersonation::stopImpersonating(); expect(UserImpersonation::isImpersonating())->toBeFalse(); - expect(session('tenancy_impersonating'))->toBeNull(); + expect(session('tenancy_impersonation_guard'))->toBeNull(); // Assert can't access the tenant dashboard pest()->get('/acme/dashboard') From c48aed940650f255ba2b6e379b5f81de51f20409 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 3 Mar 2026 12:28:42 +0100 Subject: [PATCH 2/9] Test that `stopImpersonating()` can keep the user authenticated --- tests/TenantUserImpersonationTest.php | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index e252ddc7..2e9b3c3a 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -148,6 +148,47 @@ test('tenant user can be impersonated on a tenant path', function () { ->assertRedirect('/login'); }); +test('stopImpersonating can keep the user authenticated', function() { + makeLoginRoute(); + + Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group(getRoutes(false)); + + $tenant = Tenant::create([ + 'id' => 'acme', + 'tenancy_db_name' => 'db' . Str::random(16), + ]); + + migrateTenants(); + + $user = $tenant->run(function () { + return ImpersonationUser::create([ + 'name' => 'Joe', + 'email' => 'joe@local', + 'password' => bcrypt('secret'), + ]); + }); + + // Impersonate the user + $token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard'); + + pest()->get('/acme/impersonate/' . $token->token) + ->assertRedirect('/acme/dashboard'); + + expect(UserImpersonation::isImpersonating())->toBeTrue(); + + // Stop impersonating without logging out + UserImpersonation::stopImpersonating(false); + + // The impersonation session key should be cleared + expect(UserImpersonation::isImpersonating())->toBeFalse(); + expect(session('tenancy_impersonation_guard'))->toBeNull(); + + // The user should still be authenticated + pest()->get('/acme/dashboard') + ->assertSuccessful() + ->assertSee('You are logged in as Joe'); +}); + test('tokens have a limited ttl', function () { Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes()); From 20044a878b5427d7cd571b0e70119218d49c30da Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Tue, 3 Mar 2026 23:00:31 +0100 Subject: [PATCH 3/9] tests: use literal value in auth guard assertion --- tests/TenantUserImpersonationTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index 2e9b3c3a..f9fed8ae 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -89,7 +89,8 @@ test('tenant user can be impersonated on a tenant domain', function () { ->assertSee('You are logged in as Joe'); expect(UserImpersonation::isImpersonating())->toBeTrue(); - expect(session('tenancy_impersonation_guard'))->toBe($token->auth_guard); + expect(session('tenancy_impersonation_guard'))->toBe('web'); + expect($token->auth_guard)->toBe('web'); // Leave impersonation UserImpersonation::stopImpersonating(); @@ -135,7 +136,8 @@ test('tenant user can be impersonated on a tenant path', function () { ->assertSee('You are logged in as Joe'); expect(UserImpersonation::isImpersonating())->toBeTrue(); - expect(session('tenancy_impersonation_guard'))->toBe($token->auth_guard); + expect(session('tenancy_impersonation_guard'))->toBe('web'); + expect($token->auth_guard)->toBe('web'); // Leave impersonation UserImpersonation::stopImpersonating(); From 920cb06d1f9079c7738a4471baab4be81b9ba997 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 10:23:30 +0200 Subject: [PATCH 4/9] Test that stopImpersonating only has an effect on the original guard Starting impersonation using 'web', then using a different guard and calling UserImpersonation::stopImpersonating() should log out the user from the 'web' guard stored in `session('tenancy_impersonation_guard')`. --- tests/TenantUserImpersonationTest.php | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index f9fed8ae..2bb3b910 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -191,6 +191,50 @@ test('stopImpersonating can keep the user authenticated', function() { ->assertSee('You are logged in as Joe'); }); +test('stopImpersonating logs out the user from the guard used while starting impersonation', function() { + Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group(getRoutes(false)); + + $tenant = Tenant::create([ + 'id' => 'acme', + 'tenancy_db_name' => 'db' . Str::random(16), + ]); + + migrateTenants(); + + $user = $tenant->run(function () { + return ImpersonationUser::create([ + 'name' => 'Joe', + 'email' => 'joe@local', + 'password' => bcrypt('secret'), + ]); + }); + + // Impersonate the user + $token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard'); + + pest()->get('/acme/impersonate/' . $token->token) + ->assertRedirect('/acme/dashboard'); + + expect(session('tenancy_impersonation_guard'))->toBe('web'); + + // Impersonation logged in the user using the current guard ('web') + expect(auth('web')->check())->toBeTrue(); + + config(['auth.guards.test' => [ + 'driver' => 'session', + 'provider' => 'users', + ]]); + + // Manually log in the user using a different guard + auth('test')->loginUsingId($user->id); + + // Should log out the user from the guard used for impersonation ('web') + UserImpersonation::stopImpersonating(); + + expect(auth('web')->check())->toBeFalse(); + expect(auth('test')->check())->toBeTrue(); +}); + test('tokens have a limited ttl', function () { Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes()); From 12895505332425b0ef704d390909e4e71c1e1a96 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 10:41:21 +0200 Subject: [PATCH 5/9] Rename test and use different current guard Originally, we just authenticated the user using a different guard, without actually switching to that guard. Now, we fully switch to the 'test' guard. --- tests/TenantUserImpersonationTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index 2bb3b910..e4ef9a80 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -191,7 +191,7 @@ test('stopImpersonating can keep the user authenticated', function() { ->assertSee('You are logged in as Joe'); }); -test('stopImpersonating logs out the user from the guard used while starting impersonation', function() { +test('stopImpersonating logs out the user from tenancy_impersonation_guard stored in session', function() { Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group(getRoutes(false)); $tenant = Tenant::create([ @@ -225,8 +225,9 @@ test('stopImpersonating logs out the user from the guard used while starting imp 'provider' => 'users', ]]); - // Manually log in the user using a different guard - auth('test')->loginUsingId($user->id); + // Switch guard from 'web' to 'test' and manually log in the user through 'test' + auth()->shouldUse('test'); + auth()->loginUsingId($user->id); // Should log out the user from the guard used for impersonation ('web') UserImpersonation::stopImpersonating(); From d17c7b3e86c1dabb650537f796e25a6c743d36c5 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 15:19:17 +0200 Subject: [PATCH 6/9] Make stopImpersonating throw an exception if no user is impersonated currently --- src/Features/UserImpersonation.php | 5 +++++ tests/TenantUserImpersonationTest.php | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 91bb6789..861c1f05 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -11,6 +11,7 @@ use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Database\Models\ImpersonationToken; use Stancl\Tenancy\Tenancy; +use Exception; class UserImpersonation implements Feature { @@ -84,6 +85,10 @@ class UserImpersonation implements Feature */ public static function stopImpersonating(bool $logout = true): void { + if (! static::isImpersonating()) { + throw new Exception('Not currently impersonating any user.'); + } + if ($logout) { $guard = session()->get('tenancy_impersonation_guard'); diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index e4ef9a80..dc421f73 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -234,6 +234,14 @@ test('stopImpersonating logs out the user from tenancy_impersonation_guard store expect(auth('web')->check())->toBeFalse(); expect(auth('test')->check())->toBeTrue(); + + expect(UserImpersonation::isImpersonating())->toBeFalse(); + + // tenancy_impersonation_guard isn't in the session anymore, + // stopImpersonating should throw an exception instead of logging out + expect(fn() => UserImpersonation::stopImpersonating())->toThrow(Exception::class); + + expect(auth()->check())->toBeTrue(); }); test('tokens have a limited ttl', function () { From 3c9d53258205bc024ece7e35acf7d4d5801716c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 13 Apr 2026 13:19:53 +0000 Subject: [PATCH 7/9] Fix code style (php-cs-fixer) --- src/Features/UserImpersonation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 861c1f05..034c1915 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Features; +use Exception; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; @@ -11,7 +12,6 @@ use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Database\Models\ImpersonationToken; use Stancl\Tenancy\Tenancy; -use Exception; class UserImpersonation implements Feature { From a5f5182db2469b4185bb2845e4f04fba9a688062 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 15 Apr 2026 08:58:32 +0200 Subject: [PATCH 8/9] Improve stopImpersonating docblock --- src/Features/UserImpersonation.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 034c1915..be2b01fd 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -81,7 +81,13 @@ class UserImpersonation implements Feature } /** - * Logout from the current domain and forget impersonation session. + * Stop user impersonation by forgetting the impersonation session. + * + * When $logout is true, the user will also be logged out + * from the impersonation guard stored in the session. + * + * Throws an exception if impersonation is not active + * (= the impersonation guard is not in the session). */ public static function stopImpersonating(bool $logout = true): void { From 142efdccd64a4a90a43dfbd0f50b674e86edade2 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 21 Apr 2026 14:04:59 +0200 Subject: [PATCH 9/9] Improve test name --- tests/TenantUserImpersonationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index dc421f73..b4e6f2d9 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -191,7 +191,7 @@ test('stopImpersonating can keep the user authenticated', function() { ->assertSee('You are logged in as Joe'); }); -test('stopImpersonating logs out the user from tenancy_impersonation_guard stored in session', function() { +test('stopImpersonating logs out the user from the impersonation guard stored in session', function() { Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group(getRoutes(false)); $tenant = Tenant::create([