Skip to content

[Security] add "anonymous: lazy" mode to firewalls #33676

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ CHANGELOG
4.3.0
-----

* Added `anonymous: lazy` mode to firewalls to make them (not) start the session as late as possible
* Added new encoder types: `auto` (recommended), `native` and `sodium`
* The normalization of the cookie names configured in the `logout.delete_cookies`
option is deprecated and will be disabled in Symfony 5.0. This affects to cookies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
Expand Down Expand Up @@ -127,7 +128,7 @@ public function collect(Request $request, Response $response, \Exception $except

$logoutUrl = null;
try {
if (null !== $this->logoutUrlGenerator) {
if (null !== $this->logoutUrlGenerator && !$token instanceof AnonymousToken) {
$logoutUrl = $this->logoutUrlGenerator->getLogoutPath();
}
} catch (\Exception $e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ public function getKey()
public function addConfiguration(NodeDefinition $builder)
{
$builder
->beforeNormalization()
->ifTrue(function ($v) { return 'lazy' === $v; })
->then(function ($v) { return ['lazy' => true]; })
->end()
->children()
->booleanNode('lazy')->defaultFalse()->end()
->scalarNode('secret')->defaultNull()->end()
->end()
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ private function createFirewalls(array $config, ContainerBuilder $container)
list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);

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

// Access listener
$listeners[] = new Reference('security.access_listener');
if ($firewall['stateless'] || empty($firewall['anonymous']['lazy'])) {
$listeners[] = new Reference('security.access_listener');
}

// Exception listener
$exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless']));
Expand Down
10 changes: 10 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@
<argument /> <!-- FirewallConfig -->
</service>

<service id="security.firewall.lazy_context" class="Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext" abstract="true">
<argument type="collection" />
<argument type="service" id="security.exception_listener" />
<argument /> <!-- LogoutListener -->
<argument /> <!-- FirewallConfig -->
<argument type="service" id="security.access_listener" />
<argument type="service" id="security.untracked_token_storage" />
<argument type="service" id="security.access_map" />
</service>

<service id="security.firewall.config" class="Symfony\Bundle\SecurityBundle\Security\FirewallConfig" abstract="true">
<argument /> <!-- name -->
<argument /> <!-- user_checker -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Security;

use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\LogoutListener;

/**
* Lazily calls authentication listeners when actually required by the access listener.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class LazyFirewallContext extends FirewallContext
{
private $accessListener;
private $tokenStorage;
private $map;

public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, AccessListener $accessListener, TokenStorage $tokenStorage, AccessMapInterface $map)
{
parent::__construct($listeners, $exceptionListener, $logoutListener, $config);

$this->accessListener = $accessListener;
$this->tokenStorage = $tokenStorage;
$this->map = $map;
}

public function getListeners(): iterable
{
return [$this];
}

public function __invoke(RequestEvent $event)
{
$this->tokenStorage->setInitializer(function () use ($event) {
$event = new LazyResponseEvent($event);
foreach (parent::getListeners() as $listener) {
if (\is_callable($listener)) {
$listener($event);
} else {
@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);
$listener->handle($event);
}
}
});

try {
[$attributes] = $this->map->getPatterns($event->getRequest());

if ($attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes) {
($this->accessListener)($event);
}
} catch (LazyResponseException $e) {
$event->setResponse($e->getResponse());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ public function profileAction()

public function homepageAction()
{
return new Response('<html><body>Homepage</body></html>');
return (new Response('<html><body>Homepage</body></html>'))->setPublic();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ public function testInvalidIpsInAccessControl()
$client->request('GET', '/unprotected_resource');
}

public function testPublicHomepage()
{
$client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']);
$client->request('GET', '/en/');

$this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse());
$this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public'));
$this->assertSame(0, self::$container->get('session')->getUsageIndex());
}

private function assertAllowed($client, $path)
{
$client->request('GET', $path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ security:
check_path: /login_check
default_target_path: /profile
logout: ~
anonymous: ~
anonymous: lazy

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

access_control:
- { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/SecurityBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"symfony/security-core": "^4.4",
"symfony/security-csrf": "^4.2|^5.0",
"symfony/security-guard": "^4.2|^5.0",
"symfony/security-http": "^4.3"
"symfony/security-http": "^4.4"
},
"require-dev": {
"symfony/asset": "^3.4|^4.0|^5.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@
class TokenStorage implements TokenStorageInterface, ResetInterface
{
private $token;
private $initializer;

/**
* {@inheritdoc}
*/
public function getToken()
{
if ($initializer = $this->initializer) {
$this->initializer = null;
$initializer();
}

return $this->token;
}

Expand All @@ -43,9 +49,15 @@ public function setToken(TokenInterface $token = null)
@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);
}

$this->initializer = null;
$this->token = $token;
}

public function setInitializer(?callable $initializer): void
{
$this->initializer = $initializer;
}

public function reset()
{
$this->setToken(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Exception;

use Symfony\Component\HttpFoundation\Response;

/**
* A signaling exception that wraps a lazily computed response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class LazyResponseException extends \Exception implements ExceptionInterface
{
private $response;

public function __construct(Response $response)
{
$this->response = $response;
}

public function getResponse(): Response
{
return $this->response;
}
}
76 changes: 76 additions & 0 deletions src/Symfony/Component/Security/Http/Event/LazyResponseEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Http\Event;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\LazyResponseException;

/**
* Wraps a lazily computed response in a signaling exception.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class LazyResponseEvent extends RequestEvent
{
private $event;

public function __construct(parent $event)
{
$this->event = $event;
}

/**
* {@inheritdoc}
*/
public function setResponse(Response $response)
{
$this->stopPropagation();
$this->event->stopPropagation();

throw new LazyResponseException($response);
}

/**
* {@inheritdoc}
*/
public function getKernel(): HttpKernelInterface
{
return $this->event->getKernel();
}

/**
* {@inheritdoc}
*/
public function getRequest(): Request
{
return $this->event->getRequest();
}

/**
* {@inheritdoc}
*/
public function getRequestType(): int
{
return $this->event->getRequestType();
}

/**
* {@inheritdoc}
*/
public function isMasterRequest(): bool
{
return $this->event->isMasterRequest();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
use Symfony\Component\Security\Core\Exception\LogoutException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
Expand Down Expand Up @@ -103,6 +104,12 @@ public function onKernelException(GetResponseForExceptionEvent $event)
return;
}

if ($exception instanceof LazyResponseException) {
$event->setResponse($exception->getResponse());

return;
}

if ($exception instanceof LogoutException) {
$this->handleLogoutException($exception);

Expand Down