Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 09f9079

Browse files
committed
feature #36574 [Security] Removed anonymous in the new security system (wouterj)
This PR was merged into the 5.1-dev branch. Discussion ---------- [Security] Removed anonymous in the new security system | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | tbd This was one of the "Future considerations" of #33558: > Drop the AnonymousToken and AnonymousAuthenticator: Anonymous authentication has never made much sense and complicates things (e.g. the user can be a string). For access control, an anonymous user has the same meaning as an un-authenticated one (null). This require changes in the AccessListener and AuthorizationChecker and probably also a new Security attribute (to replace IS_AUTHENTICATED_ANONYMOUSLY). Related issues: #34909, #30609 This new experimental system is probably a once-in-a-lifetime change to make this change. @weaverryan and I have had some brainstorming about this. Some reasons why we think it makes 100% sense to do this change: * From a Security perspective, **a user that is not authenticated is similar to an "unknown" user**: They both have no rights at all. * **The higher level consequences of the AnonymousToken are confusing and inconsistent**: * It's hard to explain people new to Symfony Security that not being logged in still means you're authenticated within the Symfony app * To counter this, some higher level APIs explicitly mark anonymous tokens as not being authenticated, see e.g. the [`is_authenticated()` expression language function](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguageProvider.php#L33-L37) * The anonymous authentication resulted in the `IS_AUTHENTICATED` security attribute being removed from #35854, as there was no clear consensus on what its meaning should be * **Spring Security, which is where this originated from, makes Anonymous a very special case**: > Finally, there is an AnonymousAuthenticationFilter, which is chained after the normal authentication mechanisms and automatically adds an AnonymousAuthenticationToken to the SecurityContextHolder if there is no existing Authentication held there. > > Note that there is no real conceptual difference between a user who is “anonymously authenticated” and an unauthenticated user. Spring Security's anonymous authentication just gives you a more convenient way to configure your access-control attributes. Calls to servlet API calls such as getCallerPrincipal, for example, will still return null even though there is actually an anonymous authentication object in the SecurityContextHolder. * Symfony uses AnonymousToken much more than "just for convience in access-control attributes". **Removing anonymous tokens allows us to move towards only allowing `UserInterface` users**: #34909 --- Removing anonymous tokens do have an impact on `AccessListener` and `AuthorizationChecker`. These currently throw an exception if there is no token in the storage, instead of treating them like "unknown users" (i.e. no roles). See #30609 on a RFC about removing this exception. We can also see e.g. the [Twig `is_granted()` function explicitly catching this exception](https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php#L37-L52). * **To make the changes in `AccessListener` and `AuthorizationChecker` BC, a flag has been added - default enabled - to throw an exception when no token is present** (which is automatically disabled when the new system is used). In Symfony 5.4 (or whenever the new system is no longer experimental), we can deprecate this flag and in 6.0 we can never throw the exception anymore. * **`anonymous: lazy` has been deprecated in favor of `{ anonymous: true, lazy: true }`** This fixes the dependency on `AnonymousFactory` from the `SecurityExtension` and allows removing the `anonymous` option. * **Introduced `PUBLIC_ACCESS` Security attribute** as alternative of `IS_AUTHENTICATED_ANONYMOUSLY`. Both work in the new system, the latter only triggers a deprecation notice (but may be usefull to allow switching back and forth between old and new system). cc @javiereguiluz you might be interested, as I recently talked with you about this topic Commits ------- ac84a6c Removed AnonymousToken from the authenticator system
2 parents 28bb74c + ac84a6c commit 09f9079

File tree

14 files changed

+142
-158
lines changed

14 files changed

+142
-158
lines changed

UPGRADE-5.1.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ Routing
112112
SecurityBundle
113113
--------------
114114

115+
* Deprecated `anonymous: lazy` in favor of `lazy: true`
116+
117+
*Before*
118+
```yaml
119+
security:
120+
firewalls:
121+
main:
122+
anonymous: lazy
123+
```
124+
125+
*After*
126+
```yaml
127+
security:
128+
firewalls:
129+
main:
130+
anonymous: true
131+
lazy: true
132+
```
115133
* Marked the `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`,
116134
`HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory`
117135
and `X509Factory` as `@internal`. Instead of extending these classes, create your own implementation based on

src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
197197
->scalarNode('entry_point')->end()
198198
->scalarNode('provider')->end()
199199
->booleanNode('stateless')->defaultFalse()->end()
200+
->booleanNode('lazy')->defaultFalse()->end()
200201
->scalarNode('context')->cannotBeEmpty()->end()
201202
->arrayNode('logout')
202203
->treatTrueLike([])

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
1313

