diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md index 1b26220e72..42fd920b30 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 dfac1554d4..e3f2633298 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -197,6 +197,7 @@ class MainConfiguration implements ConfigurationInterface ->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 7caff9fa05..1feba8bcb1 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 @@ class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactory 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 @@ class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactory ->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 7b5edc7cac..55916c05e2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -112,6 +112,13 @@ class SecurityExtension extends Extension implements PrependExtensionInterface 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 @@ class SecurityExtension extends Extension implements PrependExtensionInterface 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 00691b46d5..26e47613c1 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 5b851e394d..101d0c5b1b 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 ad8beee94c..7fc9f12174 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 9036bba029..ac24795d99 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 7d3fa73e1b..0c066aeee3 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 @@ class AuthorizationCheckerTest extends TestCase $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 c0420b5d4c..0000000000 --- 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 605131c48b..8da2a994bf 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php @@ -31,17 +31,21 @@ use Symfony\Component\Security\Http\Event\LazyResponseEvent; */ 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 @@ class AccessListener extends AbstractListener [$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 @@ class AccessListener extends AbstractListener 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 @@ class AccessListener extends AbstractListener } 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 678de4c34d..52366fb548 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -144,7 +144,9 @@ class ExceptionListener 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 d5593bb375..0000000000 --- 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 75798d055a..9748e6522c 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\AuthenticationManagerInterfac 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 @@ class AccessListenerTest extends TestCase $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();