Removed AnonymousToken from the authenticator system

* Anonymous users are actual to unauthenticated users, both are now represented by no token
* Added a PUBLIC_ACCESS Security attribute to be used in access_control
* Deprecated "anonymous: lazy" in favor of "lazy: true"
This commit is contained in:
Wouter de Jong 2020-05-02 15:08:08 +02:00 committed by Fabien Potencier
parent 28bb74cd50
commit ac84a6c5d9
14 changed files with 142 additions and 158 deletions

View File

@ -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

View File

@ -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([])

View File

@ -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()
;

View File

@ -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))

View File

@ -111,13 +111,6 @@
<argument type="service" id="property_accessor" on-invalid="null" />
</service>
<service id="security.authenticator.anonymous"
class="Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator"
abstract="true">
<argument type="abstract">secret</argument>
<argument type="service" id="security.untracked_token_storage" />
</service>
<service id="security.authenticator.remember_me"
class="Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator"
abstract="true">

View File

@ -26,7 +26,8 @@ security:
firewalls:
secure:
pattern: ^/
anonymous: lazy
anonymous: ~
lazy: true
stateless: false
guard:
authenticators:

View File

@ -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:

View File

@ -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()) {

View File

@ -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
*/

View File

@ -1,67 +0,0 @@
<?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\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 <wouter@wouterj.nl>
* @author Fabien Potencier <fabien@symfony.com>
*
* @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;
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -1,55 +0,0 @@
<?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\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());
}
}

View File

@ -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();