1414
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
15+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1516
use Symfony\Component\DependencyInjection\ChildDefinition;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
1718
use Symfony\Component\DependencyInjection\Parameter;
@@ -46,16 +47,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider,
4647

4748
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
4849
{
49-
if (null === $config['secret']) {
50-
$config['secret'] = new Parameter('container.build_hash');
51-
}
52-
53-
$authenticatorId = 'security.authenticator.anonymous.'.$firewallName;
54-
$container
55-
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.anonymous'))
56-
->replaceArgument(0, $config['secret']);
57-
58-
return $authenticatorId;
50+
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));
5951
}
6052

6153
public function getPosition()
@@ -76,7 +68,7 @@ public function addConfiguration(NodeDefinition $builder)
7668
->then(function ($v) { return ['lazy' => true]; })
7769
->end()
7870
->children()
79-
->booleanNode('lazy')->defaultFalse()->end()
71+
->booleanNode('lazy')->defaultFalse()->setDeprecated('symfony/security-bundle', '5.1', 'Using "anonymous: lazy" to make the firewall lazy is deprecated, use "lazy: true" instead.')->end()
8072
->scalarNode('secret')->defaultNull()->end()
8173
->end()
8274
;

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ public function load(array $configs, ContainerBuilder $container)
112112

113113
if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) {
114114
$loader->load('security_authenticator.xml');
115+
116+
// The authenticator system no longer has anonymous tokens. This makes sure AccessListener
117+
// and AuthorizationChecker do not throw AuthenticationCredentialsNotFoundException when no
118+
// token is available in the token storage.
119+
$container->getDefinition('security.access_listener')->setArgument(4, false);
120+
$container->getDefinition('security.authorization_checker')->setArgument(4, false);
121+
$container->getDefinition('security.authorization_checker')->setArgument(5, false);
115122
} else {
116123
$loader->load('security_legacy.xml');
117124
}
@@ -269,7 +276,8 @@ private function createFirewalls(array $config, ContainerBuilder $container)
269276
list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
270277

271278
$contextId = 'security.firewall.map.context.'.$name;
272-
$context = new ChildDefinition($firewall['stateless'] || empty($firewall['anonymous']['lazy']) ? 'security.firewall.context' : 'security.firewall.lazy_context');
279+
$isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']);
280+
$context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context');
273281
$context = $container->setDefinition($contextId, $context);
274282
$context
275283
->replaceArgument(0, new IteratorArgument($listeners))

src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,6 @@
111111
<argument type="service" id="property_accessor" on-invalid="null" />
112112
</service>
113113

114-
<service id="security.authenticator.anonymous"
115-
class="Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator"
116-
abstract="true">
117-
<argument type="abstract">secret</argument>
118-
<argument type="service" id="security.untracked_token_storage" />
119-
</service>
120-
121114
<service id="security.authenticator.remember_me"
122115
class="Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator"
123116
abstract="true">

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ security:
2626
firewalls:
2727
secure:
2828
pattern: ^/
29-
anonymous: lazy
29+
anonymous: ~
30+
lazy: true
3031
stateless: false
3132
guard:
3233
authenticators:

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ security:
2727
check_path: /login_check
2828
default_target_path: /profile
2929
logout: ~
30-
anonymous: lazy
30+
anonymous: ~
31+
lazy: true
3132

3233
# This firewall is here just to check its the logout functionality
3334
second_area:

src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,30 @@ class AuthorizationChecker implements AuthorizationCheckerInterface
2929
private $accessDecisionManager;
3030
private $authenticationManager;
3131
private $alwaysAuthenticate;
32+
private $exceptionOnNoToken;
3233

33-
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, AccessDecisionManagerInterface $accessDecisionManager, bool $alwaysAuthenticate = false)
34+
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, AccessDecisionManagerInterface $accessDecisionManager, bool $alwaysAuthenticate = false, bool $exceptionOnNoToken = true)
3435
{
3536
$this->tokenStorage = $tokenStorage;
3637
$this->authenticationManager = $authenticationManager;
3738
$this->accessDecisionManager = $accessDecisionManager;
3839
$this->alwaysAuthenticate = $alwaysAuthenticate;
40+
$this->exceptionOnNoToken = $exceptionOnNoToken;
3941
}
4042

