Skip to content
This repository was archived by the owner on May 31, 2024. It is now read-only.

Commit 129cfe5

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 9772d4e + 77d0b93 commit 129cfe5

20 files changed

+299
-210
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ CHANGELOG
1414
* Deprecated returning a non-boolean value when implementing `Guard\AuthenticatorInterface::checkCredentials()`.
1515
* Deprecated passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()`
1616
* Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.)
17+
* Added `AbstractListener` which replaces the deprecated `ListenerInterface`
1718

1819
4.3.0
1920
-----

Guard/Firewall/GuardAuthenticationListener.php

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Security\Guard\AuthenticatorInterface;
2222
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
2323
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
24+
use Symfony\Component\Security\Http\Firewall\AbstractListener;
2425
use Symfony\Component\Security\Http\Firewall\LegacyListenerTrait;
2526
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
2627
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
@@ -33,7 +34,7 @@
3334
*
3435
* @final since Symfony 4.3
3536
*/
36-
class GuardAuthenticationListener implements ListenerInterface
37+
class GuardAuthenticationListener extends AbstractListener implements ListenerInterface
3738
{
3839
use LegacyListenerTrait;
3940

@@ -62,9 +63,9 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat
6263
}
6364

6465
/**
65-
* Iterates over each authenticator to see if each wants to authenticate the request.
66+
* {@inheritdoc}
6667
*/
67-
public function __invoke(RequestEvent $event)
68+
public function supports(Request $request): ?bool
6869
{
6970
if (null !== $this->logger) {
7071
$context = ['firewall_key' => $this->providerKey];
@@ -76,7 +77,39 @@ public function __invoke(RequestEvent $event)
7677
$this->logger->debug('Checking for guard authentication credentials.', $context);
7778
}
7879

80+
$guardAuthenticators = [];
81+
7982
foreach ($this->guardAuthenticators as $key => $guardAuthenticator) {
83+
if (null !== $this->logger) {
84+
$this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
85+
}
86+
87+
if ($guardAuthenticator->supports($request)) {
88+
$guardAuthenticators[$key] = $guardAuthenticator;
89+
} elseif (null !== $this->logger) {
90+
$this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
91+
}
92+
}
93+
94+
if (!$guardAuthenticators) {
95+
return false;
96+
}
97+
98+
$request->attributes->set('_guard_authenticators', $guardAuthenticators);
99+
100+
return true;
101+
}
102+
103+
/**
104+
* Iterates over each authenticator to see if each wants to authenticate the request.
105+
*/
106+
public function authenticate(RequestEvent $event)
107+
{
108+
$request = $event->getRequest();
109+
$guardAuthenticators = $request->attributes->get('_guard_authenticators');
110+
$request->attributes->remove('_guard_authenticators');
111+
112+
foreach ($guardAuthenticators as $key => $guardAuthenticator) {
80113
// get a key that's unique to *this* guard authenticator
81114
// this MUST be the same as GuardAuthenticationProvider
82115
$uniqueGuardKey = $this->providerKey.'_'.$key;
@@ -97,19 +130,6 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator
97130
{
98131
$request = $event->getRequest();
99132
try {
100-
if (null !== $this->logger) {
101-
$this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
102-
}
103-
104-
// abort the execution of the authenticator if it doesn't support the request
105-
if (!$guardAuthenticator->supports($request)) {
106-
if (null !== $this->logger) {
107-
$this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
108-
}
109-
110-
return;
111-
}
112-
113133
if (null !== $this->logger) {
114134
$this->logger->debug('Calling getCredentials() on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
115135
}

Guard/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"require": {
1919
"php": "^7.1.3",
2020
"symfony/security-core": "^3.4.22|^4.2.3|^5.0",
21-
"symfony/security-http": "^4.3"
21+
"symfony/security-http": "^4.4.1"
2222
},
2323
"require-dev": {
2424
"psr/log": "~1.0"

Http/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

Http/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 <fabien@symfony.com>
5050
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
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
}

Http/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 <fabien@symfony.com>
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 <p@tchwork.com>
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+
}

Http/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
}

Http/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

Http/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;

Http/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

0 commit comments

Comments
 (0)