* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; use Symfony\Bridge\Twig\Extension\LogoutUrlExtension; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\StatelessAuthenticatorFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Form\Extension\PasswordHasher\PasswordHasherExtension; use Symfony\Component\HttpFoundation\ChainRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\AttributesRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\IpsRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\PortRequestMatcher; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; use Symfony\Component\Routing\Loader\ContainerLoader; use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy; use Symfony\Component\Security\Core\Authorization\Strategy\ConsensusStrategy; use Symfony\Component\Security\Core\Authorization\Strategy\PriorityStrategy; use Symfony\Component\Security\Core\Authorization\Strategy\UnanimousStrategy; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\User\ChainUserChecker; use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; use Symfony\Component\Security\Http\Event\CheckPassportEvent; use Symfony\Flex\Command\InstallRecipesCommand; /** * SecurityExtension. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class SecurityExtension extends Extension implements PrependExtensionInterface { private array $requestMatchers = []; private array $expressions = []; private array $contextListeners = []; /** @var list */ private array $factories = []; /** @var AuthenticatorFactoryInterface[] */ private array $sortedFactories = []; private array $userProviderFactories = []; public function prepend(ContainerBuilder $container): void { foreach ($this->getSortedFactories() as $factory) { if ($factory instanceof PrependExtensionInterface) { $factory->prepend($container); } } } public function load(array $configs, ContainerBuilder $container): void { if (!array_filter($configs)) { $hint = class_exists(InstallRecipesCommand::class) ? 'Try running "composer symfony:recipes:install symfony/security-bundle".' : 'Please define your settings for the "security" config section.'; throw new InvalidConfigurationException('The SecurityBundle is enabled but is not configured. '.$hint); } $mainConfig = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($mainConfig, $configs); // load services $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); $loader->load('security.php'); $loader->load('password_hasher.php'); $loader->load('security_listeners.php'); $loader->load('security_authenticator.php'); $loader->load('security_authenticator_access_token.php'); if ($container::willBeAvailable('symfony/twig-bridge', LogoutUrlExtension::class, ['symfony/security-bundle'])) { $loader->load('templating_twig.php'); } $loader->load('collectors.php'); if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) { $loader->load('security_debug.php'); } if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'])) { $container->removeDefinition('security.expression_language'); $container->removeDefinition('security.access.expression_voter'); $container->removeDefinition('security.is_granted_attribute_expression_language'); $container->removeDefinition('security.is_csrf_token_valid_attribute_expression_language'); } if (!class_exists(PasswordHasherExtension::class)) { $container->removeDefinition('form.listener.password_hasher'); $container->removeDefinition('form.type_extension.form.password_hasher'); $container->removeDefinition('form.type_extension.password.password_hasher'); } // set some global scalars $container->setParameter('security.access.denied_url', $config['access_denied_url']); $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']); $container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']); if (isset($config['access_decision_manager']['service'])) { $container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service']); } elseif (isset($config['access_decision_manager']['strategy_service'])) { $container ->getDefinition('security.access.decision_manager') ->addArgument(new Reference($config['access_decision_manager']['strategy_service'])); } else { $container ->getDefinition('security.access.decision_manager') ->addArgument($this->createStrategyDefinition( $config['access_decision_manager']['strategy'] ?? MainConfiguration::STRATEGY_AFFIRMATIVE, $config['access_decision_manager']['allow_if_all_abstain'], $config['access_decision_manager']['allow_if_equal_granted_denied'] )); } $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); if (class_exists(Application::class)) { $loader->load('debug_console.php'); } $this->createFirewalls($config, $container); if ($container::willBeAvailable('symfony/routing', ContainerLoader::class, ['symfony/security-bundle'])) { $this->createLogoutUrisParameter($config['firewalls'] ?? [], $container); } else { $container->removeDefinition('security.route_loader.logout'); } $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); if ($config['password_hashers']) { $this->createHashers($config['password_hashers'], $container); } if (class_exists(Application::class)) { $loader->load('console.php'); $container->getDefinition('security.command.user_password_hash')->replaceArgument(1, array_keys($config['password_hashers'])); } $container->registerForAutoconfiguration(VoterInterface::class) ->addTag('security.voter'); } private function createStrategyDefinition(string $strategy, bool $allowIfAllAbstainDecisions, bool $allowIfEqualGrantedDeniedDecisions): Definition { return match ($strategy) { MainConfiguration::STRATEGY_AFFIRMATIVE => new Definition(AffirmativeStrategy::class, [$allowIfAllAbstainDecisions]), MainConfiguration::STRATEGY_CONSENSUS => new Definition(ConsensusStrategy::class, [$allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions]), MainConfiguration::STRATEGY_UNANIMOUS => new Definition(UnanimousStrategy::class, [$allowIfAllAbstainDecisions]), MainConfiguration::STRATEGY_PRIORITY => new Definition(PriorityStrategy::class, [$allowIfAllAbstainDecisions]), default => throw new InvalidConfigurationException(sprintf('The strategy "%s" is not supported.', $strategy)), }; } private function createRoleHierarchy(array $config, ContainerBuilder $container): void { if (!isset($config['role_hierarchy']) || 0 === \count($config['role_hierarchy'])) { $container->removeDefinition('security.access.role_hierarchy_voter'); return; } $container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']); $container->removeDefinition('security.access.simple_role_voter'); } private function createAuthorization(array $config, ContainerBuilder $container): void { foreach ($config['access_control'] as $access) { if (isset($access['request_matcher'])) { if ($access['path'] || $access['host'] || $access['port'] || $access['ips'] || $access['methods'] || $access['attributes'] || $access['route']) { throw new InvalidConfigurationException('The "request_matcher" option should not be specified alongside other options. Consider integrating your constraints inside your RequestMatcher directly.'); } $matcher = new Reference($access['request_matcher']); } else { $attributes = $access['attributes']; if ($access['route']) { if (\array_key_exists('_route', $attributes)) { throw new InvalidConfigurationException('The "route" option should not be specified alongside "attributes._route" option. Use just one of the options.'); } $attributes['_route'] = $access['route']; } $matcher = $this->createRequestMatcher( $container, $access['path'], $access['host'], $access['port'], $access['methods'], $access['ips'], $attributes ); } $roles = $access['roles']; if ($access['allow_if']) { $roles[] = $this->createExpression($container, $access['allow_if']); } $emptyAccess = 0 === \count(array_filter($access)); if ($emptyAccess) { throw new InvalidConfigurationException('One or more access control items are empty. Did you accidentally add lines only containing a "-" under "security.access_control"?'); } $container->getDefinition('security.access_map') ->addMethodCall('add', [$matcher, $roles, $access['requires_channel']]); } // allow cache warm-up for expressions if (\count($this->expressions)) { $container->getDefinition('security.cache_warmer.expression') ->replaceArgument(0, new IteratorArgument(array_values($this->expressions))); } else { $container->removeDefinition('security.cache_warmer.expression'); } } private function createFirewalls(array $config, ContainerBuilder $container): void { if (!isset($config['firewalls'])) { return; } $firewalls = $config['firewalls']; $providerIds = $this->createUserProviders($config, $container); $container->setParameter('security.firewalls', array_keys($firewalls)); // make the ContextListener aware of the configured user providers $contextListenerDefinition = $container->getDefinition('security.context_listener'); $arguments = $contextListenerDefinition->getArguments(); $userProviders = []; foreach ($providerIds as $userProviderId) { $userProviders[] = new Reference($userProviderId); } $arguments[1] = $userProviderIteratorsArgument = new IteratorArgument($userProviders); $contextListenerDefinition->setArguments($arguments); $nbUserProviders = \count($userProviders); if ($nbUserProviders > 1) { $container->setDefinition('security.user_providers', new Definition(ChainUserProvider::class, [$userProviderIteratorsArgument])); } elseif (0 === $nbUserProviders) { $container->removeDefinition('security.listener.user_provider'); } else { $container->setAlias('security.user_providers', new Alias(current($providerIds))); } if (1 === \count($providerIds)) { $container->setAlias(UserProviderInterface::class, current($providerIds)); } $customUserChecker = false; // load firewall map $mapDef = $container->getDefinition('security.firewall.map'); $map = $authenticationProviders = $contextRefs = $authenticators = []; foreach ($firewalls as $name => $firewall) { if (isset($firewall['user_checker']) && 'security.user_checker' !== $firewall['user_checker']) { $customUserChecker = true; } $configId = 'security.firewall.map.config.'.$name; [$matcher, $listeners, $exceptionListener, $logoutListener, $firewallAuthenticators] = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId); if (!$firewallAuthenticators) { $authenticators[$name] = null; } else { $firewallAuthenticatorRefs = []; foreach ($firewallAuthenticators as $authenticatorId) { $firewallAuthenticatorRefs[$authenticatorId] = new Reference($authenticatorId); } $authenticators[$name] = ServiceLocatorTagPass::register($container, $firewallAuthenticatorRefs); } $contextId = 'security.firewall.map.context.'.$name; $isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']); $context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context'); $context = $container->setDefinition($contextId, $context); $context ->replaceArgument(0, new IteratorArgument($listeners)) ->replaceArgument(1, $exceptionListener) ->replaceArgument(2, $logoutListener) ->replaceArgument(3, new Reference($configId)) ; $contextRefs[$contextId] = new Reference($contextId); $map[$contextId] = $matcher; } $container ->getDefinition('security.helper') ->replaceArgument(1, $authenticators) ; $container->setAlias('security.firewall.context_locator', (string) ServiceLocatorTagPass::register($container, $contextRefs)); $mapDef->replaceArgument(0, new Reference('security.firewall.context_locator')); $mapDef->replaceArgument(1, new IteratorArgument($map)); // register an autowire alias for the UserCheckerInterface if no custom user checker service is configured if (!$customUserChecker) { $container->setAlias(UserCheckerInterface::class, new Alias('security.user_checker', false)); } } private function createFirewall(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, array $providerIds, string $configId): array { $config = $container->setDefinition($configId, new ChildDefinition('security.firewall.config')); $config->replaceArgument(0, $id); $config->replaceArgument(1, $firewall['user_checker']); // Matcher $matcher = null; if (isset($firewall['request_matcher'])) { $matcher = new Reference($firewall['request_matcher']); } elseif (isset($firewall['pattern']) || isset($firewall['host'])) { $pattern = $firewall['pattern'] ?? null; $host = $firewall['host'] ?? null; $methods = $firewall['methods'] ?? []; $matcher = $this->createRequestMatcher($container, $pattern, $host, null, $methods); } $config->replaceArgument(2, $matcher ? (string) $matcher : null); $config->replaceArgument(3, $firewall['security']); // Security disabled? if (false === $firewall['security']) { return [$matcher, [], null, null, []]; } $config->replaceArgument(4, $firewall['stateless']); $firewallEventDispatcherId = 'security.event_dispatcher.'.$id; // Provider id (must be configured explicitly per firewall/authenticator if more than one provider is set) $defaultProvider = null; if (isset($firewall['provider'])) { if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall['provider'])])) { throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall['provider'])); } $defaultProvider = $providerIds[$normalizedName]; $container->setDefinition('security.listener.'.$id.'.user_provider', new ChildDefinition('security.listener.user_provider.abstract')) ->addTag('kernel.event_listener', ['dispatcher' => $firewallEventDispatcherId, 'event' => CheckPassportEvent::class, 'priority' => 2048, 'method' => 'checkPassport']) ->replaceArgument(0, new Reference($defaultProvider)); } elseif (1 === \count($providerIds)) { $defaultProvider = reset($providerIds); } $config->replaceArgument(5, $defaultProvider); // Register Firewall-specific event dispatcher $container->register($firewallEventDispatcherId, EventDispatcher::class) ->addTag('event_dispatcher.dispatcher', ['name' => $firewallEventDispatcherId]); $eventDispatcherLocator = $container->getDefinition('security.firewall.event_dispatcher_locator'); $eventDispatcherLocator ->replaceArgument(0, array_merge($eventDispatcherLocator->getArgument(0), [ $id => new ServiceClosureArgument(new Reference($firewallEventDispatcherId)), ])) ; // Register Firewall-specific chained user checker $container->register('security.user_checker.chain.'.$id, ChainUserChecker::class) ->addArgument(new TaggedIteratorArgument('security.user_checker.'.$id)); // Register listeners $listeners = []; $listenerKeys = []; // Channel listener $listeners[] = new Reference('security.channel_listener'); $contextKey = null; // Context serializer listener if (false === $firewall['stateless']) { $contextKey = $firewall['context'] ?? $id; $listeners[] = new Reference($this->createContextListener($container, $contextKey, $firewallEventDispatcherId)); $sessionStrategyId = 'security.authentication.session_strategy'; $container ->setDefinition('security.listener.session.'.$id, new ChildDefinition('security.listener.session')) ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } else { $sessionStrategyId = 'security.authentication.session_strategy_noop'; } $container->setAlias(new Alias('security.authentication.session_strategy.'.$id, false), $sessionStrategyId); $config->replaceArgument(6, $contextKey); // Logout listener $logoutListenerId = null; if (isset($firewall['logout'])) { $logoutListenerId = 'security.logout_listener.'.$id; $logoutListener = $container->setDefinition($logoutListenerId, new ChildDefinition('security.logout_listener')); $logoutListener->replaceArgument(2, new Reference($firewallEventDispatcherId)); $logoutListener->replaceArgument(3, [ 'csrf_parameter' => $firewall['logout']['csrf_parameter'], 'csrf_token_id' => $firewall['logout']['csrf_token_id'], 'logout_path' => $firewall['logout']['path'], ]); $container->setDefinition('security.logout.listener.default.'.$id, new ChildDefinition('security.logout.listener.default')) ->replaceArgument(1, $firewall['logout']['target']) ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); // add CSRF provider if ($firewall['logout']['enable_csrf']) { $logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_manager'])); } // add session logout listener if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) { $container->setDefinition('security.logout.listener.session.'.$id, new ChildDefinition('security.logout.listener.session')) ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } // add cookie logout listener if (\count($firewall['logout']['delete_cookies']) > 0) { $container->setDefinition('security.logout.listener.cookie_clearing.'.$id, new ChildDefinition('security.logout.listener.cookie_clearing')) ->addArgument($firewall['logout']['delete_cookies']) ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } // add clear site data listener if ($firewall['logout']['clear_site_data'] ?? false) { $container->setDefinition('security.logout.listener.clear_site_data.'.$id, new ChildDefinition('security.logout.listener.clear_site_data')) ->addArgument($firewall['logout']['clear_site_data']) ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } // register with LogoutUrlGenerator $container ->getDefinition('security.logout_url_generator') ->addMethodCall('registerListener', [ $id, $firewall['logout']['path'], $firewall['logout']['csrf_token_id'], $firewall['logout']['csrf_parameter'], isset($firewall['logout']['csrf_token_manager']) ? new Reference($firewall['logout']['csrf_token_manager']) : null, false === $firewall['stateless'] && isset($firewall['context']) ? $firewall['context'] : null, ]) ; $config->replaceArgument(12, $firewall['logout']); } // Determine default entry point $configuredEntryPoint = $firewall['entry_point'] ?? null; // Authentication listeners $firewallAuthenticationProviders = []; [$authListeners, $defaultEntryPoint] = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint); // $configuredEntryPoint is resolved into a service ID and stored in $defaultEntryPoint $configuredEntryPoint = $defaultEntryPoint; // authenticator manager $authenticators = array_map(fn ($id) => new Reference($id), $firewallAuthenticationProviders); $container ->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authenticator.manager')) ->replaceArgument(0, $authenticators) ->replaceArgument(2, new Reference($firewallEventDispatcherId)) ->replaceArgument(3, $id) ->replaceArgument(7, $firewall['required_badges'] ?? []) ->addTag('monolog.logger', ['channel' => 'security']) ; $managerLocator = $container->getDefinition('security.authenticator.managers_locator'); $managerLocator->replaceArgument(0, array_merge($managerLocator->getArgument(0), [$id => new ServiceClosureArgument(new Reference($managerId))])); // authenticator manager listener $container ->setDefinition('security.firewall.authenticator.'.$id, new ChildDefinition('security.firewall.authenticator')) ->replaceArgument(0, new Reference($managerId)) ; if ($container->hasDefinition('debug.security.firewall')) { $container ->register('debug.security.firewall.authenticator.'.$id, TraceableAuthenticatorManagerListener::class) ->setDecoratedService('security.firewall.authenticator.'.$id) ->setArguments([new Reference('debug.security.firewall.authenticator.'.$id.'.inner')]) ->addTag('kernel.reset', ['method' => 'reset']) ; } // user checker listener $container ->setDefinition('security.listener.user_checker.'.$id, new ChildDefinition('security.listener.user_checker')) ->replaceArgument(0, new Reference('security.user_checker.'.$id)) ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); $listeners[] = new Reference('security.firewall.authenticator.'.$id); // Add authenticators to the debug:firewall command if ($container->hasDefinition('security.command.debug_firewall')) { $debugCommand = $container->getDefinition('security.command.debug_firewall'); $debugCommand->replaceArgument(3, array_merge($debugCommand->getArgument(3), [$id => $authenticators])); } $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); $listeners = array_merge($listeners, $authListeners); // Switch user listener if (isset($firewall['switch_user'])) { $listenerKeys[] = 'switch_user'; $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider, $firewall['stateless'])); } // Access listener $listeners[] = new Reference('security.access_listener'); // Exception listener $exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless'])); $config->replaceArgument(8, $firewall['access_denied_handler'] ?? null); $config->replaceArgument(9, $firewall['access_denied_url'] ?? null); $container->setAlias('security.user_checker.'.$id, new Alias($firewall['user_checker'], false)); $userCheckerLocator = $container->getDefinition('security.user_checker_locator'); $userCheckerLocator->replaceArgument(0, array_merge($userCheckerLocator->getArgument(0), [$id => new ServiceClosureArgument(new Reference('security.user_checker.'.$id))])); foreach ($this->getSortedFactories() as $factory) { $key = str_replace('-', '_', $factory->getKey()); if ('custom_authenticators' !== $key && \array_key_exists($key, $firewall)) { $listenerKeys[] = $key; } } if ($firewall['custom_authenticators'] ?? false) { foreach ($firewall['custom_authenticators'] as $customAuthenticatorId) { $listenerKeys[] = $customAuthenticatorId; } } $config->replaceArgument(10, $listenerKeys); $config->replaceArgument(11, $firewall['switch_user'] ?? null); return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null, $firewallAuthenticationProviders]; } private function createContextListener(ContainerBuilder $container, string $contextKey, ?string $firewallEventDispatcherId): string { if (isset($this->contextListeners[$contextKey])) { return $this->contextListeners[$contextKey]; } $listenerId = 'security.context_listener.'.\count($this->contextListeners); $listener = $container->setDefinition($listenerId, new ChildDefinition('security.context_listener')); $listener->replaceArgument(2, $contextKey); if (null !== $firewallEventDispatcherId) { $listener->replaceArgument(4, new Reference($firewallEventDispatcherId)); $listener->addTag('kernel.event_listener', ['event' => KernelEvents::RESPONSE, 'method' => 'onKernelResponse']); } return $this->contextListeners[$contextKey] = $listenerId; } private function createAuthenticationListeners(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, ?string $defaultProvider, array $providerIds, ?string $defaultEntryPoint): array { $listeners = []; $entryPoints = []; foreach ($this->getSortedFactories() as $factory) { $key = str_replace('-', '_', $factory->getKey()); if (isset($firewall[$key])) { $userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds); if (!$factory instanceof AuthenticatorFactoryInterface) { throw new InvalidConfigurationException(sprintf('Authenticator factory "%s" ("%s") must implement "%s".', get_debug_type($factory), $key, AuthenticatorFactoryInterface::class)); } if (null === $userProvider && !$factory instanceof StatelessAuthenticatorFactoryInterface) { $userProvider = $this->createMissingUserProvider($container, $id, $key); } $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider); if (\is_array($authenticators)) { foreach ($authenticators as $authenticator) { $authenticationProviders[] = $authenticator; $entryPoints[] = $authenticator; } } else { $authenticationProviders[] = $authenticators; $entryPoints[$key] = $authenticators; } if ($factory instanceof FirewallListenerFactoryInterface) { $firewallListenerIds = $factory->createListeners($container, $id, $firewall[$key]); foreach ($firewallListenerIds as $firewallListenerId) { $listeners[] = new Reference($firewallListenerId); } } } } // the actual entry point is configured by the RegisterEntryPointPass $container->setParameter('security.'.$id.'._indexed_authenticators', $entryPoints); return [$listeners, $defaultEntryPoint]; } private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds): ?string { if (isset($firewall[$factoryKey]['provider'])) { if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) { throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$factoryKey]['provider'])); } return $providerIds[$normalizedName]; } if ($defaultProvider) { return $defaultProvider; } if (!$providerIds) { if ($firewall['stateless'] ?? false) { return null; } return $this->createMissingUserProvider($container, $id, $factoryKey); } if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { return 'security.user_providers'; } throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" authenticator on "%s" firewall is ambiguous as there is more than one registered provider. Set the "provider" key to one of the configured providers, even if your custom authenticators don\'t use it.', $factoryKey, $id)); } private function createMissingUserProvider(ContainerBuilder $container, string $id, string $factoryKey): string { $userProvider = sprintf('security.user.provider.missing.%s', $factoryKey); $container->setDefinition( $userProvider, (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id) ); return $userProvider; } private function createHashers(array $hashers, ContainerBuilder $container): void { $hasherMap = []; foreach ($hashers as $class => $hasher) { $hasherMap[$class] = $this->createHasher($hasher); } $container ->getDefinition('security.password_hasher_factory') ->setArguments([$hasherMap]) ; } /** * @param array $config * * @return Reference|array */ private function createHasher(array $config): Reference|array { // a custom hasher service if (isset($config['id'])) { return $config['migrate_from'] ?? false ? [ 'instance' => new Reference($config['id']), 'migrate_from' => $config['migrate_from'], ] : new Reference($config['id']); } if ($config['migrate_from'] ?? false) { return $config; } // plaintext hasher if ('plaintext' === $config['algorithm']) { $arguments = [$config['ignore_case']]; return [ 'class' => PlaintextPasswordHasher::class, 'arguments' => $arguments, ]; } // pbkdf2 hasher if ('pbkdf2' === $config['algorithm']) { return [ 'class' => Pbkdf2PasswordHasher::class, 'arguments' => [ $config['hash_algorithm'], $config['encode_as_base64'], $config['iterations'], $config['key_length'], ], ]; } // bcrypt hasher if ('bcrypt' === $config['algorithm']) { $config['algorithm'] = 'native'; $config['native_algorithm'] = \PASSWORD_BCRYPT; return $this->createHasher($config); } // Argon2i hasher if ('argon2i' === $config['algorithm']) { if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { $config['algorithm'] = 'sodium'; } elseif (\defined('PASSWORD_ARGON2I')) { $config['algorithm'] = 'native'; $config['native_algorithm'] = \PASSWORD_ARGON2I; } else { throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available; use "%s" instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id" or "auto' : 'auto')); } return $this->createHasher($config); } if ('argon2id' === $config['algorithm']) { if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { $config['algorithm'] = 'sodium'; } elseif (\defined('PASSWORD_ARGON2ID')) { $config['algorithm'] = 'native'; $config['native_algorithm'] = \PASSWORD_ARGON2ID; } else { throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available; use "%s" or libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto')); } return $this->createHasher($config); } if ('native' === $config['algorithm']) { return [ 'class' => NativePasswordHasher::class, 'arguments' => [ $config['time_cost'], (($config['memory_cost'] ?? 0) << 10) ?: null, $config['cost'], ] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []), ]; } if ('sodium' === $config['algorithm']) { if (!SodiumPasswordHasher::isSupported()) { throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.'); } return [ 'class' => SodiumPasswordHasher::class, 'arguments' => [ $config['time_cost'], (($config['memory_cost'] ?? 0) << 10) ?: null, ], ]; } // run-time configured hasher return $config; } // Parses user providers and returns an array of their ids private function createUserProviders(array $config, ContainerBuilder $container): array { $providerIds = []; foreach ($config['providers'] as $name => $provider) { $id = $this->createUserDaoProvider($name, $provider, $container); $providerIds[str_replace('-', '_', $name)] = $id; } return $providerIds; } // Parses a tag and returns the id for the related user provider service private function createUserDaoProvider(string $name, array $provider, ContainerBuilder $container): string { $name = $this->getUserProviderId($name); // Doctrine Entity and In-memory DAO provider are managed by factories foreach ($this->userProviderFactories as $factory) { $key = str_replace('-', '_', $factory->getKey()); if (!empty($provider[$key])) { $factory->create($container, $name, $provider[$key]); return $name; } } // Existing DAO service provider if (isset($provider['id'])) { $container->setAlias($name, new Alias($provider['id'], false)); return $provider['id']; } // Chain provider if (isset($provider['chain'])) { $providers = []; foreach ($provider['chain']['providers'] as $providerName) { $providers[] = new Reference($this->getUserProviderId($providerName)); } $container ->setDefinition($name, new ChildDefinition('security.user.provider.chain')) ->addArgument(new IteratorArgument($providers)); return $name; } throw new InvalidConfigurationException(sprintf('Unable to create definition for "%s" user provider.', $name)); } private function getUserProviderId(string $name): string { return 'security.user.provider.concrete.'.strtolower($name); } private function createExceptionListener(ContainerBuilder $container, array $config, string $id, ?string $defaultEntryPoint, bool $stateless): string { $exceptionListenerId = 'security.exception_listener.'.$id; $listener = $container->setDefinition($exceptionListenerId, new ChildDefinition('security.exception_listener')); $listener->replaceArgument(3, $id); $listener->replaceArgument(4, null === $defaultEntryPoint ? null : new Reference($defaultEntryPoint)); $listener->replaceArgument(8, $stateless); // access denied handler setup if (isset($config['access_denied_handler'])) { $listener->replaceArgument(6, new Reference($config['access_denied_handler'])); } elseif (isset($config['access_denied_url'])) { $listener->replaceArgument(5, $config['access_denied_url']); } return $exceptionListenerId; } private function createSwitchUserListener(ContainerBuilder $container, string $id, array $config, ?string $defaultProvider, bool $stateless): string { $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider; if (!$userProvider) { throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "switch_user" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $id)); } if ($stateless && null !== $config['target_route']) { throw new InvalidConfigurationException(sprintf('Cannot set a "target_route" for the "switch_user" listener on the "%s" firewall as it is stateless.', $id)); } $switchUserListenerId = 'security.authentication.switchuser_listener.'.$id; $listener = $container->setDefinition($switchUserListenerId, new ChildDefinition('security.authentication.switchuser_listener')); $listener->replaceArgument(1, new Reference($userProvider)); $listener->replaceArgument(2, new Reference('security.user_checker.'.$id)); $listener->replaceArgument(3, $id); $listener->replaceArgument(6, $config['parameter']); $listener->replaceArgument(7, $config['role']); $listener->replaceArgument(9, $stateless); $listener->replaceArgument(11, $config['target_route']); return $switchUserListenerId; } private function createExpression(ContainerBuilder $container, string $expression): Reference { if (isset($this->expressions[$id = '.security.expression.'.ContainerBuilder::hash($expression)])) { return $this->expressions[$id]; } if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'])) { throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'); } $container ->register($id, Expression::class) ->addArgument($expression) ; return $this->expressions[$id] = new Reference($id); } private function createRequestMatcher(ContainerBuilder $container, ?string $path = null, ?string $host = null, ?int $port = null, array $methods = [], ?array $ips = null, array $attributes = []): Reference { if ($methods) { $methods = array_map('strtoupper', $methods); } if ($ips) { foreach ($ips as $ip) { $container->resolveEnvPlaceholders($ip, null, $usedEnvs); if (!$usedEnvs && !$this->isValidIps($ip)) { throw new \LogicException(sprintf('The given value "%s" in the "security.access_control" config option is not a valid IP address.', $ip)); } $usedEnvs = null; } } $id = '.security.request_matcher.'.ContainerBuilder::hash([ChainRequestMatcher::class, $path, $host, $port, $methods, $ips, $attributes]); if (isset($this->requestMatchers[$id])) { return $this->requestMatchers[$id]; } $arguments = []; if ($methods) { if (!$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([MethodRequestMatcher::class, $methods]))) { $container->register($lid, MethodRequestMatcher::class)->setArguments([$methods]); } $arguments[] = new Reference($lid); } if ($path) { if (!$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([PathRequestMatcher::class, $path]))) { $container->register($lid, PathRequestMatcher::class)->setArguments([$path]); } $arguments[] = new Reference($lid); } if ($host) { if (!$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([HostRequestMatcher::class, $host]))) { $container->register($lid, HostRequestMatcher::class)->setArguments([$host]); } $arguments[] = new Reference($lid); } if ($ips) { if (!$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([IpsRequestMatcher::class, $ips]))) { $container->register($lid, IpsRequestMatcher::class)->setArguments([$ips]); } $arguments[] = new Reference($lid); } if ($attributes) { if (!$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([AttributesRequestMatcher::class, $attributes]))) { $container->register($lid, AttributesRequestMatcher::class)->setArguments([$attributes]); } $arguments[] = new Reference($lid); } if ($port) { if (!$container->hasDefinition($lid = '.security.request_matcher.'.ContainerBuilder::hash([PortRequestMatcher::class, $port]))) { $container->register($lid, PortRequestMatcher::class)->setArguments([$port]); } $arguments[] = new Reference($lid); } $container ->register($id, ChainRequestMatcher::class) ->setArguments([$arguments]) ; return $this->requestMatchers[$id] = new Reference($id); } public function addAuthenticatorFactory(AuthenticatorFactoryInterface $factory): void { $this->factories[] = [$factory->getPriority(), $factory]; $this->sortedFactories = []; } public function addUserProviderFactory(UserProviderFactoryInterface $factory): void { $this->userProviderFactories[] = $factory; } public function getXsdValidationBasePath(): string|false { return __DIR__.'/../Resources/config/schema'; } public function getNamespace(): string { return 'http://symfony.com/schema/dic/security'; } public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface { // first assemble the factories return new MainConfiguration($this->getSortedFactories(), $this->userProviderFactories); } private function isValidIps(string|array $ips): bool { $ipsList = array_reduce((array) $ips, fn ($ips, $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []); if (!$ipsList) { return false; } foreach ($ipsList as $cidr) { if (!$this->isValidIp($cidr)) { return false; } } return true; } private function isValidIp(string $cidr): bool { $cidrParts = explode('/', $cidr); if (1 === \count($cidrParts)) { return false !== filter_var($cidrParts[0], \FILTER_VALIDATE_IP); } $ip = $cidrParts[0]; $netmask = $cidrParts[1]; if (!ctype_digit($netmask)) { return false; } if (filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { return $netmask <= 32; } if (filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { return $netmask <= 128; } return false; } /** * @return array */ private function getSortedFactories(): array { if (!$this->sortedFactories) { $factories = []; foreach ($this->factories as $i => $factory) { $factories[] = array_merge($factory, [$i]); } usort($factories, fn ($a, $b) => $b[0] <=> $a[0] ?: $a[2] <=> $b[2]); $this->sortedFactories = array_column($factories, 1); } return $this->sortedFactories; } private function createLogoutUrisParameter(array $firewallsConfig, ContainerBuilder $container): void { $logoutUris = []; foreach ($firewallsConfig as $name => $config) { if (!$logoutPath = $config['logout']['path'] ?? null) { continue; } if ('/' === $logoutPath[0]) { $logoutUris[$name] = $logoutPath; } } $container->setParameter('security.logout_uris', $logoutUris); } }