SecurityExtension.php 42 KB

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