From 842d3a25fb97ee5f3ec8c7e1a2aa30552d737607 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 18:21:39 +0100 Subject: [PATCH 1/5] add \`guard\` param to the user tags Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011p6KWCwGqfnEv1ntSxnYUD --- src/Auth/UserTags.php | 33 ++++++++++++++++-------- tests/Tags/User/UserTagsTest.php | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/Auth/UserTags.php b/src/Auth/UserTags.php index 4ffe3c487c2..c4abde581ad 100644 --- a/src/Auth/UserTags.php +++ b/src/Auth/UserTags.php @@ -76,7 +76,7 @@ public function index() // No user found? Get the current one. if (! $user) { - if (! $user = User::current()) { + if (! $user = $this->currentUser()) { return $this->parseNoResults(); } } @@ -84,6 +84,15 @@ public function index() return $this->aliasedResult($user); } + private function currentUser() + { + if (! $guard = $this->params->get('guard')) { + return User::current(); + } + + return User::fromUser(auth($guard)->user()); + } + /** * Alias of the {{ user }} tag. * @@ -420,6 +429,10 @@ public function logoutUrl() $queryParams['redirect'] = $redirect; } + if ($guard = $this->params->get('guard')) { + $queryParams['guard'] = $guard; + } + return route('statamic.logout', $queryParams); } @@ -430,7 +443,7 @@ public function logoutUrl() */ public function logout() { - auth()->logout(); + auth($this->params->get('guard'))->logout(); abort(redirect($this->params->get('redirect', '/'), $this->params->get('response', 302))); } @@ -575,12 +588,12 @@ public function resetPasswordForm() */ public function can() { - if (! $user = User::current()) { + if (! $user = $this->currentUser()) { return $this->parser ? null : false; } $permissions = Arr::wrap($this->params->explode(['permission', 'do'])); - $arguments = $this->params->except(['permission', 'do'])->all(); + $arguments = $this->params->except(['permission', 'do', 'guard'])->all(); foreach ($permissions as $permission) { if ($user->can($permission, $arguments)) { @@ -600,12 +613,12 @@ public function can() */ public function cant() { - if (! $user = User::current()) { + if (! $user = $this->currentUser()) { return $this->parser ? $this->parse() : true; } $permissions = Arr::wrap($this->params->explode(['permission', 'do'])); - $arguments = $this->params->except(['permission', 'do'])->all(); + $arguments = $this->params->except(['permission', 'do', 'guard'])->all(); $can = false; @@ -632,7 +645,7 @@ public function cant() */ public function is() { - if (! $user = User::current()) { + if (! $user = $this->currentUser()) { return $this->parser ? null : false; } @@ -660,7 +673,7 @@ public function is() */ public function isnt() { - if (! $user = User::current()) { + if (! $user = $this->currentUser()) { return $this->parser ? $this->parse() : true; } @@ -695,7 +708,7 @@ public function isnt() */ public function in() { - if (! $user = User::current()) { + if (! $user = $this->currentUser()) { return $this->parser ? null : false; } @@ -719,7 +732,7 @@ public function in() */ public function notIn() { - if (! $user = User::current()) { + if (! $user = $this->currentUser()) { return $this->parser ? $this->parse() : true; } diff --git a/tests/Tags/User/UserTagsTest.php b/tests/Tags/User/UserTagsTest.php index 3f1cdb73efd..a08f37c8068 100644 --- a/tests/Tags/User/UserTagsTest.php +++ b/tests/Tags/User/UserTagsTest.php @@ -133,6 +133,30 @@ public function it_renders_user_in_tag_content() $this->assertEquals('', $this->tag('{{ user:not_in group="favourite|non_favourite" }}yes{{ /user:not_in }}')); } + #[Test] + #[DataProvider('guardProvider')] + public function it_renders_tag_content_for_a_specific_guard($tag, $expected) + { + config()->set('auth.guards.statamic', config('auth.guards.web')); + $this->setTestRoles(['admin' => ['configure collections']]); + $this->setTestUserGroups(['favourite' => ['admin']]); + + auth('statamic')->login(User::make()->email('admin@example.com')->assignRole('admin')->addToGroup('favourite')->save()); + + $this->assertEquals('', $this->tag(sprintf($tag, ''))); + $this->assertEquals($expected, $this->tag(sprintf($tag, ' guard="statamic"'))); + } + + public static function guardProvider() + { + return [ + 'user' => ['{{ user%s }}{{ email }}{{ /user }}', 'admin@example.com'], + 'can' => ['{{ user:can%s do="configure collections" }}yes{{ /user:can }}', 'yes'], + 'is' => ['{{ user:is%s role="admin" }}yes{{ /user:is }}', 'yes'], + 'in' => ['{{ user:in%s group="favourite" }}yes{{ /user:in }}', 'yes'], + ]; + } + #[Test] public function it_can_logout_user() { @@ -167,12 +191,32 @@ public function it_can_logout_user_with_custom_redirect() $this->assertEquals(url('home'), $exception->getResponse()->getTargetUrl()); } + #[Test] + public function it_can_logout_user_from_a_specific_guard() + { + config()->set('auth.guards.statamic', config('auth.guards.web')); + + auth('statamic')->login(User::make()->save()); + auth('web')->login(User::make()->save()); + + try { + $this->tag('{{ user:logout guard="statamic" }}'); + } catch (HttpResponseException $exception) { + // + } + + $this->assertFalse(auth('statamic')->check()); + $this->assertTrue(auth('web')->check()); + } + #[Test] public function it_can_render_logout_url() { $this->assertEquals(route('statamic.logout'), $this->tag('{{ user:logout_url }}')); $this->assertEquals(route('statamic.logout', ['redirect' => 'home']), $this->tag('{{ user:logout_url redirect="home" }}')); + + $this->assertEquals(route('statamic.logout', ['guard' => 'statamic']), $this->tag('{{ user:logout_url guard="statamic" }}')); } #[Test] From 0d40ff6279f1d0b5b147062d61d065fb5343967d Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 18:21:39 +0100 Subject: [PATCH 2/5] accept a \`guard\` query param on the logout route Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011p6KWCwGqfnEv1ntSxnYUD --- src/Http/Controllers/User/LoginController.php | 6 ++++- tests/Tags/User/LogoutTest.php | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Http/Controllers/User/LoginController.php b/src/Http/Controllers/User/LoginController.php index ad857e6dea5..971ca2177cf 100644 --- a/src/Http/Controllers/User/LoginController.php +++ b/src/Http/Controllers/User/LoginController.php @@ -108,7 +108,11 @@ protected function fireFailedEvent($request, $user = null) public function logout() { - Auth::logout(); + if ($guard = request()->get('guard')) { + abort_unless(array_key_exists($guard, config('auth.guards')), 404); + } + + Auth::guard($guard)->logout(); $redirect = request()->get('redirect'); diff --git a/tests/Tags/User/LogoutTest.php b/tests/Tags/User/LogoutTest.php index 2150dc432bc..6eeeaa67f36 100644 --- a/tests/Tags/User/LogoutTest.php +++ b/tests/Tags/User/LogoutTest.php @@ -44,6 +44,32 @@ public function it_does_not_redirect_to_external_url() $this->assertGuest(); } + #[Test] + public function it_can_logout_a_specific_guard() + { + config()->set('auth.guards.statamic', config('auth.guards.web')); + + $this + ->actingAs($this->createUser(), 'statamic') + ->actingAs(User::make()->id('web-user')->email('web@example.com')->save(), 'web') + ->get(route('statamic.logout', ['guard' => 'statamic'])) + ->assertRedirect('/'); + + $this->assertGuest('statamic'); + $this->assertAuthenticated('web'); + } + + #[Test] + public function it_does_not_logout_an_unknown_guard() + { + $this + ->actingAs($this->createUser()) + ->get(route('statamic.logout', ['guard' => 'nope'])) + ->assertNotFound(); + + $this->assertAuthenticated(); + } + private function createUser() { return tap(User::make()->id('test-user')->email('test@example.com')->password('secret'))->save(); From d064027cdde4703ef4f31975c5071d0c1fca6312 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 19:10:23 +0100 Subject: [PATCH 3/5] only log out of stateful guards on the logout route Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FqBuDquzfGFkbgyQGDCMfp --- src/Http/Controllers/User/LoginController.php | 26 +++++++++++++---- tests/Tags/User/LogoutTest.php | 28 +++++++++++++++++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Http/Controllers/User/LoginController.php b/src/Http/Controllers/User/LoginController.php index 971ca2177cf..6d449a044ee 100644 --- a/src/Http/Controllers/User/LoginController.php +++ b/src/Http/Controllers/User/LoginController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\User; use Illuminate\Auth\Events\Failed; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -108,11 +109,7 @@ protected function fireFailedEvent($request, $user = null) public function logout() { - if ($guard = request()->get('guard')) { - abort_unless(array_key_exists($guard, config('auth.guards')), 404); - } - - Auth::guard($guard)->logout(); + Auth::guard($this->requestedGuard())->logout(); $redirect = request()->get('redirect'); @@ -123,6 +120,25 @@ public function logout() return redirect($url); } + private function requestedGuard(): ?string + { + $guard = request()->query('guard'); + + if ($guard === null || $guard === '') { + return null; + } + + abort_unless(is_string($guard) && $this->isStatefulGuard($guard), 404); + + return $guard; + } + + private function isStatefulGuard(string $guard): bool + { + return array_key_exists($guard, config('auth.guards')) + && Auth::guard($guard) instanceof StatefulGuard; + } + protected function username() { return 'email'; diff --git a/tests/Tags/User/LogoutTest.php b/tests/Tags/User/LogoutTest.php index 6eeeaa67f36..0de57c52841 100644 --- a/tests/Tags/User/LogoutTest.php +++ b/tests/Tags/User/LogoutTest.php @@ -2,6 +2,7 @@ namespace Tests\Tags\User; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\User; use Tests\PreventSavingStacheItemsToDisk; @@ -60,11 +61,34 @@ public function it_can_logout_a_specific_guard() } #[Test] - public function it_does_not_logout_an_unknown_guard() + #[DataProvider('invalidGuardProvider')] + public function it_does_not_logout_an_invalid_guard($guard) { $this ->actingAs($this->createUser()) - ->get(route('statamic.logout', ['guard' => 'nope'])) + ->get(route('statamic.logout', ['guard' => $guard])) + ->assertNotFound(); + + $this->assertAuthenticated(); + } + + public static function invalidGuardProvider() + { + return [ + 'unknown guard' => ['nope'], + 'falsy string' => ['0'], + 'array' => [['web']], + ]; + } + + #[Test] + public function it_does_not_logout_a_non_session_guard() + { + config()->set('auth.guards.api', ['driver' => 'token', 'provider' => 'users']); + + $this + ->actingAs($this->createUser()) + ->get(route('statamic.logout', ['guard' => 'api'])) ->assertNotFound(); $this->assertAuthenticated(); From b2a7541e81fea38942480f37a6de1a69a74902dd Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 19:10:25 +0100 Subject: [PATCH 4/5] test the cp logout route ignores the `guard` param Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FqBuDquzfGFkbgyQGDCMfp --- tests/Tags/User/LogoutTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Tags/User/LogoutTest.php b/tests/Tags/User/LogoutTest.php index 0de57c52841..78016eb2ad1 100644 --- a/tests/Tags/User/LogoutTest.php +++ b/tests/Tags/User/LogoutTest.php @@ -94,6 +94,21 @@ public function it_does_not_logout_a_non_session_guard() $this->assertAuthenticated(); } + #[Test] + public function it_ignores_the_guard_param_on_the_cp_logout_route() + { + config()->set('auth.guards.statamic', config('auth.guards.web')); + + $this + ->actingAs($this->createUser(), 'statamic') + ->actingAs(User::make()->id('web-user')->email('web@example.com')->save(), 'web') + ->get(cp_route('logout', ['guard' => 'statamic'])) + ->assertRedirect('/'); + + $this->assertAuthenticated('statamic'); + $this->assertGuest('web'); + } + private function createUser() { return tap(User::make()->id('test-user')->email('test@example.com')->password('secret'))->save(); From fbc70ac1e3ddaf7c014a8590df3be49c7712a777 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 7 Sep 2026 19:25:01 +0100 Subject: [PATCH 5/5] simplify the guard check and type the logout test data provider Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FqBuDquzfGFkbgyQGDCMfp --- src/Http/Controllers/User/LoginController.php | 2 +- tests/Tags/User/LogoutTest.php | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Http/Controllers/User/LoginController.php b/src/Http/Controllers/User/LoginController.php index 6d449a044ee..626e24db47c 100644 --- a/src/Http/Controllers/User/LoginController.php +++ b/src/Http/Controllers/User/LoginController.php @@ -124,7 +124,7 @@ private function requestedGuard(): ?string { $guard = request()->query('guard'); - if ($guard === null || $guard === '') { + if (! $guard) { return null; } diff --git a/tests/Tags/User/LogoutTest.php b/tests/Tags/User/LogoutTest.php index 78016eb2ad1..f4431e7544b 100644 --- a/tests/Tags/User/LogoutTest.php +++ b/tests/Tags/User/LogoutTest.php @@ -62,8 +62,10 @@ public function it_can_logout_a_specific_guard() #[Test] #[DataProvider('invalidGuardProvider')] - public function it_does_not_logout_an_invalid_guard($guard) + public function it_does_not_logout_an_invalid_guard(string $guard) { + config()->set('auth.guards.api', ['driver' => 'token', 'provider' => 'users']); + $this ->actingAs($this->createUser()) ->get(route('statamic.logout', ['guard' => $guard])) @@ -72,23 +74,20 @@ public function it_does_not_logout_an_invalid_guard($guard) $this->assertAuthenticated(); } - public static function invalidGuardProvider() + public static function invalidGuardProvider(): array { return [ 'unknown guard' => ['nope'], - 'falsy string' => ['0'], - 'array' => [['web']], + 'non-session guard' => ['api'], ]; } #[Test] - public function it_does_not_logout_a_non_session_guard() + public function it_does_not_logout_an_array_of_guards() { - config()->set('auth.guards.api', ['driver' => 'token', 'provider' => 'users']); - $this ->actingAs($this->createUser()) - ->get(route('statamic.logout', ['guard' => 'api'])) + ->get(route('statamic.logout', ['guard' => ['web']])) ->assertNotFound(); $this->assertAuthenticated();