/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

http://github.com/symfony/symfony · PHP · 1210 lines · 906 code · 217 blank · 87 comment · 140 complexity · 30d9221a81715933bbd5cd47635492a0 MD5 · raw file

Large files are truncated click here to view the full file

  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
  11. use Composer\InstalledVersions;
  12. use Symfony\Bridge\Twig\Extension\LogoutUrlExtension;
  13. use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
  14. use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface;
  15. use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
  16. use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
  17. use Symfony\Bundle\SecurityBundle\Security\LegacyLogoutHandlerListener;
  18. use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
  19. use Symfony\Component\Config\FileLocator;
  20. use Symfony\Component\Console\Application;
  21. use Symfony\Component\DependencyInjection\Alias;
  22. use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
  23. use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
  24. use Symfony\Component\DependencyInjection\ChildDefinition;
  25. use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
  26. use Symfony\Component\DependencyInjection\ContainerBuilder;
  27. use Symfony\Component\DependencyInjection\Definition;
  28. use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
  29. use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
  30. use Symfony\Component\DependencyInjection\Reference;
  31. use Symfony\Component\EventDispatcher\EventDispatcher;
  32. use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
  33. use Symfony\Component\HttpKernel\DependencyInjection\Extension;
  34. use Symfony\Component\HttpKernel\KernelEvents;
  35. use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
  36. use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
  37. use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
  38. use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
  39. use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy;
  40. use Symfony\Component\Security\Core\Authorization\Strategy\ConsensusStrategy;
  41. use Symfony\Component\Security\Core\Authorization\Strategy\PriorityStrategy;
  42. use Symfony\Component\Security\Core\Authorization\Strategy\UnanimousStrategy;
  43. use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
  44. use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
  45. use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
  46. use Symfony\Component\Security\Core\User\ChainUserProvider;
  47. use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
  48. use Symfony\Component\Security\Core\User\UserProviderInterface;
  49. use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener;
  50. use Symfony\Component\Security\Http\Event\CheckPassportEvent;
  51. /**
  52. * SecurityExtension.
  53. *
  54. * @author Fabien Potencier <fabien@symfony.com>
  55. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  56. */
  57. class SecurityExtension extends Extension implements PrependExtensionInterface
  58. {
  59. private $requestMatchers = [];
  60. private $expressions = [];
  61. private $contextListeners = [];
  62. /** @var list<array{int, AuthenticatorFactoryInterface|SecurityFactoryInterface}> */
  63. private $factories = [];
  64. /** @var list<AuthenticatorFactoryInterface|SecurityFactoryInterface> */
  65. private $sortedFactories = [];
  66. private $userProviderFactories = [];
  67. private $statelessFirewallKeys = [];
  68. private $authenticatorManagerEnabled = false;
  69. public function prepend(ContainerBuilder $container)
  70. {
  71. foreach ($this->getSortedFactories() as $factory) {
  72. if ($factory instanceof PrependExtensionInterface) {
  73. $factory->prepend($container);
  74. }
  75. }
  76. }
  77. public function load(array $configs, ContainerBuilder $container)
  78. {
  79. if (!class_exists(InstalledVersions::class)) {
  80. trigger_deprecation('symfony/security-bundle', '5.4', 'Configuring Symfony without the Composer Runtime API is deprecated. Consider upgrading to Composer 2.1 or later.');
  81. }
  82. if (!array_filter($configs)) {
  83. return;
  84. }
  85. $mainConfig = $this->getConfiguration($configs, $container);
  86. $config = $this->processConfiguration($mainConfig, $configs);
  87. // load services
  88. $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config'));
  89. $loader->load('security.php');
  90. $loader->load('password_hasher.php');
  91. $loader->load('security_listeners.php');
  92. $loader->load('security_rememberme.php');
  93. if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) {
  94. if ($config['always_authenticate_before_granting']) {
  95. throw new InvalidConfigurationException('The security option "always_authenticate_before_granting" cannot be used when "enable_authenticator_manager" is set to true. If you rely on this behavior, set it to false.');
  96. }
  97. $loader->load('security_authenticator.php');
  98. // The authenticator system no longer has anonymous tokens. This makes sure AccessListener
  99. // and AuthorizationChecker do not throw AuthenticationCredentialsNotFoundException when no
  100. // token is available in the token storage.
  101. $container->getDefinition('security.access_listener')->setArgument(3, false);
  102. $container->getDefinition('security.authorization_checker')->setArgument(3, false);
  103. $container->getDefinition('security.authorization_checker')->setArgument(4, false);
  104. } else {
  105. trigger_deprecation('symfony/security-bundle', '5.3', 'Not setting the "security.enable_authenticator_manager" config option to true is deprecated.');
  106. if ($config['always_authenticate_before_granting']) {
  107. $authorizationChecker = $container->getDefinition('security.authorization_checker');
  108. $authorizationCheckerArgs = $authorizationChecker->getArguments();
  109. array_splice($authorizationCheckerArgs, 1, 0, [new Reference('security.authentication_manager')]);
  110. $authorizationChecker->setArguments($authorizationCheckerArgs);
  111. }
  112. $loader->load('security_legacy.php');
  113. }
  114. if ($container::willBeAvailable('symfony/twig-bridge', LogoutUrlExtension::class, ['symfony/security-bundle'], true)) {
  115. $loader->load('templating_twig.php');
  116. }
  117. $loader->load('collectors.php');
  118. $loader->load('guard.php');
  119. $container->getDefinition('data_collector.security')->addArgument($this->authenticatorManagerEnabled);
  120. if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) {
  121. $loader->load('security_debug.php');
  122. }
  123. if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'], true)) {
  124. $container->removeDefinition('security.expression_language');
  125. $container->removeDefinition('security.access.expression_voter');
  126. }
  127. // set some global scalars
  128. $container->setParameter('security.access.denied_url', $config['access_denied_url']);
  129. $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']);
  130. $container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']);
  131. if (isset($config['access_decision_manager']['service'])) {
  132. $container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service']);
  133. } elseif (isset($config['access_decision_manager']['strategy_service'])) {
  134. $container
  135. ->getDefinition('security.access.decision_manager')
  136. ->addArgument(new Reference($config['access_decision_manager']['strategy_service']));
  137. } else {
  138. $container
  139. ->getDefinition('security.access.decision_manager')
  140. ->addArgument($this->createStrategyDefinition(
  141. $config['access_decision_manager']['strategy'] ?? MainConfiguration::STRATEGY_AFFIRMATIVE,
  142. $config['access_decision_manager']['allow_if_all_abstain'],
  143. $config['access_decision_manager']['allow_if_equal_granted_denied']
  144. ));
  145. }
  146. $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']);
  147. $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']);
  148. if (class_exists(Application::class)) {
  149. $loader->load('debug_console.php');
  150. $debugCommand = $container->getDefinition('security.command.debug_firewall');
  151. $debugCommand->replaceArgument(4, $this->authenticatorManagerEnabled);
  152. }
  153. $this->createFirewalls($config, $container);
  154. $this->createAuthorization($config, $container);
  155. $this->createRoleHierarchy($config, $container);
  156. $container->getDefinition('security.authentication.guard_handler')
  157. ->replaceArgument(2, $this->statelessFirewallKeys);
  158. // @deprecated since Symfony 5.3
  159. if ($config['encoders']) {
  160. $this->createEncoders($config['encoders'], $container);
  161. }
  162. if ($config['password_hashers']) {
  163. $this->createHashers($config['password_hashers'], $container);
  164. }
  165. if (class_exists(Application::class)) {
  166. $loader->load('console.php');
  167. // @deprecated since Symfony 5.3
  168. $container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders']));
  169. $container->getDefinition('security.command.user_password_hash')->replaceArgument(1, array_keys($config['password_hashers']));
  170. }
  171. $container->registerForAutoconfiguration(VoterInterface::class)
  172. ->addTag('security.voter');
  173. }
  174. /**
  175. * @throws \InvalidArgumentException if the $strategy is invalid
  176. */
  177. private function createStrategyDefinition(string $strategy, bool $allowIfAllAbstainDecisions, bool $allowIfEqualGrantedDeniedDecisions): Definition
  178. {
  179. switch ($strategy) {
  180. case MainConfiguration::STRATEGY_AFFIRMATIVE:
  181. return new Definition(AffirmativeStrategy::class, [$allowIfAllAbstainDecisions]);
  182. case MainConfiguration::STRATEGY_CONSENSUS:
  183. return new Definition(ConsensusStrategy::class, [$allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions]);
  184. case MainConfiguration::STRATEGY_UNANIMOUS:
  185. return new Definition(UnanimousStrategy::class, [$allowIfAllAbstainDecisions]);
  186. case MainConfiguration::STRATEGY_PRIORITY:
  187. return new Definition(PriorityStrategy::class, [$allowIfAllAbstainDecisions]);
  188. }
  189. throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategy));
  190. }
  191. private function createRoleHierarchy(array $config, ContainerBuilder $container)
  192. {
  193. if (!isset($config['role_hierarchy']) || 0 === \count($config['role_hierarchy'])) {
  194. $container->removeDefinition('security.access.role_hierarchy_voter');
  195. return;
  196. }
  197. $container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']);
  198. $container->removeDefinition('security.access.simple_role_voter');
  199. }
  200. private function createAuthorization(array $config, ContainerBuilder $container)
  201. {
  202. foreach ($config['access_control'] as $access) {
  203. $matcher = $this->createRequestMatcher(
  204. $container,
  205. $access['path'],
  206. $access['host'],
  207. $access['port'],
  208. $access['methods'],
  209. $access['ips']
  210. );
  211. $attributes = $access['roles'];
  212. if ($access['allow_if']) {
  213. $attributes[] = $this->createExpression($container, $access['allow_if']);
  214. }
  215. $emptyAccess = 0 === \count(array_filter($access));
  216. if ($emptyAccess) {
  217. throw new InvalidConfigurationException('One or more access control items are empty. Did you accidentally add lines only containing a "-" under "security.access_control"?');
  218. }
  219. $container->getDefinition('security.access_map')
  220. ->addMethodCall('add', [$matcher, $attributes, $access['requires_channel']]);
  221. }
  222. // allow cache warm-up for expressions
  223. if (\count($this->expressions)) {
  224. $container->getDefinition('security.cache_warmer.expression')
  225. ->replaceArgument(0, new IteratorArgument(array_values($this->expressions)));
  226. } else {
  227. $container->removeDefinition('security.cache_warmer.expression');
  228. }
  229. }
  230. private function createFirewalls(array $config, ContainerBuilder $container)
  231. {
  232. if (!isset($config['firewalls'])) {
  233. return;
  234. }
  235. $firewalls = $config['firewalls'];
  236. $providerIds = $this->createUserProviders($config, $container);
  237. $container->setParameter('security.firewalls', array_keys($firewalls));
  238. // make the ContextListener aware of the configured user providers
  239. $contextListenerDefinition = $container->getDefinition('security.context_listener');
  240. $arguments = $contextListenerDefinition->getArguments();
  241. $userProviders = [];
  242. foreach ($providerIds as $userProviderId) {
  243. $userProviders[] = new Reference($userProviderId);
  244. }
  245. $arguments[1] = $userProviderIteratorsArgument = new IteratorArgument($userProviders);
  246. $contextListenerDefinition->setArguments($arguments);
  247. $nbUserProviders = \count($userProviders);
  248. if ($nbUserProviders > 1) {
  249. $container->setDefinition('security.user_providers', new Definition(ChainUserProvider::class, [$userProviderIteratorsArgument]))
  250. ->setPublic(false);
  251. } elseif (0 === $nbUserProviders) {
  252. $container->removeDefinition('security.listener.user_provider');
  253. } else {
  254. $container->setAlias('security.user_providers', new Alias(current($providerIds)))->setPublic(false);
  255. }
  256. if (1 === \count($providerIds)) {
  257. $container->setAlias(UserProviderInterface::class, current($providerIds));
  258. }
  259. $customUserChecker = false;
  260. // load firewall map
  261. $mapDef = $container->getDefinition('security.firewall.map');
  262. $map = $authenticationProviders = $contextRefs = [];
  263. foreach ($firewalls as $name => $firewall) {
  264. if (isset($firewall['user_checker']) && 'security.user_checker' !== $firewall['user_checker']) {
  265. $customUserChecker = true;
  266. }
  267. $configId = 'security.firewall.map.config.'.$name;
  268. [$matcher, $listeners, $exceptionListener, $logoutListener] = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
  269. $contextId = 'security.firewall.map.context.'.$name;
  270. $isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']);
  271. $context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context');
  272. $context = $container->setDefinition($contextId, $context);
  273. $context
  274. ->replaceArgument(0, new IteratorArgument($listeners))
  275. ->replaceArgument(1, $exceptionListener)
  276. ->replaceArgument(2, $logoutListener)
  277. ->replaceArgument(3, new Reference($configId))
  278. ;
  279. $contextRefs[$contextId] = new Reference($contextId);
  280. $map[$contextId] = $matcher;
  281. }
  282. $container->setAlias('security.firewall.context_locator', (string) ServiceLocatorTagPass::register($container, $contextRefs));
  283. $mapDef->replaceArgument(0, new Reference('security.firewall.context_locator'));
  284. $mapDef->replaceArgument(1, new IteratorArgument($map));
  285. if (!$this->authenticatorManagerEnabled) {
  286. // add authentication providers to authentication manager
  287. $authenticationProviders = array_map(function ($id) {
  288. return new Reference($id);
  289. }, array_values(array_unique($authenticationProviders)));
  290. $container
  291. ->getDefinition('security.authentication.manager')
  292. ->replaceArgument(0, new IteratorArgument($authenticationProviders));
  293. }
  294. // register an autowire alias for the UserCheckerInterface if no custom user checker service is configured
  295. if (!$customUserChecker) {
  296. $container->setAlias('Symfony\Component\Security\Core\User\UserCheckerInterface', new Alias('security.user_checker', false));
  297. }
  298. }
  299. private function createFirewall(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, array $providerIds, string $configId)
  300. {
  301. $config = $container->setDefinition($configId, new ChildDefinition('security.firewall.config'));
  302. $config->replaceArgument(0, $id);
  303. $config->replaceArgument(1, $firewall['user_checker']);
  304. // Matcher
  305. $matcher = null;
  306. if (isset($firewall['request_matcher'])) {
  307. $matcher = new Reference($firewall['request_matcher']);
  308. } elseif (isset($firewall['pattern']) || isset($firewall['host'])) {
  309. $pattern = $firewall['pattern'] ?? null;
  310. $host = $firewall['host'] ?? null;
  311. $methods = $firewall['methods'] ?? [];
  312. $matcher = $this->createRequestMatcher($container, $pattern, $host, null, $methods);
  313. }
  314. $config->replaceArgument(2, $matcher ? (string) $matcher : null);
  315. $config->replaceArgument(3, $firewall['security']);
  316. // Security disabled?
  317. if (false === $firewall['security']) {
  318. return [$matcher, [], null, null];
  319. }
  320. $config->replaceArgument(4, $firewall['stateless']);
  321. $firewallEventDispatcherId = 'security.event_dispatcher.'.$id;
  322. // Provider id (must be configured explicitly per firewall/authenticator if more than one provider is set)
  323. $defaultProvider = null;
  324. if (isset($firewall['provider'])) {
  325. if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall['provider'])])) {
  326. throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall['provider']));
  327. }
  328. $defaultProvider = $providerIds[$normalizedName];
  329. if ($this->authenticatorManagerEnabled) {
  330. $container->setDefinition('security.listener.'.$id.'.user_provider', new ChildDefinition('security.listener.user_provider.abstract'))
  331. ->addTag('kernel.event_listener', ['dispatcher' => $firewallEventDispatcherId, 'event' => CheckPassportEvent::class, 'priority' => 2048, 'method' => 'checkPassport'])
  332. ->replaceArgument(0, new Reference($defaultProvider));
  333. }
  334. } elseif (1 === \count($providerIds)) {
  335. $defaultProvider = reset($providerIds);
  336. }
  337. $config->replaceArgument(5, $defaultProvider);
  338. // Register Firewall-specific event dispatcher
  339. $container->register($firewallEventDispatcherId, EventDispatcher::class)
  340. ->addTag('event_dispatcher.dispatcher', ['name' => $firewallEventDispatcherId]);
  341. // Register listeners
  342. $listeners = [];
  343. $listenerKeys = [];
  344. // Channel listener
  345. $listeners[] = new Reference('security.channel_listener');
  346. $contextKey = null;
  347. $contextListenerId = null;
  348. // Context serializer listener
  349. if (false === $firewall['stateless']) {
  350. $contextKey = $firewall['context'] ?? $id;
  351. $listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey, $this->authenticatorManagerEnabled ? $firewallEventDispatcherId : null));
  352. $sessionStrategyId = 'security.authentication.session_strategy';
  353. if ($this->authenticatorManagerEnabled) {
  354. $container
  355. ->setDefinition('security.listener.session.'.$id, new ChildDefinition('security.listener.session'))
  356. ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
  357. }
  358. } else {
  359. $this->statelessFirewallKeys[] = $id;
  360. $sessionStrategyId = 'security.authentication.session_strategy_noop';
  361. }
  362. $container->setAlias(new Alias('security.authentication.session_strategy.'.$id, false), $sessionStrategyId);
  363. $config->replaceArgument(6, $contextKey);
  364. // Logout listener
  365. $logoutListenerId = null;
  366. if (isset($firewall['logout'])) {
  367. $logoutListenerId = 'security.logout_listener.'.$id;
  368. $logoutListener = $container->setDefinition($logoutListenerId, new ChildDefinition('security.logout_listener'));
  369. $logoutListener->replaceArgument(2, new Reference($firewallEventDispatcherId));
  370. $logoutListener->replaceArgument(3, [
  371. 'csrf_parameter' => $firewall['logout']['csrf_parameter'],
  372. 'csrf_token_id' => $firewall['logout']['csrf_token_id'],
  373. 'logout_path' => $firewall['logout']['path'],
  374. ]);
  375. // add default logout listener
  376. if (isset($firewall['logout']['success_handler'])) {
  377. // deprecated, to be removed in Symfony 6.0
  378. $logoutSuccessHandlerId = $firewall['logout']['success_handler'];
  379. $container->register('security.logout.listener.legacy_success_listener.'.$id, LegacyLogoutHandlerListener::class)
  380. ->setArguments([new Reference($logoutSuccessHandlerId)])
  381. ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
  382. } else {
  383. $logoutSuccessListenerId = 'security.logout.listener.default.'.$id;
  384. $container->setDefinition($logoutSuccessListenerId, new ChildDefinition('security.logout.listener.default'))
  385. ->replaceArgument(1, $firewall['logout']['target'])
  386. ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
  387. }
  388. // add CSRF provider
  389. if (isset($firewall['logout']['csrf_token_generator'])) {
  390. $logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_generator']));
  391. }
  392. // add session logout listener
  393. if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
  394. $container->setDefinition('security.logout.listener.session.'.$id, new ChildDefinition('security.logout.listener.session'))
  395. ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
  396. }
  397. // add cookie logout listener
  398. if (\count($firewall['logout']['delete_cookies']) > 0) {
  399. $container->setDefinition('security.logout.listener.cookie_clearing.'.$id, new ChildDefinition('security.logout.listener.cookie_clearing'))
  400. ->addArgument($firewall['logout']['delete_cookies'])
  401. ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
  402. }
  403. // add custom listeners (deprecated)
  404. foreach ($firewall['logout']['handlers'] as $i => $handlerId) {
  405. $container->register('security.logout.listener.legacy_handler.'.$i, LegacyLogoutHandlerListener::class)
  406. ->addArgument(new Reference($handlerId))
  407. ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
  408. }
  409. // register with LogoutUrlGenerator
  410. $container
  411. ->getDefinition('security.logout_url_generator')
  412. ->addMethodCall('registerListener', [
  413. $id,
  414. $firewall['logout']['path'],
  415. $firewall['logout']['csrf_token_id'],
  416. $firewall['logout']['csrf_parameter'],
  417. isset($firewall['logout']['csrf_token_generator']) ? new Reference($firewall['logout']['csrf_token_generator']) : null,
  418. false === $firewall['stateless'] && isset($firewall['context']) ? $firewall['context'] : null,
  419. ])
  420. ;
  421. }
  422. // Determine default entry point
  423. $configuredEntryPoint = $firewall['entry_point'] ?? null;
  424. // Authentication listeners
  425. $firewallAuthenticationProviders = [];
  426. [$authListeners, $defaultEntryPoint] = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId);
  427. if (!$this->authenticatorManagerEnabled) {
  428. $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders);
  429. } else {
  430. // $configuredEntryPoint is resolved into a service ID and stored in $defaultEntryPoint
  431. $configuredEntryPoint = $defaultEntryPoint;
  432. // authenticator manager
  433. $authenticators = array_map(function ($id) {
  434. return new Reference($id);
  435. }, $firewallAuthenticationProviders);
  436. $container
  437. ->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authenticator.manager'))
  438. ->replaceArgument(0, $authenticators)
  439. ->replaceArgument(2, new Reference($firewallEventDispatcherId))
  440. ->replaceArgument(3, $id)
  441. ->replaceArgument(7, $firewall['required_badges'] ?? [])
  442. ->addTag('monolog.logger', ['channel' => 'security'])
  443. ;
  444. $managerLocator = $container->getDefinition('security.authenticator.managers_locator');
  445. $managerLocator->replaceArgument(0, array_merge($managerLocator->getArgument(0), [$id => new ServiceClosureArgument(new Reference($managerId))]));
  446. // authenticator manager listener
  447. $container
  448. ->setDefinition('security.firewall.authenticator.'.$id, new ChildDefinition('security.firewall.authenticator'))
  449. ->replaceArgument(0, new Reference($managerId))
  450. ;
  451. if ($container->hasDefinition('debug.security.firewall') && $this->authenticatorManagerEnabled) {
  452. $container
  453. ->register('debug.security.firewall.authenticator.'.$id, TraceableAuthenticatorManagerListener::class)
  454. ->setDecoratedService('security.firewall.authenticator.'.$id)
  455. ->setArguments([new Reference('debug.security.firewall.authenticator.'.$id.'.inner')])
  456. ;
  457. }
  458. // user checker listener
  459. $container
  460. ->setDefinition('security.listener.user_checker.'.$id, new ChildDefinition('security.listener.user_checker'))
  461. ->replaceArgument(0, new Reference('security.user_checker.'.$id))
  462. ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
  463. $listeners[] = new Reference('security.firewall.authenticator.'.$id);
  464. // Add authenticators to the debug:firewall command
  465. if ($container->hasDefinition('security.command.debug_firewall')) {
  466. $debugCommand = $container->getDefinition('security.command.debug_firewall');
  467. $debugCommand->replaceArgument(3, array_merge($debugCommand->getArgument(3), [$id => $authenticators]));
  468. }
  469. }
  470. $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint);
  471. $listeners = array_merge($listeners, $authListeners);
  472. // Switch user listener
  473. if (isset($firewall['switch_user'])) {
  474. $listenerKeys[] = 'switch_user';
  475. $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider, $firewall['stateless']));
  476. }
  477. // Access listener
  478. $listeners[] = new Reference('security.access_listener');
  479. // Exception listener
  480. $exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless']));
  481. $config->replaceArgument(8, $firewall['access_denied_handler'] ?? null);
  482. $config->replaceArgument(9, $firewall['access_denied_url'] ?? null);
  483. $container->setAlias('security.user_checker.'.$id, new Alias($firewall['user_checker'], false));
  484. foreach ($this->getSortedFactories() as $factory) {
  485. $key = str_replace('-', '_', $factory->getKey());
  486. if ('custom_authenticators' !== $key && \array_key_exists($key, $firewall)) {
  487. $listenerKeys[] = $key;
  488. }
  489. }
  490. if ($firewall['custom_authenticators'] ?? false) {
  491. foreach ($firewall['custom_authenticators'] as $customAuthenticatorId) {
  492. $listenerKeys[] = $customAuthenticatorId;
  493. }
  494. }
  495. $config->replaceArgument(10, $listenerKeys);
  496. $config->replaceArgument(11, $firewall['switch_user'] ?? null);
  497. return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null];
  498. }
  499. private function createContextListener(ContainerBuilder $container, string $contextKey, ?string $firewallEventDispatcherId)
  500. {
  501. if (isset($this->contextListeners[$contextKey])) {
  502. return $this->contextListeners[$contextKey];
  503. }
  504. $listenerId = 'security.context_listener.'.\count($this->contextListeners);
  505. $listener = $container->setDefinition($listenerId, new ChildDefinition('security.context_listener'));
  506. $listener->replaceArgument(2, $contextKey);
  507. if (null !== $firewallEventDispatcherId) {
  508. $listener->replaceArgument(4, new Reference($firewallEventDispatcherId));
  509. $listener->addTag('kernel.event_listener', ['event' => KernelEvents::RESPONSE, 'method' => 'onKernelResponse']);
  510. }
  511. return $this->contextListeners[$contextKey] = $listenerId;
  512. }
  513. private function createAuthenticationListeners(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, ?string $defaultProvider, array $providerIds, ?string $defaultEntryPoint, string $contextListenerId = null)
  514. {
  515. $listeners = [];
  516. $hasListeners = false;
  517. $entryPoints = [];
  518. foreach ($this->getSortedFactories() as $factory) {
  519. $key = str_replace('-', '_', $factory->getKey());
  520. if (isset($firewall[$key])) {
  521. $userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds, $contextListenerId);
  522. if ($this->authenticatorManagerEnabled) {
  523. if (!$factory instanceof AuthenticatorFactoryInterface) {
  524. throw new InvalidConfigurationException(sprintf('Cannot configure AuthenticatorManager as "%s" authentication does not support it, set "security.enable_authenticator_manager" to `false`.', $key));
  525. }
  526. $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider);
  527. if (\is_array($authenticators)) {
  528. foreach ($authenticators as $authenticator) {
  529. $authenticationProviders[] = $authenticator;
  530. $entryPoints[] = $authenticator;
  531. }
  532. } else {
  533. $authenticationProviders[] = $authenticators;
  534. $entryPoints[$key] = $authenticators;
  535. }
  536. } else {
  537. [$provider, $listenerId, $defaultEntryPoint] = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);
  538. $listeners[] = new Reference($listenerId);
  539. $authenticationProviders[] = $provider;
  540. }
  541. if ($factory instanceof FirewallListenerFactoryInterface) {
  542. $firewallListenerIds = $factory->createListeners($container, $id, $firewall[$key]);
  543. foreach ($firewallListenerIds as $firewallListenerId) {
  544. $listeners[] = new Reference($firewallListenerId);
  545. }
  546. }
  547. $hasListeners = true;
  548. }
  549. }
  550. // the actual entry point is configured by the RegisterEntryPointPass
  551. $container->setParameter('security.'.$id.'._indexed_authenticators', $entryPoints);
  552. if (false === $hasListeners && !$this->authenticatorManagerEnabled) {
  553. throw new InvalidConfigurationException(sprintf('No authentication listener registered for firewall "%s".', $id));
  554. }
  555. return [$listeners, $defaultEntryPoint];
  556. }
  557. private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): string
  558. {
  559. if (isset($firewall[$factoryKey]['provider'])) {
  560. if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) {
  561. throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$factoryKey]['provider']));
  562. }
  563. return $providerIds[$normalizedName];
  564. }
  565. if ('remember_me' === $factoryKey && $contextListenerId) {
  566. $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']);
  567. }
  568. if ($defaultProvider) {
  569. return $defaultProvider;
  570. }
  571. if (!$providerIds) {
  572. $userProvider = sprintf('security.user.provider.missing.%s', $factoryKey);
  573. $container->setDefinition(
  574. $userProvider,
  575. (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)
  576. );
  577. return $userProvider;
  578. }
  579. if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) {
  580. if ('custom_authenticators' === $factoryKey) {
  581. trigger_deprecation('symfony/security-bundle', '5.4', 'Not configuring explicitly the provider for the "%s" listener on "%s" firewall is deprecated because it\'s ambiguous as there is more than one registered provider.', $factoryKey, $id);
  582. }
  583. return 'security.user_providers';
  584. }
  585. throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" %s on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $this->authenticatorManagerEnabled ? 'authenticator' : 'listener', $id));
  586. }
  587. private function createEncoders(array $encoders, ContainerBuilder $container)
  588. {
  589. $encoderMap = [];
  590. foreach ($encoders as $class => $encoder) {
  591. if (class_exists($class) && !is_a($class, PasswordAuthenticatedUserInterface::class, true)) {
  592. trigger_deprecation('symfony/security-bundle', '5.3', 'Configuring an encoder for a user class that does not implement "%s" is deprecated, class "%s" should implement it.', PasswordAuthenticatedUserInterface::class, $class);
  593. }
  594. $encoderMap[$class] = $this->createEncoder($encoder);
  595. }
  596. $container
  597. ->getDefinition('security.encoder_factory.generic')
  598. ->setArguments([$encoderMap])
  599. ;
  600. }
  601. private function createEncoder(array $config)
  602. {
  603. // a custom encoder service
  604. if (isset($config['id'])) {
  605. return new Reference($config['id']);
  606. }
  607. if ($config['migrate_from'] ?? false) {
  608. return $config;
  609. }
  610. // plaintext encoder
  611. if ('plaintext' === $config['algorithm']) {
  612. $arguments = [$config['ignore_case']];
  613. return [
  614. 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder',
  615. 'arguments' => $arguments,
  616. ];
  617. }
  618. // pbkdf2 encoder
  619. if ('pbkdf2' === $config['algorithm']) {
  620. return [
  621. 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder',
  622. 'arguments' => [
  623. $config['hash_algorithm'],
  624. $config['encode_as_base64'],
  625. $config['iterations'],
  626. $config['key_length'],
  627. ],
  628. ];
  629. }
  630. // bcrypt encoder
  631. if ('bcrypt' === $config['algorithm']) {
  632. $config['algorithm'] = 'native';
  633. $config['native_algorithm'] = \PASSWORD_BCRYPT;
  634. return $this->createEncoder($config);
  635. }
  636. // Argon2i encoder
  637. if ('argon2i' === $config['algorithm']) {
  638. if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
  639. $config['algorithm'] = 'sodium';
  640. } elseif (\defined('PASSWORD_ARGON2I')) {
  641. $config['algorithm'] = 'native';
  642. $config['native_algorithm'] = \PASSWORD_ARGON2I;
  643. } else {
  644. throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Use "%s" instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id", "auto' : 'auto'));
  645. }
  646. return $this->createEncoder($config);
  647. }
  648. if ('argon2id' === $config['algorithm']) {
  649. if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
  650. $config['algorithm'] = 'sodium';
  651. } elseif (\defined('PASSWORD_ARGON2ID')) {
  652. $config['algorithm'] = 'native';
  653. $config['native_algorithm'] = \PASSWORD_ARGON2ID;
  654. } else {
  655. throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. Either use "%s", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto'));
  656. }
  657. return $this->createEncoder($config);
  658. }
  659. if ('native' === $config['algorithm']) {
  660. return [
  661. 'class' => NativePasswordEncoder::class,
  662. 'arguments' => [
  663. $config['time_cost'],
  664. (($config['memory_cost'] ?? 0) << 10) ?: null,
  665. $config['cost'],
  666. ] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
  667. ];
  668. }
  669. if ('sodium' === $config['algorithm']) {
  670. if (!SodiumPasswordHasher::isSupported()) {
  671. throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.');
  672. }
  673. return [
  674. 'class' => SodiumPasswordEncoder::class,
  675. 'arguments' => [
  676. $config['time_cost'],
  677. (($config['memory_cost'] ?? 0) << 10) ?: null,
  678. ],
  679. ];
  680. }
  681. // run-time configured encoder
  682. return $config;
  683. }
  684. private function createHashers(array $hashers, ContainerBuilder $container)
  685. {
  686. $hasherMap = [];
  687. foreach ($hashers as $class => $hasher) {
  688. // @deprecated since Symfony 5.3, remove the check in 6.0
  689. if (class_exists($class) && !is_a($class, PasswordAuthenticatedUserInterface::class, true)) {
  690. trigger_deprecation('symfony/security-bundle', '5.3', 'Configuring a password hasher for a user class that does not implement "%s" is deprecated, class "%s" should implement it.', PasswordAuthenticatedUserInterface::class, $class);
  691. }
  692. $hasherMap[$class] = $this->createHasher($hasher);
  693. }
  694. $container
  695. ->getDefinition('security.password_hasher_factory')
  696. ->setArguments([$hasherMap])
  697. ;
  698. }
  699. private function createHasher(array $config)
  700. {
  701. // a custom hasher service
  702. if (isset($config['id'])) {
  703. return new Reference($config['id']);
  704. }
  705. if ($config['migrate_from'] ?? false) {
  706. return $config;
  707. }
  708. // plaintext hasher
  709. if ('plaintext' === $config['algorithm']) {
  710. $arguments = [$config['ignore_case']];
  711. return [
  712. 'class' => PlaintextPasswordHasher::class,
  713. 'arguments' => $arguments,
  714. ];
  715. }
  716. // pbkdf2 hasher
  717. if ('pbkdf2' === $config['algorithm']) {
  718. return [
  719. 'class' => Pbkdf2PasswordHasher::class,
  720. 'arguments' => [
  721. $config['hash_algorithm'],
  722. $config['encode_as_base64'],
  723. $config['iterations'],
  724. $config['key_length'],
  725. ],
  726. ];
  727. }
  728. // bcrypt hasher
  729. if ('bcrypt' === $config['algorithm']) {
  730. $config['algorithm'] = 'native';
  731. $config['native_algorithm'] = \PASSWORD_BCRYPT;
  732. return $this->createHasher($config);
  733. }
  734. // Argon2i hasher
  735. if ('argon2i' === $config['algorithm']) {
  736. if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
  737. $config['algorithm'] = 'sodium';
  738. } elseif (\defined('PASSWORD_ARGON2I')) {
  739. $config['algorithm'] = 'native';
  740. $config['native_algorithm'] = \PASSWORD_ARGON2I;
  741. } else {
  742. throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Either use "%s" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id", "auto' : 'auto'));
  743. }
  744. return $this->createHasher($config);
  745. }
  746. if ('argon2id' === $config['algorithm']) {
  747. if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
  748. $config['algorithm'] = 'sodium';
  749. } elseif (\defined('PASSWORD_ARGON2ID')) {
  750. $config['algorithm'] = 'native';
  751. $config['native_algorithm'] = \PASSWORD_ARGON2ID;
  752. } else {
  753. throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. Either use "%s", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto'));
  754. }
  755. return $this->createHasher($config);
  756. }
  757. if ('native' === $config['algorithm']) {
  758. return [
  759. 'class' => NativePasswordHasher::class,
  760. 'arguments' => [
  761. $config['time_cost'],
  762. (($config['memory_cost'] ?? 0) << 10) ?: null,
  763. $config['cost'],
  764. ] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
  765. ];
  766. }
  767. if ('sodium' === $config['algorithm']) {
  768. if (!SodiumPasswordHasher::isSupported()) {
  769. throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.');
  770. }
  771. return [
  772. 'class' => SodiumPasswordHasher::class,
  773. 'arguments' => [
  774. $config['time_cost'],
  775. (($config['memory_cost'] ?? 0) << 10) ?: null,
  776. ],
  777. ];
  778. }
  779. // run-time configured hasher
  780. return $config;
  781. }
  782. // Parses user providers and returns an array of their ids
  783. private function createUserProviders(array $config, ContainerBuilder $container): array
  784. {
  785. $providerIds = [];
  786. foreach ($config['providers'] as $name => $provider) {
  787. $id = $this->createUserDaoProvider($name, $provider, $container);
  788. $providerIds[str_replace('-', '_', $name)] = $id;
  789. }
  790. return $providerIds;
  791. }
  792. // Parses a <provider> tag and returns the id for the related user provider service
  793. private function createUserDaoProvider(string $name, array $provider, ContainerBuilder $container): string
  794. {
  795. $name = $this->getUserProviderId($name);
  796. // Doctrine Entity and In-memory DAO provider are managed by factories
  797. foreach ($this->userProviderFactories as $factory) {
  798. $key = str_replace('-', '_', $factory->getKey());
  799. if (!empty($provider[$key])) {
  800. $factory->create($container, $name, $provider[$key]);
  801. return $name;
  802. }
  803. }
  804. // Existing DAO service provider
  805. if (isset($provider['id'])) {
  806. $container->setAlias($name, new Alias($provider['id'], false));
  807. return $provider['id'];
  808. }
  809. // Chain provider
  810. if (isset($provider['chain'])) {
  811. $providers = [];
  812. foreach ($provider['chain']['providers'] as $providerName) {
  813. $providers[] = new Reference($this->getUserProviderId($providerName));
  814. }
  815. $container
  816. ->setDefinition($name, new ChildDefinition('security.user.provider.chain'))
  817. ->addArgument(new IteratorArgument($providers));
  818. return $name;
  819. }
  820. throw new InvalidConfigurationException(sprintf('Unable to create definition for "%s" user provider.', $name));
  821. }
  822. private function getUserProviderId(string $name): string
  823. {
  824. return 'security.user.provider.concrete.'.strtolower($name);
  825. }
  826. private function createExceptionListener(ContainerBuilder $container, array $config, string $id, ?string $defaultEntryPoint, bool $stateless): string
  827. {
  828. $exceptionListenerId = 'security.exception_listener.'.$id;
  829. $listener = $container->setDefinition($exceptionListenerId, new ChildDefinition('security.exception_listener'));
  830. $listener->replaceArgument(3, $id);
  831. $listener->replaceArgument(4, null === $defaultEntryPoint ? null : new Reference($defaultEntryPoint));
  832. $listener->replaceArgument(8, $stateless);
  833. // access denied handler setup
  834. if (isset($config['access_denied_handler'])) {
  835. $listener->replaceArgument(6, new Reference($config['access_denied_handler']));
  836. } elseif (isset($config['access_denied_url'])) {
  837. $listener->replaceArgument(5, $config['access_denied_url']);
  838. }
  839. return $exceptionListenerId;
  840. }
  841. private function createSwitchUserListener(ContainerBuilder $container, string $id, array $config, ?string $defaultProvider, bool $stateless): string
  842. {
  843. $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider;
  844. if (!$userProvider) {
  845. 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));
  846. }
  847. $switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
  848. $listener = $container->setDefinition($switchUserListenerId, new ChildDefinition('security.authentication.switchuser_listener'));
  849. $listener->replaceArgument(1, new Reference($userProvider));
  850. $listener->replaceArgument(2, new Reference('security.user_checker.'.$id));
  851. $listener->replaceArgument(3, $id);
  852. $listener->replaceArgument(6, $config['parameter']);
  853. $listener->replaceArgument(7, $config['role']);
  854. $listener->replaceArgument(9, $stateless);
  855. return $switchUserListenerId;
  856. }
  857. private function createExpression(ContainerBuilder $container, string $expression): Reference
  858. {
  859. if (isset($this->expressions[$id = '.security.expression.'.ContainerBuilder::hash($expression)])) {
  860. return $this->expressions[$id];
  861. }
  862. if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'], true)) {
  863. throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
  864. }
  865. $container
  866. ->register($id, 'Symfony\Component\ExpressionLanguage\Expression')
  867. ->setPublic(false)
  868. ->addArgument($expression)
  869. ;
  870. return $this->expressions[$id] = new Reference($id);
  871. }