diff --git a/src/bundle/DependencyInjection/Factory/Security/TwoFactorFactory.php b/src/bundle/DependencyInjection/Factory/Security/TwoFactorFactory.php index 62d229b4..8ec3c7e0 100644 --- a/src/bundle/DependencyInjection/Factory/Security/TwoFactorFactory.php +++ b/src/bundle/DependencyInjection/Factory/Security/TwoFactorFactory.php @@ -136,7 +136,8 @@ private function createAuthenticatorService( ->replaceArgument(0, new Reference($twoFactorFirewallConfigId)) ->replaceArgument(2, new Reference($successHandlerId)) ->replaceArgument(3, new Reference($failureHandlerId)) - ->replaceArgument(4, new Reference($authRequiredHandlerId)); + ->replaceArgument(4, new Reference($authRequiredHandlerId)) + ->addTag('kernel.reset', ['method' => 'reset']); return $authenticatorId; } diff --git a/src/bundle/DependencyInjection/Factory/Security/TwoFactorServicesFactory.php b/src/bundle/DependencyInjection/Factory/Security/TwoFactorServicesFactory.php index 9344f306..bedf61c2 100644 --- a/src/bundle/DependencyInjection/Factory/Security/TwoFactorServicesFactory.php +++ b/src/bundle/DependencyInjection/Factory/Security/TwoFactorServicesFactory.php @@ -107,7 +107,8 @@ public function createProviderPreparationListener(ContainerBuilder $container, s ->replaceArgument(3, $firewallName) ->replaceArgument(4, $config['prepare_on_login'] ?? TwoFactorFactory::DEFAULT_PREPARE_ON_LOGIN) ->replaceArgument(5, $config['prepare_on_access_denied'] ?? TwoFactorFactory::DEFAULT_PREPARE_ON_ACCESS_DENIED) - ->addTag('kernel.event_subscriber'); + ->addTag('kernel.event_subscriber') + ->addTag('kernel.reset', ['method' => 'reset']); } public function createKernelExceptionListener(ContainerBuilder $container, string $firewallName, string $authRequiredHandlerId): void diff --git a/src/bundle/Resources/config/security.php b/src/bundle/Resources/config/security.php index 4728c36a..d5a25d54 100644 --- a/src/bundle/Resources/config/security.php +++ b/src/bundle/Resources/config/security.php @@ -30,6 +30,7 @@ $container->services() ->set('scheb_two_factor.security.authenticator', TwoFactorAuthenticator::class) ->tag('monolog.logger', ['channel' => 'security']) + ->tag('kernel.reset', ['method' => 'reset']) ->args([ abstract_arg('Two-factor firewall config'), service('security.token_storage'), diff --git a/tests/DependencyInjection/ResettableServicesAreTaggedTest.php b/tests/DependencyInjection/ResettableServicesAreTaggedTest.php new file mode 100644 index 00000000..8520f8cf --- /dev/null +++ b/tests/DependencyInjection/ResettableServicesAreTaggedTest.php @@ -0,0 +1,120 @@ +compileContainerWithTwoFactorFirewall(); + + $untagged = []; + foreach ($container->getDefinitions() as $id => $definition) { + if (!$this->isResettable($definition)) { + continue; + } + + if ($definition->hasTag(self::RESET_TAG)) { + continue; + } + + $untagged[] = $id; + } + + $this->assertSame([], $untagged, sprintf( + 'These services implement %s but carry no "%s" tag, so their state survives into the next request under a persistent worker: %s', + ResetInterface::class, + self::RESET_TAG, + implode(', ', $untagged), + )); + } + + /** + * The control for the arm above: without at least one resettable definition to look at, an empty + * `$untagged` would pass against any container at all. + */ + #[Test] + public function createAuthenticator_resettableServiceDefinitions_areActuallyPresent(): void + { + $container = $this->compileContainerWithTwoFactorFirewall(); + + $resettable = []; + foreach ($container->getDefinitions() as $id => $definition) { + if (!$this->isResettable($definition)) { + continue; + } + + $resettable[] = $id; + } + + $this->assertNotEmpty($resettable, 'No resettable service was found, so the assertion above examined nothing'); + } + + private function isResettable(Definition $definition): bool + { + $class = $definition->getClass(); + + if (null === $class || !class_exists($class)) { + return false; + } + + return (new ReflectionClass($class))->implementsInterface(ResetInterface::class); + } + + private function compileContainerWithTwoFactorFirewall(): ContainerBuilder + { + $container = new ContainerBuilder(); + + // Stub services the extension's configuration refers to. + $container->setDefinition('acme_test.persister', new Definition()); + $container->setDefinition('acme_test.mailer', new Definition()); + + $extension = new SchebTwoFactorExtension(); + $extension->load([[]], $container); + + $factory = new TwoFactorFactory(new TwoFactorServicesFactory()); + $factory->createAuthenticator($container, self::FIREWALL_NAME, [], self::USER_PROVIDER); + $factory->createListeners($container, self::FIREWALL_NAME, []); + + // Resolves every ChildDefinition against its parent, which is what gives the per-firewall + // definitions their class — and what drops any tag the child did not re-add. + (new ResolveChildDefinitionsPass())->process($container); + + return $container; + } +}