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/src/Http/Controllers/User/LoginController.php b/src/Http/Controllers/User/LoginController.php index ad857e6dea5..626e24db47c 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,7 +109,7 @@ protected function fireFailedEvent($request, $user = null) public function logout() { - Auth::logout(); + Auth::guard($this->requestedGuard())->logout(); $redirect = request()->get('redirect'); @@ -119,6 +120,25 @@ public function logout() return redirect($url); } + private function requestedGuard(): ?string + { + $guard = request()->query('guard'); + + if (! $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 2150dc432bc..f4431e7544b 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; @@ -44,6 +45,69 @@ 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] + #[DataProvider('invalidGuardProvider')] + 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])) + ->assertNotFound(); + + $this->assertAuthenticated(); + } + + public static function invalidGuardProvider(): array + { + return [ + 'unknown guard' => ['nope'], + 'non-session guard' => ['api'], + ]; + } + + #[Test] + public function it_does_not_logout_an_array_of_guards() + { + $this + ->actingAs($this->createUser()) + ->get(route('statamic.logout', ['guard' => ['web']])) + ->assertNotFound(); + + $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(); 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]