Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 94c7372

Browse filesBrowse files
committed
[Security] Add a method in the security helper to ease programmatic logout (#40663)
1 parent 8bdbd34 commit 94c7372
Copy full SHA for 94c7372

File tree

7 files changed

+202
-1
lines changed
Filter options

7 files changed

+202
-1
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+7Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security.php
+3Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php
+4Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Security.php
+24Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Tests/SecurityTest.php
+111-1Lines 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

+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http;
13+
14+
use Symfony\Component\Security\Core\Exception\LogicException;
15+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
16+
17+
class FirewallEventDispatcherMap implements FirewallEventDispatcherMapInterface
18+
{
19+
private array $map = [];
20+
21+
public function addEventDispatcher(string $firewallName, EventDispatcherInterface $eventDispatcher): void
22+
{
23+
$this->map[$firewallName] = $eventDispatcher;
24+
}
25+
26+
public function getEventDispatcher(string $firewallName): ?EventDispatcherInterface
27+
{
28+
if (\array_key_exists($firewallName, $this->map)) {
29+
return $this->map[$firewallName];
30+
}
31+
32+
throw new LogicException(sprintf('The firewall "%s" does not have an event dispatcher.', $firewallName));
33+
}
34+
}
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http;
13+
14+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
15+
16+
interface FirewallEventDispatcherMapInterface
17+
{
18+
public function getEventDispatcher(string $firewallName): ?EventDispatcherInterface;
19+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.