Skip to content

Commit 58b648f

Browse files
committed
feature #41175 [Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication (Seldaek)
This PR was squashed before being merged into the 5.3-dev branch. Discussion ---------- [Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | yes | New feature? | yes ish <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | Fix #40971, Fix #28314, Fix #18384 | License | MIT | Doc PR | symfony/symfony-docs#... <!-- required for new features --> This is a possible implementation to gather feedback mostly.. `TokenVerifierInterface` naming is kinda bad perhaps.. But my goal would be to merge it in TokenProviderInterface for 6.0 so it's not so important. Not sure if/how to best indicate this in terms of deprecation notices. Anyway wondering if this would be an acceptable implementation (ideally in an application I would probably override the new methods from DoctrineTokenProvider to something like this which is less of a hack and does expiration properly: ```php public function verifyToken(PersistentTokenInterface $token, string $tokenValue) { if (hash_equals($token->getTokenValue(), $tokenValue)) { return true; } if (!$this->cache->hasItem('rememberme-' . $token->getSeries())) { return false; } /** `@var` CacheItem $item */ $item = $this->cache->getItem('rememberme-' . $token->getSeries()); $oldToken = $item->get(); return hash_equals($oldToken, $tokenValue); } public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void { $this->updateToken($token->getSeries(), $tokenValue, $lastUsed); /** `@var` CacheItem $item */ $item = $this->cache->getItem('rememberme-'.$token->getSeries()); $item->set($token->getTokenValue()); $item->expiresAfter(60); $this->cache->save($item); } ``` If you think it'd be fine to require optionally the cache inside DoctrineTokenProvider to enable this feature instead of the hackish way I did it, that'd be ok for me too. The current `DoctrineTokenProvider` implementation of `TokenVerifierInterface` relies on the lucky fact that series are generated using `base64_encode(random_bytes(64))` which always ends in the `==` padding of base64, so that allowed me to store an alternative token value temporarily by replacing `==` with `_`. Alternative implementation options: 1. Inject cache in `DoctrineTokenProvider` and do a proper implementation (as shown above) that way 2. Do not implement at all in `DoctrineTokenProvider` and let users who care implement this themselves. 3. Implement as a new `token_verifier` option that could be configured on the `firewall->remember_me` key so you can pass an implementation if needed, and possibly ship a default one using cache that could be autoconfigured 4. Add events that allow modifying the token to be verified, and allow receiving the newly updated token incl series, instead of TokenVerifierInterface, but then we need to inject a dispatcher in RememberMeAuthenticator. `@chalasr` `@wouterj` sorry for the long description but in the hope of getting this included in 5.3.0, if you can provide guidance I will happily work on this further tomorrow to try and wrap it up ASAP. Commits ------- 1992337d87 [Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication
2 parents 0996901 + 29ec5ed commit 58b648f

File tree

5 files changed

+66
-0
lines changed

5 files changed

+66
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
/**
18+
* Cleans up the remember me verifier cache if cache is missing.
19+
*
20+
* @author Jordi Boggiano <[email protected]>
21+
*/
22+
class CleanRememberMeVerifierPass implements CompilerPassInterface
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function process(ContainerBuilder $container)
28+
{
29+
if (!$container->hasDefinition('cache.system')) {
30+
$container->removeDefinition('cache.security_token_verifier');
31+
}
32+
}
33+
}

DependencyInjection/Security/Factory/RememberMeFactory.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
2020
use Symfony\Component\DependencyInjection\ChildDefinition;
2121
use Symfony\Component\DependencyInjection\ContainerBuilder;
22+
use Symfony\Component\DependencyInjection\ContainerInterface;
2223
use Symfony\Component\DependencyInjection\Definition;
2324
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
2425
use Symfony\Component\DependencyInjection\Reference;
2526
use Symfony\Component\HttpFoundation\Cookie;
27+
use Symfony\Component\Security\Core\Authentication\RememberMe\CacheTokenVerifier;
2628
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;
2729