4143
/**
4244
* {@inheritdoc}
4345
*
44-
* @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token
46+
* @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token and $exceptionOnNoToken is set to true
4547
*/
4648
final public function isGranted($attribute, $subject = null): bool
4749
{
4850
if (null === ($token = $this->tokenStorage->getToken())) {
49-
throw new AuthenticationCredentialsNotFoundException('The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.');
51+
if ($this->exceptionOnNoToken) {
52+
throw new AuthenticationCredentialsNotFoundException('The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.');
53+
}
54+
55+
return false;
5056
}
5157

5258
if ($this->alwaysAuthenticate || !$token->isAuthenticated()) {

src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ public function testVoteWithoutAuthenticationToken()
7373
$this->authorizationChecker->isGranted('ROLE_FOO');
7474
}
7575

76+
public function testVoteWithoutAuthenticationTokenAndExceptionOnNoTokenIsFalse()
77+
{
78+
$authorizationChecker = new AuthorizationChecker($this->tokenStorage, $this->authenticationManager, $this->accessDecisionManager, false, false);
79+
80+
$this->assertFalse($authorizationChecker->isGranted('ROLE_FOO'));
81+
}
82+
7683
/**
7784
* @dataProvider isGrantedProvider
7885
*/

src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php

Lines changed: 0 additions & 67 deletions
This file was deleted.

src/Symfony/Component/Security/Http/Firewall/AccessListener.php

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,21 @@
3131
*/
3232
class AccessListener extends AbstractListener
3333
{
34+
const PUBLIC_ACCESS = 'PUBLIC_ACCESS';
35+
3436
private $tokenStorage;
3537
private $accessDecisionManager;
3638
private $map;
3739
private $authManager;
40+
private $exceptionOnNoToken;
3841

39-
public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, AuthenticationManagerInterface $authManager)
42+
public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, AuthenticationManagerInterface $authManager, bool $exceptionOnNoToken = true)
4043
{
4144
$this->tokenStorage = $tokenStorage;
4245
$this->accessDecisionManager = $accessDecisionManager;
4346
$this->map = $map;
4447
$this->authManager = $authManager;
48+
$this->exceptionOnNoToken = $exceptionOnNoToken;
4549
}
4650

4751
/**
@@ -52,18 +56,18 @@ public function supports(Request $request): ?bool
5256
[$attributes] = $this->map->getPatterns($request);
5357
$request->attributes->set('_access_control_attributes', $attributes);
5458

55-
return $attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes ? true : null;
59+
return $attributes && ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes && [self::PUBLIC_ACCESS] !== $attributes) ? true : null;
5660
}
5761

5862
/**
5963
* Handles access authorization.
6064
*
6165
* @throws AccessDeniedException
62-
* @throws AuthenticationCredentialsNotFoundException
66+
* @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token and $exceptionOnNoToken is set to true
6367
*/
6468
public function authenticate(RequestEvent $event)
6569
{
66-
if (!$event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) {
70+
if (!$event instanceof LazyResponseEvent && null === ($token = $this->tokenStorage->getToken()) && $this->exceptionOnNoToken) {
6771
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
6872
}
6973

@@ -76,8 +80,26 @@ public function authenticate(RequestEvent $event)
7680
return;
7781
}
7882

79-
if ($event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) {
80-
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
83+
if ($event instanceof LazyResponseEvent) {
84+
$token = $this->tokenStorage->getToken();
85+
}
86+
87+
if (null === $token) {
88+
if ($this->exceptionOnNoToken) {
89+
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
90+
}
91+
92+
if ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] === $attributes) {
93+
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.');
94+
95+
return;
96+
}
97+
98+
if ([self::PUBLIC_ACCESS] === $attributes) {
99+
return;
100+
}
101+
102+
throw $this->createAccessDeniedException($request, $attributes);
81103
}
82104

83105
if (!$token->isAuthenticated()) {
@@ -86,11 +108,16 @@ public function authenticate(RequestEvent $event)
86108
}
87109

88110
if (!$this->accessDecisionManager->decide($token, $attributes, $request, true)) {
89-
$exception = new AccessDeniedException();
90-
$exception->setAttributes($attributes);
91-
$exception->setSubject($request);
92-
93-
throw $exception;
111+
throw $this->createAccessDeniedException($request, $attributes);
94112
}
95113
}
114+
115+
private function createAccessDeniedException(Request $request, array $attributes)
116+
{
117+
$exception = new AccessDeniedException();
118+
$exception->setAttributes($attributes);
119+
$exception->setSubject($request);
120+
121+
return $exception;
122+
}
96123
}

src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ private function handleAccessDeniedException(ExceptionEvent $event, AccessDenied
144144

145145
try {
146146
$insufficientAuthenticationException = new InsufficientAuthenticationException('Full authentication is required to access this resource.', 0, $exception);
147-
$insufficientAuthenticationException->setToken($token);
147+
if (null !== $token) {
148+
$insufficientAuthenticationException->setToken($token);
149+
}
148150

149151
$event->setResponse($this->startAuthentication($event->getRequest(), $insufficientAuthenticationException));
150152
} catch (\Exception $e) {

0 commit comments

Comments
 (0)