Skip to content

Commit 5d42598

Browse files
committed
[Security] Add a method in the security helper to ease programmatic logout (#40663)
1 parent 8bdbd34 commit 5d42598

File tree

5 files changed

+149
-1
lines changed

5 files changed

+149
-1
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,13 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
353353
$container->register($firewallEventDispatcherId, EventDispatcher::class)
354354
->addTag('event_dispatcher.dispatcher', ['name' => $firewallEventDispatcherId]);
355355

356+
$eventDispatcherLocator = $container->getDefinition('security.firewall.event_dispatcher_locator');
357+
$eventDispatcherLocator
358+
->replaceArgument(0, array_merge($eventDispatcherLocator->getArgument(0), [
359+
$id => new ServiceClosureArgument(new Reference($firewallEventDispatcherId)),
360+
]))
361+
;
362+
356363
// Register listeners
357364
$listeners = [];
358365
$listenerKeys = [];

src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
->args([service_locator([
8080
'security.token_storage' => service('security.token_storage'),
8181
'security.authorization_checker' => service('security.authorization_checker'),
82+
'request_stack' => service('request_stack'),
83+
'security.firewall.map' => service('security.firewall.map'),
84+
'security.firewall.event_dispatcher_locator' => service('security.firewall.event_dispatcher_locator'),
8285
])])
8386
->alias(Security::class, 'security.helper')
8487

src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14+
use Symfony\Component\DependencyInjection\ServiceLocator;
1415
use Symfony\Component\Security\Http\AccessMap;
1516
use Symfony\Component\Security\Http\Authentication\CustomAuthenticationFailureHandler;
1617
use Symfony\Component\Security\Http\Authentication\CustomAuthenticationSuccessHandler;
@@ -159,5 +160,8 @@
159160
service('security.access_map'),
160161
])
161162
->tag('monolog.logger', ['channel' => 'security'])
163+
164+
->set('security.firewall.event_dispatcher_locator', ServiceLocator::class)
165+
->args([[]])
162166
;
163167
};

src/Symfony/Component/Security/Core/Security.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
namespace Symfony\Component\Security\Core;
1313

1414
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\HttpFoundation\Response;
1516
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1617
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
18+
use Symfony\Component\Security\Core\Exception\LogicException;
1719
use Symfony\Component\Security\Core\User\UserInterface;
20+
use Symfony\Component\Security\Http\Event\LogoutEvent;
1821

1922
/**
2023
* Helper class for commonly-needed security tasks.
@@ -57,4 +60,25 @@ public function getToken(): ?TokenInterface
5760
{
5861
return $this->container->get('security.token_storage')->getToken();
5962
}
63+
64+
/**
65+
* Logout the current user automatically. Dispatch the logout event.
66+
*/
67+
public function logout(): ?Response
68+
{
69+
$request = $this->container->get('request_stack')->getMainRequest();
70+
$logoutEvent = new LogoutEvent($request, $this->container->get('security.token_storage')->getToken());
71+
72+
$firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request);
73+
74+
if (null === $firewallConfig) {
75+
throw new LogicException('It is not possible to logout, as the request is not behind a firewall.');
76+
}
77+
$firewallName = $firewallConfig->getName();
78+
79+
$this->container->get('security.firewall.event_dispatcher_locator')->get($firewallName)->dispatch($logoutEvent);
80+
$this->container->get('security.token_storage')->setToken();
81+
82+
return $logoutEvent->getResponse();
83+
}
6084
}

src/Symfony/Component/Security/Core/Tests/SecurityTest.php

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,22 @@
1111

1212
namespace Symfony\Component\Security\Core\Tests;
1313

14+
use PHPUnit\Framework\MockObject\MockObject;
1415
use PHPUnit\Framework\TestCase;
1516
use Psr\Container\ContainerInterface;
17+
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
18+
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
19+
use Symfony\Component\HttpFoundation\Request;
20+
use Symfony\Component\HttpFoundation\RequestStack;
1621
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1722
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1823
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
1924
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
25+
use Symfony\Component\Security\Core\Exception\LogicException;
2026
use Symfony\Component\Security\Core\Security;
2127
use Symfony\Component\Security\Core\User\InMemoryUser;
28+
use Symfony\Component\Security\Http\Event\LogoutEvent;
29+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2230

