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 2a9b2db

Browse filesBrowse files
johnkrovitchchalasr
authored andcommitted
[Security] Add a method in the security helper to ease programmatic logout (#40663)
1 parent fa24df6 commit 2a9b2db
Copy full SHA for 2a9b2db

File tree

9 files changed

+257
-2
lines changed
Filter options

9 files changed

+257
-2
lines changed

‎src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Deprecate the `Symfony\Component\Security\Core\Security` service alias, use `Symfony\Bundle\SecurityBundle\Security\Security` instead
99
* Add `Security::getFirewallConfig()` to help to get the firewall configuration associated to the Request
1010
* Add `Security::login()` to login programmatically
11+
* Add `Security::logout()` to logout programmatically
1112

1213
6.1
1314
---

‎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
@@ -376,6 +376,13 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
376376
$container->register($firewallEventDispatcherId, EventDispatcher::class)
377377
->addTag('event_dispatcher.dispatcher', ['name' => $firewallEventDispatcherId]);
378378

379+
$eventDispatcherLocator = $container->getDefinition('security.firewall.event_dispatcher_locator');
380+
$eventDispatcherLocator
381+
->replaceArgument(0, array_merge($eventDispatcherLocator->getArgument(0), [
382+
$id => new ServiceClosureArgument(new Reference($firewallEventDispatcherId)),
383+
]))
384+
;
385+
379386
// Register listeners
380387
$listeners = [];
381388
$listenerKeys = [];

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
'request_stack' => service('request_stack'),
8787
'security.firewall.map' => service('security.firewall.map'),
8888
'security.user_checker' => service('security.user_checker'),
89+
'security.firewall.event_dispatcher_locator' => service('security.firewall.event_dispatcher_locator'),
8990
]),
9091
abstract_arg('authenticators'),
9192
])

‎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;
@@ -160,5 +161,8 @@
160161
service('security.access_map'),
161162
])
162163
->tag('monolog.logger', ['channel' => 'security'])
164+
165+
->set('security.firewall.event_dispatcher_locator', ServiceLocator::class)
166+
->args([[]])
163167
;
164168
};

‎src/Symfony/Bundle/SecurityBundle/Security/Security.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Security/Security.php
+24Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
1617
use Symfony\Component\Security\Core\Exception\LogicException;
1718
use Symfony\Component\Security\Core\Security as LegacySecurity;
1819
use Symfony\Component\Security\Core\User\UserInterface;
1920
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
21+
use Symfony\Component\Security\Http\Event\LogoutEvent;
2022
use Symfony\Contracts\Service\ServiceProviderInterface;
2123

2224
/**
@@ -60,6 +62,28 @@ public function login(UserInterface $user, string $authenticatorName = null, str
6062
$this->container->get('security.user_authenticator')->authenticateUser($user, $authenticator, $request);
6163
}
6264

65+
/**
66+
* Logout the current user by dispatching the LogoutEvent.
67+
*
68+
* @return Response|null The LogoutEvent's Response if any
69+
*/
70+
public function logout(): ?Response
71+
{
72+
$request = $this->container->get('request_stack')->getMainRequest();
73+
$logoutEvent = new LogoutEvent($request, $this->container->get('security.token_storage')->getToken());
74+
$firewallConfig = $this->container->get('security.firewall.map')->getFirewallConfig($request);
75+
76+
if (!$firewallConfig) {
77+
throw new LogicException('It is not possible to logout, as the request is not behind a firewall.');
78+
}
79+
$firewallName = $firewallConfig->getName();
80+
81+
$this->container->get('security.firewall.event_dispatcher_locator')->get($firewallName)->dispatch($logoutEvent);
82+
$this->container->get('security.token_storage')->setToken();
83+
84+
return $logoutEvent->getResponse();
85+
}
86+
6387
private function getAuthenticator(?string $authenticatorName, string $firewallName): AuthenticatorInterface
6488
{
6589
if (!\array_key_exists($firewallName, $this->authenticators)) {

‎src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ public function testLoginWithBuiltInAuthenticator(string $authenticator)
9999
$this->assertSame(200, $response->getStatusCode());
100100
$this->assertSame(['message' => 'Welcome @chalasr!'], json_decode($response->getContent(), true));
101101
}
102+
103+
/**
104+
* @testWith ["json_login"]
105+
* ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"]
106+
*/
107+
public function testLogout(string $authenticator)
108+
{
109+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
110+
static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator;
111+
$client->request('GET', '/welcome');
112+
$this->assertEquals('chalasr', static::getContainer()->get('security.helper')->getUser()->getUserIdentifier());
113+
114+
$client->request('GET', '/auto-logout');
115+
$response = $client->getResponse();
116+
$this->assertSame(200, $response->getStatusCode());
117+
$this->assertNull(static::getContainer()->get('security.helper')->getUser());
118+
$this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true));
119+
}
102120
}
103121

