diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md
index 1b26220e723f5..42fd920b30ab9 100644
--- a/UPGRADE-5.1.md
+++ b/UPGRADE-5.1.md
@@ -112,6 +112,24 @@ Routing
SecurityBundle
--------------
+ * Deprecated `anonymous: lazy` in favor of `lazy: true`
+
+ *Before*
+ ```yaml
+ security:
+ firewalls:
+ main:
+ anonymous: lazy
+ ```
+
+ *After*
+ ```yaml
+ security:
+ firewalls:
+ main:
+ anonymous: true
+ lazy: true
+ ```
* Marked the `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`,
`HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory`
and `X509Factory` as `@internal`. Instead of extending these classes, create your own implementation based on
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
index dfac1554d4bab..e3f2633298178 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
@@ -197,6 +197,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
->scalarNode('entry_point')->end()
->scalarNode('provider')->end()
->booleanNode('stateless')->defaultFalse()->end()
+ ->booleanNode('lazy')->defaultFalse()->end()
->scalarNode('context')->cannotBeEmpty()->end()
->arrayNode('logout')
->treatTrueLike([])
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php
index 7caff9fa05913..1feba8bcb1899 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php
@@ -12,6 +12,7 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
+use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Parameter;
@@ -46,16 +47,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider,
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
{
- if (null === $config['secret']) {
- $config['secret'] = new Parameter('container.build_hash');
- }
-
- $authenticatorId = 'security.authenticator.anonymous.'.$firewallName;
- $container
- ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.anonymous'))
- ->replaceArgument(0, $config['secret']);
-
- return $authenticatorId;
+ throw new InvalidConfigurationException(sprintf('The authenticator manager no longer has "anonymous" security. Please remove this option under the "%s" firewall'.($config['lazy'] ? ' and add "lazy: true"' : '').'.', $firewallName));
}
public function getPosition()
@@ -76,7 +68,7 @@ public function addConfiguration(NodeDefinition $builder)
->then(function ($v) { return ['lazy' => true]; })
->end()
->children()
- ->booleanNode('lazy')->defaultFalse()->end()
+ ->booleanNode('lazy')->defaultFalse()->setDeprecated('symfony/security-bundle', '5.1', 'Using "anonymous: lazy" to make the firewall lazy is deprecated, use "lazy: true" instead.')->end()
->scalarNode('secret')->defaultNull()->end()
->end()
;
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
index 7b5edc7cac796..55916c05e22de 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
@@ -112,6 +112,13 @@ public function load(array $configs, ContainerBuilder $container)
if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) {
$loader->load('security_authenticator.xml');
+
+ // The authenticator system no longer has anonymous tokens. This makes sure AccessListener
+ // and AuthorizationChecker do not throw AuthenticationCredentialsNotFoundException when no
+ // token is available in the token storage.
+ $container->getDefinition('security.access_listener')->setArgument(4, false);
+ $container->getDefinition('security.authorization_checker')->setArgument(4, false);
+ $container->getDefinition('security.authorization_checker')->setArgument(5, false);
} else {
$loader->load('security_legacy.xml');
}
@@ -269,7 +276,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 = new ChildDefinition($firewall['stateless'] || empty($firewall['anonymous']['lazy']) ? 'security.firewall.context' : 'security.firewall.lazy_context');
+ $isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']);
+ $context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context');
$context = $container->setDefinition($contextId, $context);
$context
->replaceArgument(0, new IteratorArgument($listeners))
diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml
index 00691b46d59c8..26e47613c102a 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml
@@ -111,13 +111,6 @@
-
- secret
-
-
-
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml
index 5b851e394dd65..101d0c5b1b52c 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml
@@ -26,7 +26,8 @@ security:
firewalls:
secure:
pattern: ^/
- anonymous: lazy
+ anonymous: ~
+ lazy: true
stateless: false
guard:
authenticators:
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml
index ad8beee94c2e0..7fc9f12174251 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml
@@ -27,7 +27,8 @@ security:
check_path: /login_check
default_target_path: /profile
logout: ~
- anonymous: lazy
+ anonymous: ~
+ lazy: true
# This firewall is here just to check its the logout functionality
second_area:
diff --git a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
index 9036bba029d2e..ac24795d99827 100644
--- a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
+++ b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
@@ -29,24 +29,30 @@ class AuthorizationChecker implements AuthorizationCheckerInterface
private $accessDecisionManager;
private $authenticationManager;
private $alwaysAuthenticate;
+ private $exceptionOnNoToken;
- public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, AccessDecisionManagerInterface $accessDecisionManager, bool $alwaysAuthenticate = false)
+ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, AccessDecisionManagerInterface $accessDecisionManager, bool $alwaysAuthenticate = false, bool $exceptionOnNoToken = true)
{
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
$this->accessDecisionManager = $accessDecisionManager;
$this->alwaysAuthenticate = $alwaysAuthenticate;
+ $this->exceptionOnNoToken = $exceptionOnNoToken;
}
/**
* {@inheritdoc}
*
- * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token
+ * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token and $exceptionOnNoToken is set to true
*/
final public function isGranted($attribute, $subject = null): bool
{
if (null === ($token = $this->tokenStorage->getToken())) {
- throw new AuthenticationCredentialsNotFoundException('The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.');
+ if ($this->exceptionOnNoToken) {
+ throw new AuthenticationCredentialsNotFoundException('The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.');
+ }
+
+ return false;
}
if ($this->alwaysAuthenticate || !$token->isAuthenticated()) {
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php
index 7d3fa73e1b1ce..0c066aeee3b65 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php
@@ -73,6 +73,13 @@ public function testVoteWithoutAuthenticationToken()
$this->authorizationChecker->isGranted('ROLE_FOO');
}
+ public function testVoteWithoutAuthenticationTokenAndExceptionOnNoTokenIsFalse()
+ {
+ $authorizationChecker = new AuthorizationChecker($this->tokenStorage, $this->authenticationManager, $this->accessDecisionManager, false, false);
+
+ $this->assertFalse($authorizationChecker->isGranted('ROLE_FOO'));
+ }
+
/**
* @dataProvider isGrantedProvider
*/
diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php
deleted file mode 100644
index c0420b5d4cfec..0000000000000
--- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php
+++ /dev/null
@@ -1,67 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\Security\Http\Authenticator;
-
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
-use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
-use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
-use Symfony\Component\Security\Core\Exception\AuthenticationException;
-use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport;
-use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
-
-/**
- * @author Wouter de Jong
- * @author Fabien Potencier
- *
- * @final
- * @experimental in 5.1
- */
-class AnonymousAuthenticator implements AuthenticatorInterface
-{
- private $secret;
- private $tokenStorage;
-
- public function __construct(string $secret, TokenStorageInterface $tokenStorage)
- {
- $this->secret = $secret;
- $this->tokenStorage = $tokenStorage;
- }
-
- public function supports(Request $request): ?bool
- {
- // do not overwrite already stored tokens (i.e. from the session)
- // the `null` return value indicates that this authenticator supports lazy firewalls
- return null === $this->tokenStorage->getToken() ? null : false;
- }
-
- public function authenticate(Request $request): PassportInterface
- {
- return new AnonymousPassport();
- }
-
- public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
- {
- return new AnonymousToken($this->secret, 'anon.', []);
- }
-
- public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
- {
- return null; // let the original request continue
- }
-
- public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
- {
- return null;
- }
-}
diff --git a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
index 605131c48bbfb..8da2a994bf48f 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
@@ -31,17 +31,21 @@
*/
class AccessListener extends AbstractListener
{
+ const PUBLIC_ACCESS = 'PUBLIC_ACCESS';
+
private $tokenStorage;
private $accessDecisionManager;
private $map;
private $authManager;
+ private $exceptionOnNoToken;
- public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, AuthenticationManagerInterface $authManager)
+ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, AuthenticationManagerInterface $authManager, bool $exceptionOnNoToken = true)
{
$this->tokenStorage = $tokenStorage;
$this->accessDecisionManager = $accessDecisionManager;
$this->map = $map;
$this->authManager = $authManager;
+ $this->exceptionOnNoToken = $exceptionOnNoToken;
}
/**
@@ -52,18 +56,18 @@ public function supports(Request $request): ?bool
[$attributes] = $this->map->getPatterns($request);
$request->attributes->set('_access_control_attributes', $attributes);
- return $attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes ? true : null;
+ return $attributes && ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes && [self::PUBLIC_ACCESS] !== $attributes) ? true : null;
}
/**
* Handles access authorization.
*
* @throws AccessDeniedException
- * @throws AuthenticationCredentialsNotFoundException
+ * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token and $exceptionOnNoToken is set to true
*/
public function authenticate(RequestEvent $event)
{
- if (!$event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) {
+ if (!$event instanceof LazyResponseEvent && null === ($token = $this->tokenStorage->getToken()) && $this->exceptionOnNoToken) {
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
}
@@ -76,8 +80,26 @@ public function authenticate(RequestEvent $event)
return;
}
- if ($event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) {
- throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
+ if ($event instanceof LazyResponseEvent) {
+ $token = $this->tokenStorage->getToken();
+ }
+
+ if (null === $token) {
+ if ($this->exceptionOnNoToken) {
+ throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
+ }
+
+ if ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] === $attributes) {
+ trigger_deprecation('symfony/security-http', '5.1', 'Using "IS_AUTHENTICATED_ANONYMOUSLY" in your access_control rules when using the authenticator Security system is deprecated, use "PUBLIC_ACCESS" instead.');
+
+ return;
+ }
+
+ if ([self::PUBLIC_ACCESS] === $attributes) {
+ return;
+ }
+
+ throw $this->createAccessDeniedException($request, $attributes);
}
if (!$token->isAuthenticated()) {
@@ -86,11 +108,16 @@ public function authenticate(RequestEvent $event)
}
if (!$this->accessDecisionManager->decide($token, $attributes, $request, true)) {
- $exception = new AccessDeniedException();
- $exception->setAttributes($attributes);
- $exception->setSubject($request);
-
- throw $exception;
+ throw $this->createAccessDeniedException($request, $attributes);
}
}
+
+ private function createAccessDeniedException(Request $request, array $attributes)
+ {
+ $exception = new AccessDeniedException();
+ $exception->setAttributes($attributes);
+ $exception->setSubject($request);
+
+ return $exception;
+ }
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
index 678de4c34d947..52366fb5487c6 100644
--- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
@@ -144,7 +144,9 @@ private function handleAccessDeniedException(ExceptionEvent $event, AccessDenied
try {
$insufficientAuthenticationException = new InsufficientAuthenticationException('Full authentication is required to access this resource.', 0, $exception);
- $insufficientAuthenticationException->setToken($token);
+ if (null !== $token) {
+ $insufficientAuthenticationException->setToken($token);
+ }
$event->setResponse($this->startAuthentication($event->getRequest(), $insufficientAuthenticationException));
} catch (\Exception $e) {
diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php
deleted file mode 100644
index d5593bb375093..0000000000000
--- a/src/Symfony/Component/Security/Http/Tests/Authenticator/AnonymousAuthenticatorTest.php
+++ /dev/null
@@ -1,55 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\Security\Http\Tests\Authenticator;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
-use Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator;
-
-class AnonymousAuthenticatorTest extends TestCase
-{
- private $tokenStorage;
- private $authenticator;
- private $request;
-
- protected function setUp(): void
- {
- $this->tokenStorage = $this->createMock(TokenStorageInterface::class);
- $this->authenticator = new AnonymousAuthenticator('s3cr3t', $this->tokenStorage);
- $this->request = new Request();
- }
-
- /**
- * @dataProvider provideSupportsData
- */
- public function testSupports($tokenAlreadyAvailable, $result)
- {
- $this->tokenStorage->expects($this->any())->method('getToken')->willReturn($tokenAlreadyAvailable ? $this->createMock(TokenStorageInterface::class) : null);
-
- $this->assertEquals($result, $this->authenticator->supports($this->request));
- }
-
- public function provideSupportsData()
- {
- yield [true, null];
- yield [false, false];
- }
-
- public function testAuthenticatedToken()
- {
- $token = $this->authenticator->createAuthenticatedToken($this->authenticator->authenticate($this->request), 'main');
-
- $this->assertTrue($token->isAuthenticated());
- $this->assertEquals('anon.', $token->getUser());
- }
-}
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
index 75798d055a385..9748e6522c6ad 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
@@ -19,6 +19,7 @@
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
use Symfony\Component\Security\Http\Firewall\AccessListener;
@@ -229,6 +230,55 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken()
$listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST));
}
+ public function testHandleWhenTheSecurityTokenStorageHasNoTokenAndExceptionOnTokenIsFalse()
+ {
+ $this->expectException(AccessDeniedException::class);
+ $tokenStorage = new TokenStorage();
+ $request = new Request();
+
+ $accessMap = $this->createMock(AccessMapInterface::class);
+ $accessMap->expects($this->any())
+ ->method('getPatterns')
+ ->with($this->equalTo($request))
+ ->willReturn([['foo' => 'bar'], null])
+ ;
+
+ $listener = new AccessListener(
+ $tokenStorage,
+ $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')->getMock(),
+ $accessMap,
+ $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock(),
+ false
+ );
+
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST));
+ }
+
+ public function testHandleWhenPublicAccessIsAllowedAndExceptionOnTokenIsFalse()
+ {
+ $tokenStorage = new TokenStorage();
+ $request = new Request();
+
+ $accessMap = $this->createMock(AccessMapInterface::class);
+ $accessMap->expects($this->any())
+ ->method('getPatterns')
+ ->with($this->equalTo($request))
+ ->willReturn([[AccessListener::PUBLIC_ACCESS], null])
+ ;
+
+ $listener = new AccessListener(
+ $tokenStorage,
+ $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')->getMock(),
+ $accessMap,
+ $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock(),
+ false
+ );
+
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST));
+
+ $this->expectNotToPerformAssertions();
+ }
+
public function testHandleMWithultipleAttributesShouldBeHandledAsAnd()
{
$request = new Request();