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:
parent
28bb74cd50
commit
ac84a6c5d9
|
@ -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
|
||||
|
|
|
@ -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([])
|
||||
|
|
|
@ -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()
|
||||
;
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -26,7 +26,8 @@ security:
|
|||
firewalls:
|
||||
secure:
|
||||
pattern: ^/
|
||||
anonymous: lazy
|
||||
anonymous: ~
|
||||
lazy: true
|
||||
stateless: false
|
||||
guard:
|
||||
authenticators:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
Reference in New Issue