2830
/**
@@ -120,10 +122,12 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
120122
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
121123
} elseif (isset($config['token_provider'])) {
122124
$tokenProviderId = $this->createTokenProvider($container, $firewallName, $config['token_provider']);
125+
$tokenVerifier = $this->createTokenVerifier($container, $firewallName, $config['token_verifier'] ?? null);
123126
$container->setDefinition($rememberMeHandlerId, new ChildDefinition('security.authenticator.persistent_remember_me_handler'))
124127
->replaceArgument(0, new Reference($tokenProviderId))
125128
->replaceArgument(2, new Reference($userProviderId))
126129
->replaceArgument(4, $config)
130+
->replaceArgument(6, $tokenVerifier)
127131
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
128132
} else {
129133
$signatureHasherId = 'security.authenticator.remember_me_signature_hasher.'.$firewallName;
@@ -218,6 +222,9 @@ public function addConfiguration(NodeDefinition $node)
218222
->end()
219223
->end()
220224
->end()
225+
->end()
226+
->scalarNode('token_verifier')
227+
->info('The service ID of a custom rememberme token verifier.')
221228
->end();
222229

223230
foreach ($this->options as $name => $value) {
@@ -308,4 +315,20 @@ private function createTokenProvider(ContainerBuilder $container, string $firewa
308315

309316
return $tokenProviderId;
310317
}
318+
319+
private function createTokenVerifier(ContainerBuilder $container, string $firewallName, ?string $serviceId): Reference
320+
{
321+
if ($serviceId) {
322+
return new Reference($serviceId);
323+
}
324+
325+
$tokenVerifierId = 'security.remember_me.token_verifier.'.$firewallName;
326+
327+
$container->register($tokenVerifierId, CacheTokenVerifier::class)
328+
->addArgument(new Reference('cache.security_token_verifier', ContainerInterface::NULL_ON_INVALID_REFERENCE))
329+
->addArgument(60)
330+
->addArgument('rememberme-'.$firewallName.'-stale-');
331+
332+
return new Reference($tokenVerifierId, ContainerInterface::NULL_ON_INVALID_REFERENCE);
333+
}
311334
}

Resources/config/schema/security-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@
350350
<xsd:attribute name="secret" type="xsd:string" use="required" />
351351
<xsd:attribute name="service" type="xsd:string" />
352352
<xsd:attribute name="token-provider" type="xsd:string" />
353+
<xsd:attribute name="token-verifier" type="xsd:string" />
353354
<xsd:attribute name="catch-exceptions" type="xsd:boolean" />
354355
<xsd:attribute name="secure" type="remember_me_secure" />
355356
<xsd:attribute name="samesite" type="remember_me_samesite" />

Resources/config/security_authenticator_remember_me.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
service('request_stack'),
5252
abstract_arg('options'),
5353
service('logger')->nullOnInvalid(),
54+
abstract_arg('token verifier'),
5455
])
5556
->tag('monolog.logger', ['channel' => 'security'])
5657

@@ -87,5 +88,11 @@
8788
service('logger')->nullOnInvalid(),
8889
])
8990
->tag('monolog.logger', ['channel' => 'security'])
91+
92+
// Cache
93+
->set('cache.security_token_verifier')
94+
->parent('cache.system')
95+
->private()
96+
->tag('cache.pool')
9097
;
9198
};

SecurityBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass;
1515
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass;
1616
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass;
17+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\CleanRememberMeVerifierPass;
1718
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfFeaturesPass;
1819
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterEntryPointPass;
1920
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterGlobalSecurityEventListenersPass;
@@ -76,6 +77,7 @@ public function build(ContainerBuilder $container)
7677
$container->addCompilerPass(new AddExpressionLanguageProvidersPass());
7778
$container->addCompilerPass(new AddSecurityVotersPass());
7879
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING);
80+
$container->addCompilerPass(new CleanRememberMeVerifierPass());
7981
$container->addCompilerPass(new RegisterCsrfFeaturesPass());
8082
$container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200);
8183
$container->addCompilerPass(new RegisterLdapLocatorPass());

0 commit comments

Comments
 (0)