Skip to content

Commit 7629834

Browse files
committed
bug #34627 [Security/Http] call auth listeners/guards eagerly when they "support" the request (nicolas-grekas)
This PR was merged into the 4.4 branch. Discussion ---------- [Security/Http] call auth listeners/guards eagerly when they "support" the request | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #34614, Fix #34679 | License | MIT | Doc PR | - This fixes the form authenticator linked to #34614. Since laziness is here to provide compatibility with HTTP caching, it should be disabled when the request cannot be cached. Tests don't pass yet, but I'm on the path to something here. The PR now introduces a new `AbstractListener` that splits the handling logic in two: - `supports(Request): ?bool` is always called eagerly and tells whether the listener matches the request for an earger call or a lazy call - `authenticate(RequestEvent)` does the rest of the job when `supports()` allows so - lazily or not depending on the return value of `supports()`. Of course, this remains compatible with non-lazy logics, see `AbstractListener::__invoke()`. Commits ------- b20ebe6b90 [Security/Http] call auth listeners/guards eagerly when they "support" the request
2 parents 6fbd0fc + 31af8f7 commit 7629834

17 files changed

+261
-193
lines changed

Firewall.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ protected function handleRequest(GetResponseEvent $event, $listeners)
138138
if (\is_callable($listener)) {
139139
$listener($event);
140140
} else {
141-
@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);
141+
@trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($listener), AbstractListener::class), E_USER_DEPRECATED);
142142
$listener->handle($event);
143143
}
144144

Firewall/AbstractAuthenticationListener.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
* @author Fabien Potencier <[email protected]>
5050
* @author Johannes M. Schmitt <[email protected]>
5151
*/
52-
abstract class AbstractAuthenticationListener implements ListenerInterface
52+
abstract class AbstractAuthenticationListener extends AbstractListener implements ListenerInterface
5353
{
5454
use LegacyListenerTrait;
5555

@@ -105,20 +105,24 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer
105105
$this->rememberMeServices = $rememberMeServices;
106106
}
107107

108+
/**
109+
* {@inheritdoc}
110+
*/
111+
public function supports(Request $request): ?bool
112+
{
113+
return $this->requiresAuthentication($request);
114+
}
115+
108116
/**
109117
* Handles form based authentication.
110118
*
111119
* @throws \RuntimeException
112120
* @throws SessionUnavailableException
113121
*/
114-
public function __invoke(RequestEvent $event)
122+
public function authenticate(RequestEvent $event)
115123
{
116124
$request = $event->getRequest();
117125

118-
if (!$this->requiresAuthentication($request)) {
119-
return;
120-
}
121-
122126
if (!$request->hasSession()) {
123127
throw new \RuntimeException('This authentication method requires a session.');
124128
}

Firewall/AbstractListener.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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\Firewall;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Event\RequestEvent;
16+
17+
/**
18+
* A base class for listeners that can tell whether they should authenticate incoming requests.
19+
*
20+
* @author Nicolas Grekas <[email protected]>
21+
*/
22+
abstract class AbstractListener
23+
{
24+
final public function __invoke(RequestEvent $event)
25+
{
26+
if (false !== $this->supports($event->getRequest())) {
27+
$this->authenticate($event);
28+
}
29+
}
30+
31+
/**
32+
* Tells whether the authenticate() method should be called or not depending on the incoming request.
33+
*
34+
* Returning null means authenticate() can be called lazily when accessing the token storage.
35+
*/
36+
abstract public function supports(Request $request): ?bool;
37+
38+
/**
39+
* Does whatever is required to authenticate the request, typically calling $event->setResponse() internally.
40+
*/
41+
abstract public function authenticate(RequestEvent $event);
42+
}

Firewall/AbstractPreAuthenticatedListener.php

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
*
3636
* @internal since Symfony 4.3
3737
*/
38-
abstract class AbstractPreAuthenticatedListener implements ListenerInterface
38+
abstract class AbstractPreAuthenticatedListener extends AbstractListener implements ListenerInterface
3939
{
4040
use LegacyListenerTrait;
4141

@@ -56,20 +56,31 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM
5656
}
5757

5858
/**
59-
* Handles pre-authentication.
59+
* {@inheritdoc}
6060
*/
61-
public function __invoke(RequestEvent $event)
61+
public function supports(Request $request): ?bool
6262
{
63-
$request = $event->getRequest();
64-
6563
try {
66-
list($user, $credentials) = $this->getPreAuthenticatedData($request);
64+
$request->attributes->set('_pre_authenticated_data', $this->getPreAuthenticatedData($request));
6765
} catch (BadCredentialsException $e) {
6866
$this->clearToken($e);
6967

70-
return;
68+
return false;
7169
}
7270

71+
return true;
72+
}
73+
74+
/**
75+
* Handles pre-authentication.
76+
*/
77+
public function authenticate(RequestEvent $event)
78+
{
79+
$request = $event->getRequest();
80+
81+
[$user, $credentials] = $request->attributes->get('_pre_authenticated_data');
82+
$request->attributes->remove('_pre_authenticated_data');
83+
7384
if (null !== $this->logger) {
7485
$this->logger->debug('Checking current security token.', ['token' => (string) $this->tokenStorage->getToken()]);
7586
}

Firewall/AccessListener.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespace Symfony\Component\Security\Http\Firewall;
1313

14+
use Symfony\Component\HttpFoundation\Request;
1415
use Symfony\Component\HttpKernel\Event\RequestEvent;
1516
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
1617
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1718
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
19+
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
1820
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
1921
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
2022
use Symfony\Component\Security\Http\AccessMapInterface;
@@ -27,7 +29,7 @@
2729
*
2830
* @final since Symfony 4.3
2931
*/
30-
class AccessListener implements ListenerInterface
32+
class AccessListener extends AbstractListener implements ListenerInterface
3133
{
3234
use LegacyListenerTrait;
3335

@@ -44,23 +46,35 @@ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionM
4446
$this->authManager = $authManager;
4547
}
4648

49+
/**
50+
* {@inheritdoc}
51+
*/
52+
public function supports(Request $request): ?bool
53+
{
54+
[$attributes] = $this->map->getPatterns($request);
55+
$request->attributes->set('_access_control_attributes', $attributes);
56+
57+
return $attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes ? true : null;
58+
}
59+
4760
/**
4861
* Handles access authorization.
4962
*
5063
* @throws AccessDeniedException
5164
* @throws AuthenticationCredentialsNotFoundException
5265
*/
53-
public function __invoke(RequestEvent $event)
66+
public function authenticate(RequestEvent $event)
5467
{
5568
if (!$event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) {
5669
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
5770
}
5871

5972
$request = $event->getRequest();
6073

61-
list($attributes) = $this->map->getPatterns($request);
74+
$attributes = $request->attributes->get('_access_control_attributes');
75+
$request->attributes->remove('_access_control_attributes');
6276

63-
if (!$attributes) {
77+
if (!$attributes || ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] === $attributes && $event instanceof LazyResponseEvent)) {
6478
return;
6579
}
6680

Firewall/AnonymousAuthenticationListener.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Security\Http\Firewall;
1313

1414
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\HttpFoundation\Request;
1516
use Symfony\Component\HttpKernel\Event\RequestEvent;
1617
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
1718
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
@@ -26,7 +27,7 @@
2627
*
2728
* @final since Symfony 4.3
2829
*/
29-
class AnonymousAuthenticationListener implements ListenerInterface
30+
class AnonymousAuthenticationListener extends AbstractListener implements ListenerInterface
3031
{
3132
use LegacyListenerTrait;
3233

@@ -43,10 +44,18 @@ public function __construct(TokenStorageInterface $tokenStorage, string $secret,
4344
$this->logger = $logger;
4445
}
4546

47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function supports(Request $request): ?bool
51+
{
52+
return null; // always run authenticate() lazily with lazy firewalls
53+
}
54+
4655
/**
4756
* Handles anonymous authentication.
4857
*/
49-
public function __invoke(RequestEvent $event)
58+
public function authenticate(RequestEvent $event)
5059
{
5160
if (null !== $this->tokenStorage->getToken()) {
5261
return;

Firewall/BasicAuthenticationListener.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*
3030
* @final since Symfony 4.3
3131
*/
32-
class BasicAuthenticationListener implements ListenerInterface
32+
class BasicAuthenticationListener extends AbstractListener implements ListenerInterface
3333
{
3434
use LegacyListenerTrait;
3535

@@ -55,10 +55,18 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM
5555
$this->ignoreFailure = false;
5656
}
5757

58+
/**
59+
* {@inheritdoc}
60+
*/
61+
public function supports(Request $request): ?bool
62+
{
63+
return null !== $request->headers->get('PHP_AUTH_USER');
64+
}
65+
5866
/**
5967
* Handles basic authentication.
6068
*/
61-
public function __invoke(RequestEvent $event)
69+
public function authenticate(RequestEvent $event)
6270
{
6371
$request = $event->getRequest();
6472

Firewall/ChannelListener.php

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Security\Http\Firewall;
1313

1414
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\HttpFoundation\Request;
1516
use Symfony\Component\HttpKernel\Event\RequestEvent;
1617
use Symfony\Component\Security\Http\AccessMapInterface;
1718
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
@@ -24,7 +25,7 @@
2425
*
2526
* @final since Symfony 4.3
2627
*/
27-
class ChannelListener implements ListenerInterface
28+
class ChannelListener extends AbstractListener implements ListenerInterface
2829
{
2930
use LegacyListenerTrait;
3031

@@ -42,10 +43,8 @@ public function __construct(AccessMapInterface $map, AuthenticationEntryPointInt
4243
/**
4344
* Handles channel management.
4445
*/
45-
public function __invoke(RequestEvent $event)
46+
public function supports(Request $request): ?bool
4647
{
47-
$request = $event->getRequest();
48-
4948
list(, $channel) = $this->map->getPatterns($request);
5049

5150
if ('https' === $channel && !$request->isSecure()) {
@@ -59,21 +58,26 @@ public function __invoke(RequestEvent $event)
5958
}
6059
}
6160

62-
$response = $this->authenticationEntryPoint->start($request);
63-
64-
$event->setResponse($response);
65-
66-
return;
61+
return true;
6762
}
6863

6964
if ('http' === $channel && $request->isSecure()) {
7065
if (null !== $this->logger) {
7166
$this->logger->info('Redirecting to HTTP.');
7267
}
7368

74-
$response = $this->authenticationEntryPoint->start($request);
75-
76-
$event->setResponse($response);
69+
return true;
7770
}
71+
72+
return false;
73+
}
74+
75+
public function authenticate(RequestEvent $event)
76+
{
77+
$request = $event->getRequest();
78+
79+
$response = $this->authenticationEntryPoint->start($request);
80+
81+
$event->setResponse($response);
7882
}
7983
}

Firewall/ContextListener.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
*
4242
* @final since Symfony 4.3
4343
*/
44-
class ContextListener implements ListenerInterface
44+
class ContextListener extends AbstractListener implements ListenerInterface
4545
{
4646
use LegacyListenerTrait;
4747

@@ -84,10 +84,18 @@ public function setLogoutOnUserChange($logoutOnUserChange)
8484
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), E_USER_DEPRECATED);
8585
}
8686

87+
/**
88+
* {@inheritdoc}
89+
*/
90+
public function supports(Request $request): ?bool
91+
{
92+
return null; // always run authenticate() lazily with lazy firewalls
93+
}
94+
8795
/**
8896
* Reads the Security Token from the session.
8997
*/
90-
public function __invoke(RequestEvent $event)
98+
public function authenticate(RequestEvent $event)
9199
{
92100
if (!$this->registered && null !== $this->dispatcher && $event->isMasterRequest()) {
93101
$this->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'onKernelResponse']);

Firewall/LogoutListener.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
*
3131
* @final since Symfony 4.3
3232
*/
33-
class LogoutListener implements ListenerInterface
33+
class LogoutListener extends AbstractListener implements ListenerInterface
3434
{
3535
use LegacyListenerTrait;
3636

@@ -63,6 +63,14 @@ public function addHandler(LogoutHandlerInterface $handler)
6363
$this->handlers[] = $handler;
6464
}
6565

66+
/**
67+
* {@inheritdoc}
68+
*/
69+
public function supports(Request $request): ?bool
70+
{
71+
return $this->requiresLogout($request);
72+
}
73+
6674
/**
6775
* Performs the logout if requested.
6876
*
@@ -72,14 +80,10 @@ public function addHandler(LogoutHandlerInterface $handler)
7280
* @throws LogoutException if the CSRF token is invalid
7381
* @throws \RuntimeException if the LogoutSuccessHandlerInterface instance does not return a response
7482
*/
75-
public function __invoke(RequestEvent $event)
83+
public function authenticate(RequestEvent $event)
7684
{
7785
$request = $event->getRequest();
7886

79-
if (!$this->requiresLogout($request)) {
80-
return;
81-
}
82-
8387
if (null !== $this->csrfTokenManager) {
8488
$csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
8589

0 commit comments

Comments
 (0)