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

Skip to content

Commit 5cd1d7b

Browse files
[Security] add "anonymous: lazy" mode to firewalls
1 parent 3c7172d commit 5cd1d7b

File tree

14 files changed

+239
-6
lines changed

14 files changed

+239
-6
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
4.3.0
99
-----
1010

11+
* Added `anonymous: lazy` mode to firewalls to make them (not) start the session as late as possible
1112
* Added new encoder types: `auto` (recommended), `native` and `sodium`
1213
* The normalization of the cookie names configured in the `logout.delete_cookies`
1314
option is deprecated and will be disabled in Symfony 5.0. This affects to cookies

src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\HttpFoundation\Response;
1818
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
1919
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
20+
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
2021
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
2122
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
2223
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
@@ -127,7 +128,7 @@ public function collect(Request $request, Response $response, \Exception $except
127128

128129
$logoutUrl = null;
129130
try {
130-
if (null !== $this->logoutUrlGenerator) {
131+
if (null !== $this->logoutUrlGenerator && !$token instanceof AnonymousToken) {
131132
$logoutUrl = $this->logoutUrlGenerator->getLogoutPath();
132133
}
133134
} catch (\Exception $e) {

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ public function getKey()
5555
public function addConfiguration(NodeDefinition $builder)
5656
{
5757
$builder
58+
->beforeNormalization()
59+
->ifTrue(function ($v) { return 'lazy' === $v; })
60+
->then(function ($v) { return ['lazy' => true]; })
61+
->end()
5862
->children()
63+
->booleanNode('lazy')->defaultFalse()->end()
5964
->scalarNode('secret')->defaultNull()->end()
6065
->end()
6166
;

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ private function createFirewalls(array $config, ContainerBuilder $container)
243243
list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
244244

245245
$contextId = 'security.firewall.map.context.'.$name;
246-
$context = $container->setDefinition($contextId, new ChildDefinition('security.firewall.context'));
246+
$context = new ChildDefinition($firewall['stateless'] || empty($firewall['anonymous']['lazy']) ? 'security.firewall.context' : 'security.firewall.lazy_context');
247+
$context = $container->setDefinition($contextId, $context);
247248
$context
248249
->replaceArgument(0, new IteratorArgument($listeners))
249250
->replaceArgument(1, $exceptionListener)
@@ -409,7 +410,9 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
409410
}
410411

411412
// Access listener
412-
$listeners[] = new Reference('security.access_listener');
413+
if ($firewall['stateless'] || empty($firewall['anonymous']['lazy'])) {
414+
$listeners[] = new Reference('security.access_listener');
415+
}
413416

414417
// Exception listener
415418
$exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless']));

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@
151151
<argument /> <!-- FirewallConfig -->
152152
</service>
153153

154+
<service id="security.firewall.lazy_context" class="Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext" abstract="true">
155+
<argument type="collection" />
156+
<argument type="service" id="security.exception_listener" />
157+
<argument /> <!-- LogoutListener -->
158+
<argument /> <!-- FirewallConfig -->
159+
<argument type="service" id="security.access_listener" />
160+
<argument type="service" id="security.untracked_token_storage" />
161+
<argument type="service" id="security.access_map" />
162+
</service>
163+
154164
<service id="security.firewall.config" class="Symfony\Bundle\SecurityBundle\Security\FirewallConfig" abstract="true">
155165
<argument /> <!-- name -->
156166
<argument /> <!-- user_checker -->
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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\Security;
13+
14+
use Symfony\Component\HttpKernel\Event\RequestEvent;
15+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
16+
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
17+
use Symfony\Component\Security\Core\Exception\LazyResponseException;
18+
use Symfony\Component\Security\Http\AccessMapInterface;
19+
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
20+
use Symfony\Component\Security\Http\Firewall\AccessListener;
21+
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
22+
use Symfony\Component\Security\Http\Firewall\LogoutListener;
23+
24+
/**
25+
* Lazily calls authentication listeners when actually required by the access listener.
26+
*
27+
* @author Nicolas Grekas <[email protected]>
28+
*/
29+
class LazyFirewallContext extends FirewallContext
30+
{
31+
private $accessListener;
32+
private $tokenStorage;
33+
private $map;
34+
35+
public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, AccessListener $accessListener, TokenStorage $tokenStorage, AccessMapInterface $map)
36+
{
37+
parent::__construct($listeners, $exceptionListener, $logoutListener, $config);
38+
39+
$this->accessListener = $accessListener;
40+
$this->tokenStorage = $tokenStorage;
41+
$this->map = $map;
42+
}
43+
44+
public function getListeners(): iterable
45+
{
46+
return [$this];
47+
}
48+
49+
public function __invoke(RequestEvent $event)
50+
{
51+
$this->tokenStorage->setInitializer(function () use ($event) {
52+
$event = new LazyResponseEvent($event);
53+
foreach (parent::getListeners() as $listener) {
54+
if (\is_callable($listener)) {
55+
$listener($event);
56+
} else {
57+
@trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($listener)), E_USER_DEPRECATED);
58+
$listener->handle($event);
59+
}
60+
}
61+
});
62+
63+
try {
64+
[$attributes] = $this->map->getPatterns($event->getRequest());
65+
66+
if ($attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes) {
67+
($this->accessListener)($event);
68+
}
69+
} catch (LazyResponseException $e) {
70+
$event->setResponse($e->getResponse());
71+
}
72+
}
73+
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@ public function profileAction()
5959

6060
public function homepageAction()
6161
{
62-
return new Response('<html><body>Homepage</body></html>');
62+
return (new Response('<html><body>Homepage</body></html>'))->setPublic();
6363
}
6464
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ public function testInvalidIpsInAccessControl()
129129
$client->request('GET', '/unprotected_resource');
130130
}
131131

