Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5b7fe85

Browse files
committed
[Security][SecurityBundle] Enhance automatic logout url generation
1 parent e1c28de commit 5b7fe85

File tree

7 files changed

+265
-27
lines changed

7 files changed

+265
-27
lines changed

UPGRADE-3.3.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ Security
183183
* The `RoleInterface` has been deprecated. Extend the `Symfony\Component\Security\Core\Role\Role`
184184
class in your custom role implementations instead.
185185

186+
* The `LogoutUrlGenerator::registerListener()` method will expect a 6th `$context = null` argument in 4.0.
187+
Define the argument when overriding this method.
188+
186189
SecurityBundle
187190
--------------
188191

UPGRADE-4.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ Security
281281

282282
* The `RoleInterface` has been removed. Extend the `Symfony\Component\Security\Core\Role\Role`
283283
class instead.
284+
285+
* The `LogoutUrlGenerator::registerListener()` method expects a 6th `$context = null` argument.
284286

285287
SecurityBundle
286288
--------------

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a
387387
$firewall['logout']['csrf_token_id'],
388388
$firewall['logout']['csrf_parameter'],
389389
isset($firewall['logout']['csrf_token_generator']) ? new Reference($firewall['logout']['csrf_token_generator']) : null,
390+
false === $firewall['stateless'] && isset($firewall['context']) ? $firewall['context'] : null,
390391
))
391392
;
392393
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Bundle\SecurityBundle\EventListener;
13+
14+
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
15+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
16+
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
17+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
18+
use Symfony\Component\Security\Http\Firewall;
19+
use Symfony\Component\Security\Http\FirewallMapInterface;
20+
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
21+
22+
/**
23+
* @author Maxime Steinhausser <[email protected]>
24+
*/
25+
class FirewallListener extends Firewall
26+
{
27+
private $map;
28+
private $logoutUrlGenerator;
29+
30+
public function __construct(FirewallMapInterface $map, EventDispatcherInterface $dispatcher, LogoutUrlGenerator $logoutUrlGenerator)
31+
{
32+
$this->map = $map;
33+
$this->logoutUrlGenerator = $logoutUrlGenerator;
34+
35+
parent::__construct($map, $dispatcher);
36+
}
37+
38+
public function onKernelRequest(GetResponseEvent $event)
39+
{
40+
if (!$event->isMasterRequest()) {
41+
return;
42+
}
43+
44+
if ($this->map instanceof FirewallMap && $config = $this->map->getFirewallConfig($event->getRequest())) {
45+
$this->logoutUrlGenerator->setCurrentFirewall($config->getName(), $config->getContext());
46+
}
47+
48+
parent::onKernelRequest($event);
49+
}
50+
51+
public function onKernelFinishRequest(FinishRequestEvent $event)
52+
{
53+
if ($event->isMasterRequest()) {
54+
$this->logoutUrlGenerator->setCurrentFirewall(null);
55+
}
56+
57+
parent::onKernelFinishRequest($event);
58+
}
59+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,11 @@
9898

9999

100100
<!-- Firewall related services -->
101-
<service id="security.firewall" class="Symfony\Component\Security\Http\Firewall">
101+
<service id="security.firewall" class="Symfony\Bundle\SecurityBundle\EventListener\FirewallListener">
102102
<tag name="kernel.event_subscriber" />
103103
<argument type="service" id="security.firewall.map" />
104104
<argument type="service" id="event_dispatcher" />
105+
<argument type="service" id="security.logout_url_generator" />
105106
</service>
106107

107108
<service id="security.firewall.map" class="Symfony\Bundle\SecurityBundle\Security\FirewallMap" public="false">

src/Symfony/Component/Security/Http/Logout/LogoutUrlGenerator.php

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\HttpFoundation\RequestStack;
1515
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
16+
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
1617
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1718
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
1819

@@ -28,6 +29,7 @@ class LogoutUrlGenerator
2829
private $router;
2930
private $tokenStorage;
3031
private $listeners = array();
32+
private $currentFirewall;
3133

3234
public function __construct(RequestStack $requestStack = null, UrlGeneratorInterface $router = null, TokenStorageInterface $tokenStorage = null)
3335
{
@@ -39,15 +41,29 @@ public function __construct(RequestStack $requestStack = null, UrlGeneratorInter
3941
/**
4042
* Registers a firewall's LogoutListener, allowing its URL to be generated.
4143
*
42-
* @param string $key The firewall key
43-
* @param string $logoutPath The path that starts the logout process
44-
* @param string $csrfTokenId The ID of the CSRF token
45-
* @param string $csrfParameter The CSRF token parameter name
46-
* @param CsrfTokenManagerInterface $csrfTokenManager A CsrfTokenManagerInterface instance
44+
* @param string $key The firewall key
45+
* @param string $logoutPath The path that starts the logout process
46+
* @param string $csrfTokenId The ID of the CSRF token
47+
* @param string $csrfParameter The CSRF token parameter name
48+
* @param CsrfTokenManagerInterface|null $csrfTokenManager A CsrfTokenManagerInterface instance
49+
* @param string|null $context The listener context
4750
*/
48-
public function registerListener($key, $logoutPath, $csrfTokenId, $csrfParameter, CsrfTokenManagerInterface $csrfTokenManager = null)
51+
public function registerListener($key, $logoutPath, $csrfTokenId, $csrfParameter, CsrfTokenManagerInterface $csrfTokenManager = null/*, $context = null*/)
4952
{
50-
$this->listeners[$key] = array($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager);
53+
if (func_num_args() >= 6) {
54+
$context = func_get_arg(5);
55+
} else {
56+
if (__CLASS__ !== get_class($this)) {
57+
$r = new \ReflectionMethod($this, __FUNCTION__);
58+
if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
59+
@trigger_error(sprintf('Method %s() will have a sixth `$context = null` argument in version 4.0. Not defining it is deprecated since 3.3.', get_class($this), __FUNCTION__), E_USER_DEPRECATED);
60+
}
61+
}
62+
63+
$context = null;
64+
}
65+
66+
$this->listeners[$key] = array($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager, $context);
5167
}
5268

5369
/**
@@ -74,35 +90,26 @@ public function getLogoutUrl($key = null)
7490
return $this->generateLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_URL);
7591
}
7692

93+
/**
94+
* @param string|null $key The current firewall key
95+
* @param string|null $context The current firewall context
96+
*/
97+
public function setCurrentFirewall($key, $context = null)
98+
{
99+
$this->currentFirewall = array($key, $context);
100+
}
101+
77102
/**
78103
* Generates the logout URL for the firewall.
79104
*
80105
* @param string|null $key The firewall key or null to use the current firewall key
81106
* @param int $referenceType The type of reference (one of the constants in UrlGeneratorInterface)
82107
*
83108
* @return string The logout URL
84-
*
85-
* @throws \InvalidArgumentException if no LogoutListener is registered for the key or the key could not be found automatically.
86109
*/
87110
private function generateLogoutUrl($key, $referenceType)
88111
{
89-
// Fetch the current provider key from token, if possible
90-
if (null === $key && null !== $this->tokenStorage) {
91-
$token = $this->tokenStorage->getToken();
92-
if (null !== $token && method_exists($token, 'getProviderKey')) {
93-
$key = $token->getProviderKey();
94-
}
95-
}
96-
97-
if (null === $key) {
98-
throw new \InvalidArgumentException('Unable to find the current firewall LogoutListener, please provide the provider key manually.');
99-
}
100-
101-
if (!array_key_exists($key, $this->listeners)) {
102-
throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key));
103-
}
104-
105-
list($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager) = $this->listeners[$key];
112+
list($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager) = $this->getListener($key);
106113

107114
$parameters = null !== $csrfTokenManager ? array($csrfParameter => (string) $csrfTokenManager->getToken($csrfTokenId)) : array();
108115

@@ -128,4 +135,54 @@ private function generateLogoutUrl($key, $referenceType)
128135

129136
return $url;
130137
}
138+
139+
/**
140+
* @param string|null $key The firewall key or null use the current firewall key
141+
*
142+
* @return array The logout listener found
143+
*
144+
* @throws \InvalidArgumentException if no LogoutListener is registered for the key or could not be found automatically.
145+
*/
146+
private function getListener($key)
147+
{
148+
if (null !== $key) {
149+
if (isset($this->listeners[$key])) {
150+
return $this->listeners[$key];
151+
}
152+
153+
throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key));
154+
}
155+
156+
// Fetch the current provider key from token, if possible
157+
if (null !== $this->tokenStorage) {
158+
$token = $this->tokenStorage->getToken();
159+
160+
if ($token instanceof AnonymousToken) {
161+
throw new \InvalidArgumentException('Unable to generate a logout url for an anonymous token.');
162+
}
163+
164+
if (null !== $token && method_exists($token, 'getProviderKey')) {
165+
$key = $token->getProviderKey();
166+
167+
if (isset($this->listeners[$key])) {
168+
return $this->listeners[$key];
169+
}
170+
}
171+
}
172+
173+
// Fetch from injected current firewall information, if possible
174+
list($key, $context) = $this->currentFirewall;
175+
176+
if (isset($this->listeners[$key])) {
177+
return $this->listeners[$key];
178+
}
179+
180+
foreach ($this->listeners as $listener) {
181+
if (isset($listener[4]) && $context === $listener[4]) {
182+
return $listener;
183+
}
184+
}
185+
186+
throw new \InvalidArgumentException('Unable to find the current firewall LogoutListener, please provide the provider key manually.');
187+
}
131188
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Tests\Logout;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestStack;
17+
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
18+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
19+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
20+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
21+
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
22+
23+
/**
24+
* @author Maxime Steinhausser <[email protected]>
25+
*/
26+
class LogoutUrlGeneratorTest extends TestCase
27+
{
28+
/** @var TokenStorage */
29+
private $tokenStorage;
30+
/** @var LogoutUrlGenerator */
31+
private $generator;
32+
33+
protected function setUp()
34+
{
35+
$requestStack = $this->getMockBuilder(RequestStack::class)->getMock();
36+
$request = $this->getMockBuilder(Request::class)->getMock();
37+
$requestStack->method('getCurrentRequest')->willReturn($request);
38+
39+
$this->tokenStorage = new TokenStorage();
40+
$this->generator = new LogoutUrlGenerator($requestStack, null, $this->tokenStorage);
41+
}
42+
43+
public function testGetLogoutPath()
44+
{
45+
$this->generator->registerListener('secured_area', '/logout', null, null);
46+
47+
$this->assertSame('/logout', $this->generator->getLogoutPath('secured_area'));
48+
}
49+
50+
/**
51+
* @expectedException \InvalidArgumentException
52+
* @expectedExceptionMessage No LogoutListener found for firewall key "unregistered_key".
53+
*/
54+
public function testGetLogoutPathWithoutLogoutListenerRegisteredForKeyThrowsException()
55+
{
56+
$this->generator->registerListener('secured_area', '/logout', null, null, null);
57+
58+
$this->generator->getLogoutPath('unregistered_key');
59+
}
60+
61+
public function testGuessFromToken()
62+
{
63+
$this->tokenStorage->setToken(new UsernamePasswordToken('user', 'password', 'secured_area'));
64+
$this->generator->registerListener('secured_area', '/logout', null, null);
65+
66+
$this->assertSame('/logout', $this->generator->getLogoutPath());
67+
}
68+
69+
/**
70+
* @expectedException \InvalidArgumentException
71+
* @expectedExceptionMessage Unable to generate a logout url for an anonymous token.
72+
*/
73+
public function testGuessFromAnonymousTokenThrowsException()
74+
{
75+
$this->tokenStorage->setToken(new AnonymousToken('default', 'anon.'));
76+
77+
$this->generator->getLogoutPath();
78+
}
79+
80+
public function testGuessFromCurrentFirewallKey()
81+
{
82+
$this->generator->registerListener('secured_area', '/logout', null, null);
83+
$this->generator->setCurrentFirewall('secured_area');
84+
85+
$this->assertSame('/logout', $this->generator->getLogoutPath());
86+
}
87+
88+
public function testGuessFromCurrentFirewallContext()
89+
{
90+
$this->generator->registerListener('secured_area', '/logout', null, null, null, 'secured');
91+
$this->generator->setCurrentFirewall('admin', 'secured');
92+
93+
$this->assertSame('/logout', $this->generator->getLogoutPath());
94+
}
95+
96+
public function testGuessFromTokenWithoutProviderKeyFallbacksToCurrentFirewall()
97+
{
98+
$this->tokenStorage->setToken($this->getMockBuilder(TokenInterface::class)->getMock());
99+
$this->generator->registerListener('secured_area', '/logout', null, null);
100+
$this->generator->setCurrentFirewall('secured_area');
101+
102+
$this->assertSame('/logout', $this->generator->getLogoutPath());
103+
}
104+
105+
/**
106+
* @expectedException \InvalidArgumentException
107+
* @expectedExceptionMessage Unable to find the current firewall LogoutListener, please provide the provider key manually
108+
*/
109+
public function testUnableToGuessThrowsException()
110+
{
111+
$this->generator->registerListener('secured_area', '/logout', null, null);
112+
113+
$this->generator->getLogoutPath();
114+
}
115+
}

0 commit comments

Comments
 (0)