From f38a2052e188de00204eaf495b7eadd70a9ab894 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jun 2025 16:08:14 +0200 Subject: [PATCH 1/6] Allow Symfony ^8.0 --- composer.json | 56 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index 7459b017..66bc512f 100644 --- a/composer.json +++ b/composer.json @@ -19,37 +19,37 @@ "php": ">=8.2", "composer-runtime-api": ">=2.1", "ext-xml": "*", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^7.3", - "symfony/dependency-injection": "^6.4.11|^7.1.4", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/password-hasher": "^6.4|^7.0", - "symfony/security-core": "^7.3", - "symfony/security-csrf": "^6.4|^7.0", - "symfony/security-http": "^7.3", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^7.3|^8.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/password-hasher": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.3|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^7.3|^8.0", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { - "symfony/asset": "^6.4|^7.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/ldap": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", - "symfony/twig-bundle": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/ldap": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", "twig/twig": "^3.12", "web-token/jwt-library": "^3.3.2|^4.0" }, From a0f1ef99c038e560f4c96d9f1b81398153930bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Wed, 7 May 2025 12:05:16 +0200 Subject: [PATCH 2/6] [SecurityBundle] register alias for argument for password hasher --- CHANGELOG.md | 21 +++++++++++++++++++ DependencyInjection/SecurityExtension.php | 12 +++++++++++ .../SecurityExtensionTest.php | 15 ++++++++++--- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77aa9573..5d28b7dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ CHANGELOG ========= +7.4 +--- + + * Register alias for argument for password hasher when its key is not a class name: + + With the following configuration: + ```yaml + security: + password_hashers: + recovery_code: auto + ``` + + It is possible to inject the `recovery_code` password hasher in a service: + + ```php + public function __construct( + #[Target('recovery_code')] + private readonly PasswordHasherInterface $passwordHasher, + ) { + } + ``` 7.3 --- diff --git a/DependencyInjection/SecurityExtension.php b/DependencyInjection/SecurityExtension.php index 1711964b..0d41e3db 100644 --- a/DependencyInjection/SecurityExtension.php +++ b/DependencyInjection/SecurityExtension.php @@ -49,6 +49,7 @@ use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; use Symfony\Component\Routing\Loader\ContainerLoader; use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy; use Symfony\Component\Security\Core\Authorization\Strategy\ConsensusStrategy; @@ -706,6 +707,17 @@ private function createHashers(array $hashers, ContainerBuilder $container): voi $hasherMap = []; foreach ($hashers as $class => $hasher) { $hasherMap[$class] = $this->createHasher($hasher); + // The key is not a class, so we register an alias for argument to + // ease getting the hasher + if (!class_exists($class) && !interface_exists($class)) { + $id = 'security.password_hasher.'.$class; + $container + ->register($id, PasswordHasherInterface::class) + ->setFactory([new Reference('security.password_hasher_factory'), 'getPasswordHasher']) + ->setArgument(0, $class) + ; + $container->registerAliasForArgument($id, PasswordHasherInterface::class, $class); + } } $container diff --git a/Tests/DependencyInjection/SecurityExtensionTest.php b/Tests/DependencyInjection/SecurityExtensionTest.php index d0f3549a..4999fff7 100644 --- a/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/Tests/DependencyInjection/SecurityExtensionTest.php @@ -29,6 +29,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\PasswordHasher\PasswordHasherInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\InMemoryUserChecker; @@ -883,7 +884,7 @@ public function testCustomHasherWithMigrateFrom() $container->loadFromExtension('security', [ 'password_hashers' => [ 'legacy' => 'md5', - 'App\User' => [ + TestUserChecker::class => [ 'id' => 'App\Security\CustomHasher', 'migrate_from' => 'legacy', ], @@ -895,11 +896,19 @@ public function testCustomHasherWithMigrateFrom() $hashersMap = $container->getDefinition('security.password_hasher_factory')->getArgument(0); - $this->assertArrayHasKey('App\User', $hashersMap); - $this->assertEquals($hashersMap['App\User'], [ + $this->assertArrayHasKey(TestUserChecker::class, $hashersMap); + $this->assertEquals($hashersMap[TestUserChecker::class], [ 'instance' => new Reference('App\Security\CustomHasher'), 'migrate_from' => ['legacy'], ]); + + $legacyAlias = \sprintf('%s $%s', PasswordHasherInterface::class, 'legacy'); + $this->assertTrue($container->hasAlias($legacyAlias)); + $definition = $container->getDefinition((string) $container->getAlias($legacyAlias)); + $this->assertSame(PasswordHasherInterface::class, $definition->getClass()); + + $this->assertFalse($container->hasAlias(\sprintf('%s $%s', PasswordHasherInterface::class, 'symfonyBundleSecurityBundleTestsDependencyInjectionTestUserChecker'))); + $this->assertFalse($container->hasAlias(\sprintf('.%s $%s', PasswordHasherInterface::class, TestUserChecker::class))); } public function testAuthenticatorsDecoration() From 3bd98a4d7b01ea68bd5e388879e0f7204324db74 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Wed, 11 Jun 2025 18:53:52 +0200 Subject: [PATCH 3/6] fix missing newline --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d28b7dc..1d69d188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ CHANGELOG ) { } ``` + 7.3 --- From 6865d291ace6fd2d703443265e4e23ecc79a590e Mon Sep 17 00:00:00 2001 From: matlec Date: Mon, 2 Jun 2025 09:09:50 +0200 Subject: [PATCH 4/6] [Security] Deprecate callable firewall listeners --- CHANGELOG.md | 1 + Debug/TraceableFirewallListener.php | 7 +++- Security/FirewallContext.php | 3 +- Security/LazyFirewallContext.php | 34 ++++++++++++++++--- .../SecurityDataCollectorTest.php | 20 ++++++++--- Tests/Debug/TraceableFirewallListenerTest.php | 18 ++++++++-- composer.json | 1 + 7 files changed, 70 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d69d188..73754edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ CHANGELOG ) { } ``` + * Deprecate `LazyFirewallContext::__invoke()` 7.3 --- diff --git a/Debug/TraceableFirewallListener.php b/Debug/TraceableFirewallListener.php index 45f4f498..f3a8ca22 100644 --- a/Debug/TraceableFirewallListener.php +++ b/Debug/TraceableFirewallListener.php @@ -16,6 +16,7 @@ use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; +use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; use Symfony\Contracts\Service\ResetInterface; @@ -88,7 +89,11 @@ protected function callListeners(RequestEvent $event, iterable $listeners): void } foreach ($requestListeners as $listener) { - $listener($event); + if (!$listener instanceof FirewallListenerInterface) { + $listener($event); + } elseif (false !== $listener->supports($event->getRequest())) { + $listener->authenticate($event); + } if ($event->hasResponse()) { break; diff --git a/Security/FirewallContext.php b/Security/FirewallContext.php index 63648bd6..1da89139 100644 --- a/Security/FirewallContext.php +++ b/Security/FirewallContext.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; use Symfony\Component\Security\Http\Firewall\LogoutListener; /** @@ -39,7 +40,7 @@ public function getConfig(): ?FirewallConfig } /** - * @return iterable + * @return iterable */ public function getListeners(): iterable { diff --git a/Security/LazyFirewallContext.php b/Security/LazyFirewallContext.php index 68357623..09526fde 100644 --- a/Security/LazyFirewallContext.php +++ b/Security/LazyFirewallContext.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Http\Event\LazyResponseEvent; +use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\Firewall\ExceptionListener; use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; use Symfony\Component\Security\Http\Firewall\LogoutListener; @@ -23,7 +25,7 @@ * * @author Nicolas Grekas */ -class LazyFirewallContext extends FirewallContext +class LazyFirewallContext extends FirewallContext implements FirewallListenerInterface { public function __construct( iterable $listeners, @@ -40,19 +42,26 @@ public function getListeners(): iterable return [$this]; } - public function __invoke(RequestEvent $event): void + public function supports(Request $request): ?bool + { + return true; + } + + public function authenticate(RequestEvent $event): void { $listeners = []; $request = $event->getRequest(); $lazy = $request->isMethodCacheable(); foreach (parent::getListeners() as $listener) { - if (!$lazy || !$listener instanceof FirewallListenerInterface) { + if (!$listener instanceof FirewallListenerInterface) { + trigger_deprecation('symfony/security-http', '7.4', 'Using a callable as firewall listener is deprecated, extend "%s" or implement "%s" instead.', AbstractListener::class, FirewallListenerInterface::class); + $listeners[] = $listener; - $lazy = $lazy && $listener instanceof FirewallListenerInterface; + $lazy = false; } elseif (false !== $supports = $listener->supports($request)) { $listeners[] = [$listener, 'authenticate']; - $lazy = null === $supports; + $lazy = $lazy && null === $supports; } } @@ -75,4 +84,19 @@ public function __invoke(RequestEvent $event): void } }); } + + public static function getPriority(): int + { + return 0; + } + + /** + * @deprecated since Symfony 7.4, to be removed in 8.0 + */ + public function __invoke(RequestEvent $event): void + { + trigger_deprecation('symfony/security-bundle', '7.4', 'The "%s()" method is deprecated since Symfony 7.4 and will be removed in 8.0.', __METHOD__); + + $this->authenticate($event); + } } diff --git a/Tests/DataCollector/SecurityDataCollectorTest.php b/Tests/DataCollector/SecurityDataCollectorTest.php index 5528c9b7..053bf25f 100644 --- a/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/Tests/DataCollector/SecurityDataCollectorTest.php @@ -32,6 +32,8 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Http\Firewall\AbstractListener; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; use Symfony\Component\VarDumper\Caster\ClassStub; @@ -193,8 +195,18 @@ public function testGetListeners() $request = new Request(); $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $event->setResponse($response = new Response()); - $listener = function ($e) use ($event, &$listenerCalled) { - $listenerCalled += $e === $event; + $listener = new class extends AbstractListener { + public int $callCount = 0; + + public function supports(Request $request): ?bool + { + return true; + } + + public function authenticate(RequestEvent $event): void + { + ++$this->callCount; + } }; $firewallMap = $this ->getMockBuilder(FirewallMap::class) @@ -217,9 +229,9 @@ public function testGetListeners() $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall, true); $collector->collect($request, $response); - $this->assertNotEmpty($collected = $collector->getListeners()[0]); + $this->assertCount(1, $collector->getListeners()); $collector->lateCollect(); - $this->assertSame(1, $listenerCalled); + $this->assertSame(1, $listener->callCount); } public function testCollectCollectsDecisionLogWhenStrategyIsAffirmative() diff --git a/Tests/Debug/TraceableFirewallListenerTest.php b/Tests/Debug/TraceableFirewallListenerTest.php index 4ab483a2..db6e8a0e 100644 --- a/Tests/Debug/TraceableFirewallListenerTest.php +++ b/Tests/Debug/TraceableFirewallListenerTest.php @@ -29,7 +29,9 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; /** @@ -41,9 +43,19 @@ public function testOnKernelRequestRecordsListeners() { $request = new Request(); $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); - $event->setResponse($response = new Response()); - $listener = function ($e) use ($event, &$listenerCalled) { - $listenerCalled += $e === $event; + $event->setResponse(new Response()); + $listener = new class extends AbstractListener { + public int $callCount = 0; + + public function supports(Request $request): ?bool + { + return true; + } + + public function authenticate(RequestEvent $event): void + { + ++$this->callCount; + } }; $firewallMap = $this->createMock(FirewallMap::class); $firewallMap diff --git a/composer.json b/composer.json index 66bc512f..cbad87a6 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "symfony/clock": "^6.4|^7.0|^8.0", "symfony/config": "^7.3|^8.0", "symfony/dependency-injection": "^6.4.11|^7.1.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher": "^6.4|^7.0|^8.0", "symfony/http-kernel": "^6.4|^7.0|^8.0", "symfony/http-foundation": "^6.4|^7.0|^8.0", From db1ad02dd7eb3ab3107cf7ec23d5a1b2a02ec8c7 Mon Sep 17 00:00:00 2001 From: Valmonzo Date: Mon, 23 Jun 2025 12:02:09 +0200 Subject: [PATCH 5/6] [FrameworkBundle] Allow using their name without added suffix when using #[Target] for custom services --- .../Security/Factory/LoginThrottlingFactory.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/Security/Factory/LoginThrottlingFactory.php b/DependencyInjection/Security/Factory/LoginThrottlingFactory.php index 93818f5a..eb2371fd 100644 --- a/DependencyInjection/Security/Factory/LoginThrottlingFactory.php +++ b/DependencyInjection/Security/Factory/LoginThrottlingFactory.php @@ -21,6 +21,7 @@ use Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface; use Symfony\Component\Lock\LockInterface; use Symfony\Component\RateLimiter\RateLimiterFactory; +use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; use Symfony\Component\RateLimiter\Storage\CacheStorage; use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter; @@ -115,6 +116,12 @@ private function registerRateLimiter(ContainerBuilder $container, string $name, $limiterConfig['id'] = $name; $limiter->replaceArgument(0, $limiterConfig); - $container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter'); + $factoryAlias = $container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter'); + + if (interface_exists(RateLimiterFactoryInterface::class)) { + $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter'); + $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name); + $factoryAlias->setDeprecated('symfony/security-bundle', '7.4', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.'); + } } } From 7dd07104d795cdf866013e901cac5b6912b29bf7 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 24 Jun 2025 22:41:24 +0200 Subject: [PATCH 6/6] also deprecate the internal rate limiter factory alias --- .../Security/Factory/LoginThrottlingFactory.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/Security/Factory/LoginThrottlingFactory.php b/DependencyInjection/Security/Factory/LoginThrottlingFactory.php index eb2371fd..946abdfd 100644 --- a/DependencyInjection/Security/Factory/LoginThrottlingFactory.php +++ b/DependencyInjection/Security/Factory/LoginThrottlingFactory.php @@ -13,6 +13,7 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\LogicException; @@ -121,7 +122,13 @@ private function registerRateLimiter(ContainerBuilder $container, string $name, if (interface_exists(RateLimiterFactoryInterface::class)) { $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter'); $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name); - $factoryAlias->setDeprecated('symfony/security-bundle', '7.4', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.'); + $factoryAlias->setDeprecated('symfony/security-bundle', '7.4', \sprintf('The "%%alias_id%%" autowiring alias is deprecated and will be removed in 8.0, use "%s $%s" instead.', RateLimiterFactoryInterface::class, (new Target($name.'.limiter'))->getParsedName())); + + $internalAliasId = \sprintf('.%s $%s.limiter', RateLimiterFactory::class, $name); + + if ($container->hasAlias($internalAliasId)) { + $container->getAlias($internalAliasId)->setDeprecated('symfony/security-bundle', '7.4', \sprintf('The "%%alias_id%%" autowiring alias is deprecated and will be removed in 8.0, use "%s $%s" instead.', RateLimiterFactoryInterface::class, (new Target($name.'.limiter'))->getParsedName())); + } } } }