132+
public function testPublicHomepage()
133+
{
134+
$client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']);
135+
$client->request('GET', '/en/');
136+
137+
$this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse());
138+
$this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public'));
139+
$this->assertSame(0, self::$container->get('session')->getUsageIndex());
140+
}
141+
132142
private function assertAllowed($client, $path)
133143
{
134144
$client->request('GET', $path);

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ security:
2727
check_path: /login_check
2828
default_target_path: /profile
2929
logout: ~
30-
anonymous: ~
30+
anonymous: lazy
3131

3232
# This firewall is here just to check its the logout functionality
3333
second_area:
@@ -38,6 +38,7 @@ security:
3838
path: /second/logout
3939

4040
access_control:
41+
- { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
4142
- { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
4243
- { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
4344
- { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY }

src/Symfony/Bundle/SecurityBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"symfony/security-core": "^4.4",
2525
"symfony/security-csrf": "^4.2|^5.0",
2626
"symfony/security-guard": "^4.2|^5.0",
27-
"symfony/security-http": "^4.3"
27+
"symfony/security-http": "^4.4"
2828
},
2929
"require-dev": {
3030
"symfony/asset": "^3.4|^4.0|^5.0",

src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@
2525
class TokenStorage implements TokenStorageInterface, ResetInterface
2626
{
2727
private $token;
28+
private $initializer;
2829

2930
/**
3031
* {@inheritdoc}
3132
*/
3233
public function getToken()
3334
{
35+
if ($initializer = $this->initializer) {
36+
$this->initializer = null;
37+
$initializer();
38+
}
39+
3440
return $this->token;
3541
}
3642

@@ -43,9 +49,15 @@ public function setToken(TokenInterface $token = null)
4349
@trigger_error(sprintf('Not implementing the "%s::getRoleNames()" method in "%s" is deprecated since Symfony 4.3.', TokenInterface::class, \get_class($token)), E_USER_DEPRECATED);
4450
}
4551

52+
$this->initializer = null;
4653
$this->token = $token;
4754
}
4855

56+
public function setInitializer(?callable $initializer): void
57+
{
58+
$this->initializer = $initializer;
59+
}
60+
4961
public function reset()
5062
{
5163
$this->setToken(null);
Lines 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 <[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\Core\Exception;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
16+
/**
17+
* A signaling exception that wraps a lazily computed response.
18+
*
19+
* @author Nicolas Grekas <[email protected]>
20+
*/
21+
class LazyResponseException extends \Exception implements ExceptionInterface
22+
{
23+
private $response;
24+
25+
public function __construct(Response $response)
26+
{
27+
$this->response = $response;
28+
}
29+
30+
public function getResponse(): Response
31+
{
32+
return $this->response;
33+
}
34+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\Event;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\HttpKernel\Event\RequestEvent;
17+
use Symfony\Component\HttpKernel\HttpKernelInterface;
18+
use Symfony\Component\Security\Core\Exception\LazyResponseException;
19+
20+
/**
21+
* Wraps a lazily computed response in a signaling exception.
22+
*
23+
* @author Nicolas Grekas <[email protected]>
24+
*/
25+
final class LazyResponseEvent extends RequestEvent
26+
{
27+
private $event;
28+
29+
public function __construct(parent $event)
30+
{
31+
$this->event = $event;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function setResponse(Response $response)
38+
{
39+
$this->stopPropagation();
40+
$this->event->stopPropagation();
41+
42+
throw new LazyResponseException($response);
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function getKernel(): HttpKernelInterface
49+
{
50+
return $this->event->getKernel();
51+
}
52+
53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function getRequest(): Request
57+
{
58+
return $this->event->getRequest();
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*/
64+
public function getRequestType(): int
65+
{
66+
return $this->event->getRequestType();
67+
}
68+
69+
/**
70+
* {@inheritdoc}
71+
*/
72+
public function isMasterRequest(): bool
73+
{
74+
return $this->event->isMasterRequest();
75+
}
76+
}

src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\Security\Core\Exception\AccountStatusException;
2727
use Symfony\Component\Security\Core\Exception\AuthenticationException;
2828
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
29+
use Symfony\Component\Security\Core\Exception\LazyResponseException;
2930
use Symfony\Component\Security\Core\Exception\LogoutException;
3031
use Symfony\Component\Security\Core\Security;
3132
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
@@ -103,6 +104,12 @@ public function onKernelException(GetResponseForExceptionEvent $event)
103104
return;
104105
}
105106

107+
if ($exception instanceof LazyResponseException) {
108+
$event->setResponse($exception->getResponse());
109+
110+
return;
111+
}
112+
106113
if ($exception instanceof LogoutException) {
107114
$this->handleLogoutException($exception);
108115

0 commit comments

Comments
 (0)