104122
final class UserWithoutEquatable implements UserInterface, PasswordAuthenticatedUserInterface
@@ -224,3 +242,17 @@ public function welcome()
224242
return new JsonResponse(['message' => sprintf('Welcome @%s!', $this->security->getUser()->getUserIdentifier())]);
225243
}
226244
}
245+
246+
class LogoutController
247+
{
248+
public function __construct(private Security $security)
249+
{
250+
}
251+
252+
public function logout()
253+
{
254+
$this->security->logout();
255+
256+
return new JsonResponse(['message' => 'Logout successful']);
257+
}
258+
}

‎src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ services:
1515
arguments: ['@security.helper']
1616
public: true
1717

18+
Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController:
19+
arguments: ['@security.helper']
20+
public: true
21+
1822
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~
1923

2024
security:
+5-1Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
welcome:
22
path: /welcome
3-
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome }
3+
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome
4+
5+
logout:
6+
path: /auto-logout
7+
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController::logout

‎src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Security/SecurityTest.php
+179-1Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@
1919
use Symfony\Component\DependencyInjection\ServiceLocator;
2020
use Symfony\Component\HttpFoundation\Request;
2121
use Symfony\Component\HttpFoundation\RequestStack;
22+
use Symfony\Component\HttpFoundation\Response;
2223
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
2324
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
2425
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
2526
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
27+
use Symfony\Component\Security\Core\Exception\LogicException;
2628
use Symfony\Component\Security\Core\User\InMemoryUser;
2729
use Symfony\Component\Security\Core\User\UserCheckerInterface;
2830
use Symfony\Component\Security\Core\User\UserInterface;
2931
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
3032
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
33+
use Symfony\Component\Security\Http\Event\LogoutEvent;
34+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
3135
use Symfony\Contracts\Service\ServiceProviderInterface;
3236

3337
class SecurityTest extends TestCase
@@ -117,7 +121,7 @@ public function getFirewallConfigTests()
117121
yield [$request, new FirewallConfig('main', 'acme_user_checker')];
118122
}
119123

120-
public function testAutoLogin()
124+
public function testLogin()
121125
{
122126
$request = new Request();
123127
$authenticator = $this->createMock(AuthenticatorInterface::class);
@@ -163,6 +167,180 @@ public function testAutoLogin()
163167
$security->login($user);
164168
}
165169

170+
public function testLogout()
171+
{
172+
$request = new Request();
173+
$requestStack = $this->createMock(RequestStack::class);
174+
$requestStack
175+
->expects($this->once())
176+
->method('getMainRequest')
177+
->willReturn($request)
178+
;
179+
180+
$token = $this->createMock(TokenInterface::class);
181+
$tokenStorage = $this->createMock(TokenStorageInterface::class);
182+
$tokenStorage
183+
->expects($this->once())
184+
->method('getToken')
185+
->willReturn($token)
186+
;
187+
$tokenStorage
188+
->expects($this->once())
189+
->method('setToken')
190+
;
191+
192+
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
193+
$eventDispatcher
194+
->expects($this->once())
195+
->method('dispatch')
196+
->with(new LogoutEvent($request, $token))
197+
;
198+
199+
$firewallMap = $this->createMock(FirewallMap::class);
200+
$firewallConfig = new FirewallConfig('my_firewall', 'user_checker');
201+
$firewallMap
202+
->expects($this->once())
203+
->method('getFirewallConfig')
204+
->willReturn($firewallConfig)
205+
;
206+
207+
$eventDispatcherLocator = $this->createMock(ContainerInterface::class);
208+
$eventDispatcherLocator
209+
->expects($this->atLeastOnce())
210+
->method('get')
211+
->willReturnMap([
212+
['my_firewall', $eventDispatcher],
213+
])
214+
;
215+
216+
$container = $this->createMock(ContainerInterface::class);
217+
$container
218+
->expects($this->atLeastOnce())
219+
->method('get')
220+
->willReturnMap([
221+
['request_stack', $requestStack],
222+
['security.token_storage', $tokenStorage],
223+
['security.firewall.map', $firewallMap],
224+
['security.firewall.event_dispatcher_locator', $eventDispatcherLocator],
225+
])
226+
;
227+
$security = new Security($container);
228+
$security->logout();
229+
}
230+
231+
public function testLogoutWithoutFirewall()
232+
{
233+
$request = new Request();
234+
$requestStack = $this->createMock(RequestStack::class);
235+
$requestStack
236+
->expects($this->once())
237+
->method('getMainRequest')
238+
->willReturn($request)
239+
;
240+
241+
$token = $this->createMock(TokenInterface::class);
242+
$tokenStorage = $this->createMock(TokenStorageInterface::class);
243+
$tokenStorage
244+
->expects($this->once())
245+
->method('getToken')
246+
->willReturn($token)
247+
;
248+
249+
$firewallMap = $this->createMock(FirewallMap::class);
250+
$firewallMap
251+
->expects($this->once())
252+
->method('getFirewallConfig')
253+
->willReturn(null)
254+
;
255+
256+
$container = $this->createMock(ContainerInterface::class);
257+
$container
258+
->expects($this->atLeastOnce())
259+
->method('get')
260+
->willReturnMap([
261+
['request_stack', $requestStack],
262+
['security.token_storage', $tokenStorage],
263+
['security.firewall.map', $firewallMap],
264+
])
265+
;
266+
267+
$this->expectException(LogicException::class);
268+
$security = new Security($container);
269+
$security->logout();
270+
}
271+
272+
public function testLogoutWithResponse()
273+
{
274+
$request = new Request();
275+
$requestStack = $this->createMock(RequestStack::class);
276+
$requestStack
277+
->expects($this->once())
278+
->method('getMainRequest')
279+
->willReturn($request)
280+
;
281+
282+
$token = $this->createMock(TokenInterface::class);
283+
$tokenStorage = $this->createMock(TokenStorageInterface::class);
284+
$tokenStorage
285+
->expects($this->once())
286+
->method('getToken')
287+
->willReturn($token)
288+
;
289+
$tokenStorage
290+
->expects($this->once())
291+
->method('setToken')
292+
;
293+
294+
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
295+
$eventDispatcher
296+
->expects($this->once())
297+
->method('dispatch')
298+
->willReturnCallback(function ($event) use ($request, $token) {
299+
$this->assertInstanceOf(LogoutEvent::class, $event);
300+
$this->assertEquals($request, $event->getRequest());
301+
$this->assertEquals($token, $event->getToken());
302+
303+
$event->setResponse(new Response('a custom response'));
304+
305+
return $event;
306+
})
307+
;
308+
309+
$firewallMap = $this->createMock(FirewallMap::class);
310+
$firewallConfig = new FirewallConfig('my_firewall', 'user_checker');
311+
$firewallMap
312+
->expects($this->once())
313+
->method('getFirewallConfig')
314+
->willReturn($firewallConfig)
315+
;
316+
317+
$eventDispatcherLocator = $this->createMock(ContainerInterface::class);
318+
$eventDispatcherLocator
319+
->expects($this->atLeastOnce())
320+
->method('get')
321+
->willReturnMap([
322+
['my_firewall', $eventDispatcher],
323+
])
324+
;
325+
326+
$container = $this->createMock(ContainerInterface::class);
327+
$container
328+
->expects($this->atLeastOnce())
329+
->method('get')
330+
->willReturnMap([
331+
['request_stack', $requestStack],
332+
['security.token_storage', $tokenStorage],
333+
['security.firewall.map', $firewallMap],
334+
['security.firewall.event_dispatcher_locator', $eventDispatcherLocator],
335+
])
336+
;
337+
$security = new Security($container);
338+
$response = $security->logout();
339+
340+
$this->assertInstanceOf(Response::class, $response);
341+
$this->assertEquals('a custom response', $response->getContent());
342+
}
343+
166344
private function createContainer(string $serviceId, object $serviceObject): ContainerInterface
167345
{
168346
return new ServiceLocator([$serviceId => fn () => $serviceObject]);

0 commit comments

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