2331
class SecurityTest extends TestCase
2432
{
@@ -81,7 +89,109 @@ public function testIsGranted()
8189
$this->assertTrue($security->isGranted('SOME_ATTRIBUTE', 'SOME_SUBJECT'));
8290
}
8391

84-
private function createContainer($serviceId, $serviceObject)
92+
public function testLogout()
93+
{
94+
$request = new Request();
95+
$requestStack = $this->createMock(RequestStack::class);
96+
$requestStack
97+
->expects($this->once())
98+
->method('getMainRequest')
99+
->willReturn($request)
100+
;
101+
102+
$token = $this->createMock(TokenInterface::class);
103+
$tokenStorage = $this->createMock(TokenStorageInterface::class);
104+
$tokenStorage
105+
->expects($this->once())
106+
->method('getToken')
107+
->willReturn($token)
108+
;
109+
$tokenStorage
110+
->expects($this->once())
111+
->method('setToken')
112+
;
113+
114+
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
115+
$eventDispatcher
116+
->expects($this->once())
117+
->method('dispatch')
118+
->with(new LogoutEvent($request, $token))
119+
;
120+
121+
$firewallMap = $this->createMock(FirewallMap::class);
122+
$firewallConfig = new FirewallConfig('my_firewall', 'user_checker');
123+
$firewallMap
124+
->expects($this->once())
125+
->method('getFirewallConfig')
126+
->willReturn($firewallConfig)
127+
;
128+
129+
$eventDispatcherLocator = $this->createMock(ContainerInterface::class);
130+
$eventDispatcherLocator
131+
->expects($this->atLeastOnce())
132+
->method('get')
133+
->willReturnMap([
134+
['my_firewall', $eventDispatcher],
135+
])
136+
;
137+
138+
$container = $this->createMock(ContainerInterface::class);
139+
$container
140+
->expects($this->atLeastOnce())
141+
->method('get')
142+
->willReturnMap([
143+
['request_stack', $requestStack],
144+
['security.token_storage', $tokenStorage],
145+
['security.firewall.map', $firewallMap],
146+
['security.firewall.event_dispatcher_locator', $eventDispatcherLocator],
147+
])
148+
;
149+
$security = new Security($container);
150+
$security->logout();
151+
}
152+
153+
public function testLogoutWithoutFirewall()
154+
{
155+
$request = new Request();
156+
$requestStack = $this->createMock(RequestStack::class);
157+
$requestStack
158+
->expects($this->once())
159+
->method('getMainRequest')
160+
->willReturn($request)
161+
;
162+
163+
$token = $this->createMock(TokenInterface::class);
164+
$tokenStorage = $this->createMock(TokenStorageInterface::class);
165+
$tokenStorage
166+
->expects($this->once())
167+
->method('getToken')
168+
->willReturn($token)
169+
;
170+
171+
$firewallMap = $this->createMock(FirewallMap::class);
172+
$firewallMap
173+
->expects($this->once())
174+
->method('getFirewallConfig')
175+
->willReturn(null)
176+
;
177+
178+
$container = $this->createMock(ContainerInterface::class);
179+
$container
180+
->expects($this->atLeastOnce())
181+
->method('get')
182+
->willReturnMap([
183+
['request_stack', $requestStack],
184+
['security.token_storage', $tokenStorage],
185+
['security.firewall.map', $firewallMap],
186+
])
187+
;
188+
189+
$this->expectException(LogicException::class);
190+
$security = new Security($container);
191+
$security->logout();
192+
}
193+
194+
private function createContainer($serviceId, $serviceObject): MockObject
85195
{
86196
$container = $this->createMock(ContainerInterface::class);
87197

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy