Skip to content

Commit 8eabd46

Browse files
wouterjchalasr
authored andcommitted
[Security] Rework the remember me system
1 parent 472c854 commit 8eabd46

36 files changed

+811
-298
lines changed

DependencyInjection/Compiler/RegisterGlobalSecurityEventListenersPass.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
2222
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
2323
use Symfony\Component\Security\Http\Event\LogoutEvent;
24+
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
2425
use Symfony\Component\Security\Http\SecurityEvents;
2526

2627
/**
@@ -44,6 +45,7 @@ class RegisterGlobalSecurityEventListenersPass implements CompilerPassInterface
4445
AuthenticationTokenCreatedEvent::class,
4546
AuthenticationSuccessEvent::class,
4647
InteractiveLoginEvent::class,
48+
TokenDeauthenticatedEvent::class,
4749

4850
// When events are registered by their name
4951
AuthenticationEvents::AUTHENTICATION_SUCCESS,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Bundle\SecurityBundle\RememberMe\DecoratedRememberMeHandler;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
18+
/**
19+
* Replaces the DecoratedRememberMeHandler services with the real definition.
20+
*
21+
* @author Wouter de Jong <[email protected]>
22+
*
23+
* @internal
24+
*/
25+
final class ReplaceDecoratedRememberMeHandlerPass implements CompilerPassInterface
26+
{
27+
private const HANDLER_TAG = 'security.remember_me_handler';
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function process(ContainerBuilder $container): void
33+
{
34+
$handledFirewalls = [];
35+
foreach ($container->findTaggedServiceIds(self::HANDLER_TAG) as $definitionId => $rememberMeHandlerTags) {
36+
$definition = $container->findDefinition($definitionId);
37+
if (DecoratedRememberMeHandler::class !== $definition->getClass()) {
38+
continue;
39+
}
40+
41+
// get the actual custom remember me handler definition (passed to the decorator)
42+
$realRememberMeHandler = $container->findDefinition((string) $definition->getArgument(0));
43+
if (null === $realRememberMeHandler) {
44+
throw new \LogicException(sprintf('Invalid service definition for custom remember me handler; no service found with ID "%s".', (string) $definition->getArgument(0)));
45+
}
46+
47+
foreach ($rememberMeHandlerTags as $rememberMeHandlerTag) {
48+
// some custom handlers may be used on multiple firewalls in the same application
49+
if (\in_array($rememberMeHandlerTag['firewall'], $handledFirewalls, true)) {
50+
continue;
51+
}
52+
53+
$rememberMeHandler = clone $realRememberMeHandler;
54+
$rememberMeHandler->addTag(self::HANDLER_TAG, $rememberMeHandlerTag);
55+
$container->setDefinition('security.authenticator.remember_me_handler.'.$rememberMeHandlerTag['firewall'], $rememberMeHandler);
56+
57+
$handledFirewalls[] = $rememberMeHandlerTag['firewall'];
58+
}
59+
}
60+
}
61+
}

DependencyInjection/Security/Factory/LoginLinkFactory.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,24 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
113113
->replaceArgument(1, $config['lifetime']);
114114
}
115115

116+
$signatureHasherId = 'security.authenticator.login_link_signature_hasher.'.$firewallName;
117+
$container
118+
->setDefinition($signatureHasherId, new ChildDefinition('security.authenticator.abstract_login_link_signature_hasher'))
119+
->replaceArgument(1, $config['signature_properties'])
120+
->replaceArgument(3, $expiredStorageId ? new Reference($expiredStorageId) : null)
121+
->replaceArgument(4, $config['max_uses'] ?? null)
122+
;
123+
116124
$linkerId = 'security.authenticator.login_link_handler.'.$firewallName;
117125
$linkerOptions = [
118126
'route_name' => $config['check_route'],
119127
'lifetime' => $config['lifetime'],
120-
'max_uses' => $config['max_uses'] ?? null,
121128
];
122129
$container
123130
->setDefinition($linkerId, new ChildDefinition('security.authenticator.abstract_login_link_handler'))
124131
->replaceArgument(1, new Reference($userProviderId))
125-
->replaceArgument(3, $config['signature_properties'])
126-
->replaceArgument(5, $linkerOptions)
127-
->replaceArgument(6, $expiredStorageId ? new Reference($expiredStorageId) : null)
132+
->replaceArgument(2, new Reference($signatureHasherId))
133+
->replaceArgument(3, $linkerOptions)
128134
->addTag('security.authenticator.login_linker', ['firewall' => $firewallName])
129135
;
130136

DependencyInjection/Security/Factory/RememberMeFactory.php

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@
1111

1212
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
1313

14+
use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider;
15+
use Symfony\Bundle\SecurityBundle\RememberMe\DecoratedRememberMeHandler;
1416
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
17+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
18+
use Symfony\Component\Config\FileLocator;
1519
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1620
use Symfony\Component\DependencyInjection\ChildDefinition;
1721
use Symfony\Component\DependencyInjection\ContainerBuilder;
1822
use Symfony\Component\DependencyInjection\Definition;
23+
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
1924
use Symfony\Component\DependencyInjection\Reference;
2025
use Symfony\Component\HttpFoundation\Cookie;
2126
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;
@@ -94,31 +99,66 @@ public function create(ContainerBuilder $container, string $id, array $config, ?
9499

95100
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
96101
{
97-
$templateId = $this->generateRememberMeServicesTemplateId($config, $firewallName);
98-
$rememberMeServicesId = $templateId.'.'.$firewallName;
102+
if (!$container->hasDefinition('security.authenticator.remember_me')) {
103+
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../../Resources/config'));
104+
$loader->load('security_authenticator_remember_me.php');
105+
}
106+
107+
// create remember me handler (which manage the remember-me cookies)
108+
$rememberMeHandlerId = 'security.authenticator.remember_me_handler.'.$firewallName;
109+
if (isset($config['service']) && isset($config['token_provider'])) {
110+
throw new InvalidConfigurationException(sprintf('You cannot use both "service" and "token_provider" in "security.firewalls.%s.remember_me".', $firewallName));
111+
}
112+
113+
if (isset($config['service'])) {
114+
$container->register($rememberMeHandlerId, DecoratedRememberMeHandler::class)
115+
->addArgument(new Reference($config['service']))
116+
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
117+
} elseif (isset($config['token_provider'])) {
118+
$tokenProviderId = $this->createTokenProvider($container, $firewallName, $config['token_provider']);
119+
$container->setDefinition($rememberMeHandlerId, new ChildDefinition('security.authenticator.persistent_remember_me_handler'))
120+
->replaceArgument(0, new Reference($tokenProviderId))
121+
->replaceArgument(2, new Reference($userProviderId))
122+
->replaceArgument(4, $config)
123+
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
124+
} else {
125+
$signatureHasherId = 'security.authenticator.remember_me_signature_hasher.'.$firewallName;
126+
$container->setDefinition($signatureHasherId, new ChildDefinition('security.authenticator.remember_me_signature_hasher'))
127+
->replaceArgument(1, $config['signature_properties'])
128+
;
129+
130+
$container->setDefinition($rememberMeHandlerId, new ChildDefinition('security.authenticator.signature_remember_me_handler'))
131+
->replaceArgument(0, new Reference($signatureHasherId))
132+
->replaceArgument(1, new Reference($userProviderId))
133+
->replaceArgument(3, $config)
134+
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
135+
}
99136

100-
// create remember me services (which manage the remember me cookies)
101-
$this->createRememberMeServices($container, $firewallName, $templateId, [new Reference($userProviderId)], $config);
137+
// create check remember me conditions listener (which checks if a remember-me cookie is supported and requested)
138+
$rememberMeConditionsListenerId = 'security.listener.check_remember_me_conditions.'.$firewallName;
139+
$container->setDefinition($rememberMeConditionsListenerId, new ChildDefinition('security.listener.check_remember_me_conditions'))
140+
->replaceArgument(0, array_intersect_key($config, ['always_remember_me' => true, 'remember_me_parameter' => true]))
141+
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName])
142+
;
102143

103144
// create remember me listener (which executes the remember me services for other authenticators and logout)
104-
$this->createRememberMeListener($container, $firewallName, $rememberMeServicesId);
145+
$rememberMeListenerId = 'security.listener.remember_me.'.$firewallName;
146+
$container->setDefinition($rememberMeListenerId, new ChildDefinition('security.listener.remember_me'))
147+
->replaceArgument(0, new Reference($rememberMeHandlerId))
148+
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName])
149+
;
105150

106-
// create remember me authenticator (which re-authenticates the user based on the remember me cookie)
151+
// create remember me authenticator (which re-authenticates the user based on the remember-me cookie)
107152
$authenticatorId = 'security.authenticator.remember_me.'.$firewallName;
108153
$container
109154
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me'))
110-
->replaceArgument(0, new Reference($rememberMeServicesId))
111-
->replaceArgument(3, $container->getDefinition($rememberMeServicesId)->getArgument(3))
155+
->replaceArgument(0, new Reference($rememberMeHandlerId))
156+
->replaceArgument(3, $config['name'] ?? $this->options['name'])
112157
;
113158

114159
foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) {
115160
// register ContextListener
116161
if ('security.context_listener' === substr($serviceId, 0, 25)) {
117-
$container
118-
->getDefinition($serviceId)
119-
->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)])
120-
;
121-
122162
continue;
123163
}
124164

@@ -148,15 +188,33 @@ public function addConfiguration(NodeDefinition $node)
148188
$builder
149189
->scalarNode('secret')->isRequired()->cannotBeEmpty()->end()
150190
->scalarNode('service')->end()
151-
->scalarNode('token_provider')->end()
152191
->arrayNode('user_providers')
153192
->beforeNormalization()
154193
->ifString()->then(function ($v) { return [$v]; })
155194
->end()
156195
->prototype('scalar')->end()
157196
->end()
158197
->booleanNode('catch_exceptions')->defaultTrue()->end()
159-
;
198+
->arrayNode('signature_properties')
199+
->prototype('scalar')->end()
200+
->requiresAtLeastOneElement()
201+
->info('An array of properties on your User that are used to sign the remember-me cookie. If any of these change, all existing cookies will become invalid.')
202+
->example(['email', 'password'])
203+
->end()
204+
->arrayNode('token_provider')
205+
->beforeNormalization()
206+
->ifString()->then(function ($v) { return ['service' => $v]; })
207+
->end()
208+
->children()
209+
->scalarNode('service')->info('The service ID of a custom rememberme token provider.')->end()
210+
->arrayNode('doctrine')
211+
->canBeEnabled()
212+
->children()
213+
->scalarNode('connection')->defaultNull()->end()
214+
->end()
215+
->end()
216+
->end()
217+
->end();
160218

161219
foreach ($this->options as $name => $value) {
162220
if ('secure' === $name) {
@@ -195,9 +253,8 @@ private function createRememberMeServices(ContainerBuilder $container, string $i
195253
$rememberMeServices->replaceArgument(2, $id);
196254

197255
if (isset($config['token_provider'])) {
198-
$rememberMeServices->addMethodCall('setTokenProvider', [
199-
new Reference($config['token_provider']),
200-
]);
256+
$tokenProviderId = $this->createTokenProvider($container, $id, $config['token_provider']);
257+
$rememberMeServices->addMethodCall('setTokenProvider', [new Reference($tokenProviderId)]);
201258
}
202259

203260
// remember-me options
@@ -222,17 +279,29 @@ private function createRememberMeServices(ContainerBuilder $container, string $i
222279
$rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders)));
223280
}
224281

225-
private function createRememberMeListener(ContainerBuilder $container, string $id, string $rememberMeServicesId): void
282+
private function createTokenProvider(ContainerBuilder $container, string $firewallName, array $config): string
226283
{
227-
$container
228-
->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me'))
229-
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id])
230-
->replaceArgument(0, new Reference($rememberMeServicesId))
231-
;
284+
$tokenProviderId = $config['service'] ?? false;
285+
if ($config['doctrine']['enabled'] ?? false) {
286+
if (!class_exists(DoctrineTokenProvider::class)) {
287+
throw new InvalidConfigurationException('Cannot use the "doctrine" token provider for "remember_me" because the Doctrine Bridge is not installed. Try running "composer require symfony/doctrine-bridge".');
288+
}
232289

233-
$container
234-
->setDefinition('security.logout.listener.remember_me.'.$id, new Definition(RememberMeLogoutListener::class))
235-
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id])
236-
->addArgument(new Reference($rememberMeServicesId));
290+
if (null === $config['doctrine']['connection']) {
291+
$connectionId = 'database_connection';
292+
} else {
293+
$connectionId = 'doctrine.dbal.'.$config['doctrine']['connection'].'_connection';
294+
}
295+
296+
$tokenProviderId = 'security.remember_me.doctrine_token_provider.'.$firewallName;
297+
$container->register($tokenProviderId, DoctrineTokenProvider::class)
298+
->addArgument(new Reference($connectionId));
299+
}
300+
301+
if (!$tokenProviderId) {
302+
throw new InvalidConfigurationException(sprintf('No token provider was set for firewall "%s". Either configure a service ID or set "remember_me.token_provider.doctrine" to true.', $firewallName));
303+
}
304+
305+
return $tokenProviderId;
237306
}
238307
}

DependencyInjection/SecurityExtension.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Symfony\Component\EventDispatcher\EventDispatcher;
3535
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
3636
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
37+
use Symfony\Component\HttpKernel\KernelEvents;
3738
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
3839
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
3940
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
@@ -392,7 +393,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
392393
// Context serializer listener
393394
if (false === $firewall['stateless']) {
394395
$contextKey = $firewall['context'] ?? $id;
395-
$listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey));
396+
$listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey, $this->authenticatorManagerEnabled ? $firewallEventDispatcherId : null));
396397
$sessionStrategyId = 'security.authentication.session_strategy';
397398

398399
if ($this->authenticatorManagerEnabled) {
@@ -557,7 +558,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
557558
return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null];
558559
}
559560

560-
private function createContextListener(ContainerBuilder $container, string $contextKey)
561+
private function createContextListener(ContainerBuilder $container, string $contextKey, ?string $firewallEventDispatcherId)
561562
{
562563
if (isset($this->contextListeners[$contextKey])) {
563564
return $this->contextListeners[$contextKey];
@@ -566,6 +567,10 @@ private function createContextListener(ContainerBuilder $container, string $cont
566567
$listenerId = 'security.context_listener.'.\count($this->contextListeners);
567568
$listener = $container->setDefinition($listenerId, new ChildDefinition('security.context_listener'));
568569
$listener->replaceArgument(2, $contextKey);
570+
if (null !== $firewallEventDispatcherId) {
571+
$listener->replaceArgument(4, new Reference($firewallEventDispatcherId));
572+
$listener->addTag('kernel.event_listener', ['event' => KernelEvents::RESPONSE, 'method' => 'onKernelResponse']);
573+
}
569574

570575
return $this->contextListeners[$contextKey] = $listenerId;
571576
}

0 commit comments

Comments
 (0)