1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
13
14use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
15use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
16use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
17use Symfony\Component\Config\FileLocator;
18use Symfony\Component\Console\Application;
19use Symfony\Component\DependencyInjection\Alias;
20use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
21use Symfony\Component\DependencyInjection\ChildDefinition;
22use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
23use Symfony\Component\DependencyInjection\ContainerBuilder;
24use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
25use Symfony\Component\DependencyInjection\Parameter;
26use Symfony\Component\DependencyInjection\Reference;
27use Symfony\Component\HttpKernel\DependencyInjection\Extension;
28use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
29use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
30use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
31use Symfony\Component\Templating\Helper\Helper;
32use Twig\Extension\AbstractExtension;
33
34/**
35 * SecurityExtension.
36 *
37 * @author Fabien Potencier <fabien@symfony.com>
38 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
39 */
40class SecurityExtension extends Extension
41{
42    private $requestMatchers = [];
43    private $expressions = [];
44    private $contextListeners = [];
45    private $listenerPositions = ['pre_auth', 'form', 'http', 'remember_me'];
46    private $factories = [];
47    private $userProviderFactories = [];
48    private $expressionLanguage;
49    private $logoutOnUserChangeByContextKey = [];
50    private $statelessFirewallKeys = [];
51
52    public function __construct()
53    {
54        foreach ($this->listenerPositions as $position) {
55            $this->factories[$position] = [];
56        }
57    }
58
59    public function load(array $configs, ContainerBuilder $container)
60    {
61        if (!array_filter($configs)) {
62            return;
63        }
64
65        $mainConfig = $this->getConfiguration($configs, $container);
66
67        $config = $this->processConfiguration($mainConfig, $configs);
68
69        // load services
70        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
71        $loader->load('security.xml');
72        $loader->load('security_listeners.xml');
73        $loader->load('security_rememberme.xml');
74
75        if (class_exists(Helper::class)) {
76            $loader->load('templating_php.xml');
77
78            $container->getDefinition('templating.helper.logout_url')->setPrivate(true);
79            $container->getDefinition('templating.helper.security')->setPrivate(true);
80        }
81
82        if (class_exists(AbstractExtension::class)) {
83            $loader->load('templating_twig.xml');
84        }
85
86        $loader->load('collectors.xml');
87        $loader->load('guard.xml');
88
89        $container->getDefinition('security.authentication.guard_handler')->setPrivate(true);
90        $container->getDefinition('security.firewall')->setPrivate(true);
91        $container->getDefinition('security.firewall.context')->setPrivate(true);
92        $container->getDefinition('security.validator.user_password')->setPrivate(true);
93        $container->getDefinition('security.rememberme.response_listener')->setPrivate(true);
94        $container->getAlias('security.encoder_factory')->setPrivate(true);
95
96        if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) {
97            $loader->load('security_debug.xml');
98
99            $container->getAlias('security.firewall')->setPrivate(true);
100        }
101
102        if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
103            $container->removeDefinition('security.expression_language');
104            $container->removeDefinition('security.access.expression_voter');
105        }
106
107        // set some global scalars
108        $container->setParameter('security.access.denied_url', $config['access_denied_url']);
109        $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']);
110        $container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']);
111
112        if (isset($config['access_decision_manager']['service'])) {
113            $container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service'])->setPrivate(true);
114        } else {
115            $container
116                ->getDefinition('security.access.decision_manager')
117                ->addArgument($config['access_decision_manager']['strategy'])
118                ->addArgument($config['access_decision_manager']['allow_if_all_abstain'])
119                ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']);
120        }
121
122        $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']);
123        $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']);
124
125        $this->createFirewalls($config, $container);
126        $this->createAuthorization($config, $container);
127        $this->createRoleHierarchy($config, $container);
128
129        $container->getDefinition('security.authentication.guard_handler')
130            ->replaceArgument(2, $this->statelessFirewallKeys);
131
132        if ($config['encoders']) {
133            $this->createEncoders($config['encoders'], $container);
134        }
135
136        if (class_exists(Application::class)) {
137            $loader->load('console.xml');
138            $container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders']));
139        }
140
141        // load ACL
142        if (isset($config['acl'])) {
143            $this->aclLoad($config['acl'], $container);
144        } else {
145            $container->removeDefinition('security.command.init_acl');
146            $container->removeDefinition('security.command.set_acl');
147        }
148
149        $container->registerForAutoconfiguration(VoterInterface::class)
150            ->addTag('security.voter');
151
152        if (\PHP_VERSION_ID < 70000) {
153            // add some required classes for compilation
154            $this->addClassesToCompile([
155                'Symfony\Component\Security\Http\Firewall',
156                'Symfony\Component\Security\Core\User\UserProviderInterface',
157                'Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager',
158                'Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage',
159                'Symfony\Component\Security\Core\Authorization\AccessDecisionManager',
160                'Symfony\Component\Security\Core\Authorization\AuthorizationChecker',
161                'Symfony\Component\Security\Core\Authorization\Voter\VoterInterface',
162                'Symfony\Bundle\SecurityBundle\Security\FirewallConfig',
163                'Symfony\Bundle\SecurityBundle\Security\FirewallContext',
164                'Symfony\Component\HttpFoundation\RequestMatcher',
165            ]);
166        }
167    }
168
169    private function aclLoad($config, ContainerBuilder $container)
170    {
171        if (!interface_exists('Symfony\Component\Security\Acl\Model\AclInterface')) {
172            throw new \LogicException('You must install symfony/security-acl in order to use the ACL functionality.');
173        }
174
175        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
176        $loader->load('security_acl.xml');
177
178        if (isset($config['cache']['id'])) {
179            $container->setAlias('security.acl.cache', $config['cache']['id'])->setPrivate(true);
180        }
181        $container->getDefinition('security.acl.voter.basic_permissions')->addArgument($config['voter']['allow_if_object_identity_unavailable']);
182
183        // custom ACL provider
184        if (isset($config['provider'])) {
185            $container->setAlias('security.acl.provider', $config['provider'])->setPrivate(true);
186
187            return;
188        }
189
190        $this->configureDbalAclProvider($config, $container, $loader);
191    }
192
193    private function configureDbalAclProvider(array $config, ContainerBuilder $container, $loader)
194    {
195        $loader->load('security_acl_dbal.xml');
196
197        $container->getDefinition('security.acl.dbal.schema')->setPrivate(true);
198        $container->getAlias('security.acl.dbal.connection')->setPrivate(true);
199        $container->getAlias('security.acl.provider')->setPrivate(true);
200
201        if (null !== $config['connection']) {
202            $container->setAlias('security.acl.dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['connection']))->setPrivate(true);
203        }
204
205        $container
206            ->getDefinition('security.acl.dbal.schema_listener')
207            ->addTag('doctrine.event_listener', [
208                'connection' => $config['connection'],
209                'event' => 'postGenerateSchema',
210                'lazy' => true,
211            ])
212        ;
213
214        $container->getDefinition('security.acl.cache.doctrine')->addArgument($config['cache']['prefix']);
215
216        $container->setParameter('security.acl.dbal.class_table_name', $config['tables']['class']);
217        $container->setParameter('security.acl.dbal.entry_table_name', $config['tables']['entry']);
218        $container->setParameter('security.acl.dbal.oid_table_name', $config['tables']['object_identity']);
219        $container->setParameter('security.acl.dbal.oid_ancestors_table_name', $config['tables']['object_identity_ancestors']);
220        $container->setParameter('security.acl.dbal.sid_table_name', $config['tables']['security_identity']);
221    }
222
223    private function createRoleHierarchy(array $config, ContainerBuilder $container)
224    {
225        if (!isset($config['role_hierarchy']) || 0 === \count($config['role_hierarchy'])) {
226            $container->removeDefinition('security.access.role_hierarchy_voter');
227
228            return;
229        }
230
231        $container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']);
232        $container->removeDefinition('security.access.simple_role_voter');
233    }
234
235    private function createAuthorization($config, ContainerBuilder $container)
236    {
237        if (!$config['access_control']) {
238            return;
239        }
240
241        if (\PHP_VERSION_ID < 70000) {
242            $this->addClassesToCompile([
243                'Symfony\\Component\\Security\\Http\\AccessMap',
244            ]);
245        }
246
247        foreach ($config['access_control'] as $access) {
248            $matcher = $this->createRequestMatcher(
249                $container,
250                $access['path'],
251                $access['host'],
252                $access['methods'],
253                $access['ips']
254            );
255
256            $attributes = $access['roles'];
257            if ($access['allow_if']) {
258                $attributes[] = $this->createExpression($container, $access['allow_if']);
259            }
260
261            $container->getDefinition('security.access_map')
262                      ->addMethodCall('add', [$matcher, $attributes, $access['requires_channel']]);
263        }
264    }
265
266    private function createFirewalls($config, ContainerBuilder $container)
267    {
268        if (!isset($config['firewalls'])) {
269            return;
270        }
271
272        $firewalls = $config['firewalls'];
273        $providerIds = $this->createUserProviders($config, $container);
274
275        // make the ContextListener aware of the configured user providers
276        $contextListenerDefinition = $container->getDefinition('security.context_listener');
277        $arguments = $contextListenerDefinition->getArguments();
278        $userProviders = [];
279        foreach ($providerIds as $userProviderId) {
280            $userProviders[] = new Reference($userProviderId);
281        }
282        $arguments[1] = new IteratorArgument($userProviders);
283        $contextListenerDefinition->setArguments($arguments);
284
285        $customUserChecker = false;
286
287        // load firewall map
288        $mapDef = $container->getDefinition('security.firewall.map');
289        $map = $authenticationProviders = $contextRefs = [];
290        foreach ($firewalls as $name => $firewall) {
291            if (isset($firewall['user_checker']) && 'security.user_checker' !== $firewall['user_checker']) {
292                $customUserChecker = true;
293            }
294
295            $configId = 'security.firewall.map.config.'.$name;
296
297            list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
298
299            $contextId = 'security.firewall.map.context.'.$name;
300            $context = $container->setDefinition($contextId, new ChildDefinition('security.firewall.context'));
301            $context
302                ->replaceArgument(0, new IteratorArgument($listeners))
303                ->replaceArgument(1, $exceptionListener)
304                ->replaceArgument(2, $logoutListener)
305                ->replaceArgument(3, new Reference($configId))
306            ;
307
308            $contextRefs[$contextId] = new Reference($contextId);
309            $map[$contextId] = $matcher;
310        }
311        $mapDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $contextRefs));
312        $mapDef->replaceArgument(1, new IteratorArgument($map));
313
314        // add authentication providers to authentication manager
315        $authenticationProviders = array_map(function ($id) {
316            return new Reference($id);
317        }, array_values(array_unique($authenticationProviders)));
318        $container
319            ->getDefinition('security.authentication.manager')
320            ->replaceArgument(0, new IteratorArgument($authenticationProviders))
321        ;
322
323        // register an autowire alias for the UserCheckerInterface if no custom user checker service is configured
324        if (!$customUserChecker) {
325            $container->setAlias('Symfony\Component\Security\Core\User\UserCheckerInterface', new Alias('security.user_checker', false));
326        }
327    }
328
329    private function createFirewall(ContainerBuilder $container, $id, $firewall, &$authenticationProviders, $providerIds, $configId)
330    {
331        $config = $container->setDefinition($configId, new ChildDefinition('security.firewall.config'));
332        $config->replaceArgument(0, $id);
333        $config->replaceArgument(1, $firewall['user_checker']);
334
335        // Matcher
336        $matcher = null;
337        if (isset($firewall['request_matcher'])) {
338            $matcher = new Reference($firewall['request_matcher']);
339        } elseif (isset($firewall['pattern']) || isset($firewall['host'])) {
340            $pattern = isset($firewall['pattern']) ? $firewall['pattern'] : null;
341            $host = isset($firewall['host']) ? $firewall['host'] : null;
342            $methods = isset($firewall['methods']) ? $firewall['methods'] : [];
343            $matcher = $this->createRequestMatcher($container, $pattern, $host, $methods);
344        }
345
346        $config->replaceArgument(2, $matcher ? (string) $matcher : null);
347        $config->replaceArgument(3, $firewall['security']);
348
349        // Security disabled?
350        if (false === $firewall['security']) {
351            return [$matcher, [], null, null];
352        }
353
354        $config->replaceArgument(4, $firewall['stateless']);
355
356        // Provider id (take the first registered provider if none defined)
357        $defaultProvider = null;
358        if (isset($firewall['provider'])) {
359            if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall['provider'])])) {
360                throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall['provider']));
361            }
362            $defaultProvider = $providerIds[$normalizedName];
363        } elseif (1 === \count($providerIds)) {
364            $defaultProvider = reset($providerIds);
365        }
366
367        $config->replaceArgument(5, $defaultProvider);
368
369        // Register listeners
370        $listeners = [];
371        $listenerKeys = [];
372
373        // Channel listener
374        $listeners[] = new Reference('security.channel_listener');
375
376        $contextKey = null;
377        $contextListenerId = null;
378        // Context serializer listener
379        if (false === $firewall['stateless']) {
380            $contextKey = $id;
381            if (isset($firewall['context'])) {
382                $contextKey = $firewall['context'];
383            }
384
385            if (!$logoutOnUserChange = $firewall['logout_on_user_change']) {
386                @trigger_error(sprintf('Not setting "logout_on_user_change" to true on firewall "%s" is deprecated as of 3.4, it will always be true in 4.0.', $id), \E_USER_DEPRECATED);
387            }
388
389            if (isset($this->logoutOnUserChangeByContextKey[$contextKey]) && $this->logoutOnUserChangeByContextKey[$contextKey][1] !== $logoutOnUserChange) {
390                throw new InvalidConfigurationException(sprintf('Firewalls "%s" and "%s" need to have the same value for option "logout_on_user_change" as they are sharing the context "%s".', $this->logoutOnUserChangeByContextKey[$contextKey][0], $id, $contextKey));
391            }
392
393            $this->logoutOnUserChangeByContextKey[$contextKey] = [$id, $logoutOnUserChange];
394            $listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey, $logoutOnUserChange));
395            $sessionStrategyId = 'security.authentication.session_strategy';
396        } else {
397            $this->statelessFirewallKeys[] = $id;
398            $sessionStrategyId = 'security.authentication.session_strategy_noop';
399        }
400        $container->setAlias(new Alias('security.authentication.session_strategy.'.$id, false), $sessionStrategyId);
401
402        $config->replaceArgument(6, $contextKey);
403
404        // Logout listener
405        $logoutListenerId = null;
406        if (isset($firewall['logout'])) {
407            $logoutListenerId = 'security.logout_listener.'.$id;
408            $logoutListener = $container->setDefinition($logoutListenerId, new ChildDefinition('security.logout_listener'));
409            $logoutListener->replaceArgument(3, [
410                'csrf_parameter' => $firewall['logout']['csrf_parameter'],
411                'csrf_token_id' => $firewall['logout']['csrf_token_id'],
412                'logout_path' => $firewall['logout']['path'],
413            ]);
414
415            // add logout success handler
416            if (isset($firewall['logout']['success_handler'])) {
417                $logoutSuccessHandlerId = $firewall['logout']['success_handler'];
418            } else {
419                $logoutSuccessHandlerId = 'security.logout.success_handler.'.$id;
420                $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new ChildDefinition('security.logout.success_handler'));
421                $logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']);
422            }
423            $logoutListener->replaceArgument(2, new Reference($logoutSuccessHandlerId));
424
425            // add CSRF provider
426            if (isset($firewall['logout']['csrf_token_generator'])) {
427                $logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_generator']));
428            }
429
430            // add session logout handler
431            if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
432                $logoutListener->addMethodCall('addHandler', [new Reference('security.logout.handler.session')]);
433            }
434
435            // add cookie logout handler
436            if (\count($firewall['logout']['delete_cookies']) > 0) {
437                $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id;
438                $cookieHandler = $container->setDefinition($cookieHandlerId, new ChildDefinition('security.logout.handler.cookie_clearing'));
439                $cookieHandler->addArgument($firewall['logout']['delete_cookies']);
440
441                $logoutListener->addMethodCall('addHandler', [new Reference($cookieHandlerId)]);
442            }
443
444            // add custom handlers
445            foreach ($firewall['logout']['handlers'] as $handlerId) {
446                $logoutListener->addMethodCall('addHandler', [new Reference($handlerId)]);
447            }
448
449            // register with LogoutUrlGenerator
450            $container
451                ->getDefinition('security.logout_url_generator')
452                ->addMethodCall('registerListener', [
453                    $id,
454                    $firewall['logout']['path'],
455                    $firewall['logout']['csrf_token_id'],
456                    $firewall['logout']['csrf_parameter'],
457                    isset($firewall['logout']['csrf_token_generator']) ? new Reference($firewall['logout']['csrf_token_generator']) : null,
458                    false === $firewall['stateless'] && isset($firewall['context']) ? $firewall['context'] : null,
459                ])
460            ;
461        }
462
463        // Determine default entry point
464        $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null;
465
466        // Authentication listeners
467        list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId);
468
469        $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint);
470
471        $listeners = array_merge($listeners, $authListeners);
472
473        // Switch user listener
474        if (isset($firewall['switch_user'])) {
475            $listenerKeys[] = 'switch_user';
476            $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider, $firewall['stateless'], $providerIds));
477        }
478
479        // Access listener
480        $listeners[] = new Reference('security.access_listener');
481
482        // Exception listener
483        $exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless']));
484
485        $config->replaceArgument(8, isset($firewall['access_denied_handler']) ? $firewall['access_denied_handler'] : null);
486        $config->replaceArgument(9, isset($firewall['access_denied_url']) ? $firewall['access_denied_url'] : null);
487
488        $container->setAlias('security.user_checker.'.$id, new Alias($firewall['user_checker'], false));
489
490        foreach ($this->factories as $position) {
491            foreach ($position as $factory) {
492                $key = str_replace('-', '_', $factory->getKey());
493                if (\array_key_exists($key, $firewall)) {
494                    $listenerKeys[] = $key;
495                }
496            }
497        }
498
499        if (isset($firewall['anonymous'])) {
500            $listenerKeys[] = 'anonymous';
501        }
502
503        $config->replaceArgument(10, $listenerKeys);
504        $config->replaceArgument(11, isset($firewall['switch_user']) ? $firewall['switch_user'] : null);
505
506        return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null];
507    }
508
509    private function createContextListener($container, $contextKey, $logoutUserOnChange)
510    {
511        if (isset($this->contextListeners[$contextKey])) {
512            return $this->contextListeners[$contextKey];
513        }
514
515        $listenerId = 'security.context_listener.'.\count($this->contextListeners);
516        $listener = $container->setDefinition($listenerId, new ChildDefinition('security.context_listener'));
517        $listener->replaceArgument(2, $contextKey);
518        $listener->addMethodCall('setLogoutOnUserChange', [$logoutUserOnChange]);
519
520        return $this->contextListeners[$contextKey] = $listenerId;
521    }
522
523    private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider, array $providerIds, $defaultEntryPoint, $contextListenerId = null)
524    {
525        $listeners = [];
526        $hasListeners = false;
527
528        foreach ($this->listenerPositions as $position) {
529            foreach ($this->factories[$position] as $factory) {
530                $key = str_replace('-', '_', $factory->getKey());
531
532                if (isset($firewall[$key])) {
533                    if (isset($firewall[$key]['provider'])) {
534                        if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$key]['provider'])])) {
535                            throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$key]['provider']));
536                        }
537                        $userProvider = $providerIds[$normalizedName];
538                    } elseif ('remember_me' === $key) {
539                        // RememberMeFactory will use the firewall secret when created
540                        $userProvider = null;
541                        if ($contextListenerId) {
542                            $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']);
543                        }
544                    } else {
545                        $userProvider = $defaultProvider ?: $this->getFirstProvider($id, $key, $providerIds);
546                    }
547
548                    list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);
549
550                    $listeners[] = new Reference($listenerId);
551                    $authenticationProviders[] = $provider;
552                    $hasListeners = true;
553                }
554            }
555        }
556
557        // Anonymous
558        if (isset($firewall['anonymous'])) {
559            if (null === $firewall['anonymous']['secret']) {
560                $firewall['anonymous']['secret'] = new Parameter('container.build_hash');
561            }
562
563            $listenerId = 'security.authentication.listener.anonymous.'.$id;
564            $container
565                ->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.anonymous'))
566                ->replaceArgument(1, $firewall['anonymous']['secret'])
567            ;
568
569            $listeners[] = new Reference($listenerId);
570
571            $providerId = 'security.authentication.provider.anonymous.'.$id;
572            $container
573                ->setDefinition($providerId, new ChildDefinition('security.authentication.provider.anonymous'))
574                ->replaceArgument(0, $firewall['anonymous']['secret'])
575            ;
576
577            $authenticationProviders[] = $providerId;
578            $hasListeners = true;
579        }
580
581        if (false === $hasListeners) {
582            throw new InvalidConfigurationException(sprintf('No authentication listener registered for firewall "%s".', $id));
583        }
584
585        return [$listeners, $defaultEntryPoint];
586    }
587
588    private function createEncoders($encoders, ContainerBuilder $container)
589    {
590        $encoderMap = [];
591        foreach ($encoders as $class => $encoder) {
592            $encoderMap[$class] = $this->createEncoder($encoder);
593        }
594
595        $container
596            ->getDefinition('security.encoder_factory.generic')
597            ->setArguments([$encoderMap])
598        ;
599    }
600
601    private function createEncoder($config)
602    {
603        // a custom encoder service
604        if (isset($config['id'])) {
605            return new Reference($config['id']);
606        }
607
608        // plaintext encoder
609        if ('plaintext' === $config['algorithm']) {
610            $arguments = [$config['ignore_case']];
611
612            return [
613                'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder',
614                'arguments' => $arguments,
615            ];
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
631        // bcrypt encoder
632        if ('bcrypt' === $config['algorithm']) {
633            return [
634                'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder',
635                'arguments' => [$config['cost']],
636            ];
637        }
638
639        // Argon2i encoder
640        if ('argon2i' === $config['algorithm']) {
641            if (!Argon2iPasswordEncoder::isSupported()) {
642                throw new InvalidConfigurationException('Argon2i algorithm is not supported. Please install the libsodium extension or upgrade to PHP 7.2+.');
643            }
644
645            return [
646                'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder',
647                'arguments' => [],
648            ];
649        }
650
651        // run-time configured encoder
652        return $config;
653    }
654
655    // Parses user providers and returns an array of their ids
656    private function createUserProviders($config, ContainerBuilder $container)
657    {
658        $providerIds = [];
659        foreach ($config['providers'] as $name => $provider) {
660            $id = $this->createUserDaoProvider($name, $provider, $container);
661            $providerIds[str_replace('-', '_', $name)] = $id;
662        }
663
664        return $providerIds;
665    }
666
667    // Parses a <provider> tag and returns the id for the related user provider service
668    private function createUserDaoProvider($name, $provider, ContainerBuilder $container)
669    {
670        $name = $this->getUserProviderId($name);
671
672        // Doctrine Entity and In-memory DAO provider are managed by factories
673        foreach ($this->userProviderFactories as $factory) {
674            $key = str_replace('-', '_', $factory->getKey());
675
676            if (!empty($provider[$key])) {
677                $factory->create($container, $name, $provider[$key]);
678
679                return $name;
680            }
681        }
682
683        // Existing DAO service provider
684        if (isset($provider['id'])) {
685            $container->setAlias($name, new Alias($provider['id'], false));
686
687            return $provider['id'];
688        }
689
690        // Chain provider
691        if (isset($provider['chain'])) {
692            $providers = [];
693            foreach ($provider['chain']['providers'] as $providerName) {
694                $providers[] = new Reference($this->getUserProviderId($providerName));
695            }
696
697            $container
698                ->setDefinition($name, new ChildDefinition('security.user.provider.chain'))
699                ->addArgument(new IteratorArgument($providers));
700
701            return $name;
702        }
703
704        throw new InvalidConfigurationException(sprintf('Unable to create definition for "%s" user provider.', $name));
705    }
706
707    private function getUserProviderId($name)
708    {
709        return 'security.user.provider.concrete.'.strtolower($name);
710    }
711
712    private function createExceptionListener($container, $config, $id, $defaultEntryPoint, $stateless)
713    {
714        $exceptionListenerId = 'security.exception_listener.'.$id;
715        $listener = $container->setDefinition($exceptionListenerId, new ChildDefinition('security.exception_listener'));
716        $listener->replaceArgument(3, $id);
717        $listener->replaceArgument(4, null === $defaultEntryPoint ? null : new Reference($defaultEntryPoint));
718        $listener->replaceArgument(8, $stateless);
719
720        // access denied handler setup
721        if (isset($config['access_denied_handler'])) {
722            $listener->replaceArgument(6, new Reference($config['access_denied_handler']));
723        } elseif (isset($config['access_denied_url'])) {
724            $listener->replaceArgument(5, $config['access_denied_url']);
725        }
726
727        return $exceptionListenerId;
728    }
729
730    private function createSwitchUserListener($container, $id, $config, $defaultProvider, $stateless, $providerIds)
731    {
732        $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : ($defaultProvider ?: $this->getFirstProvider($id, 'switch_user', $providerIds));
733
734        // in 4.0, ignore the `switch_user.stateless` key if $stateless is `true`
735        if ($stateless && false === $config['stateless']) {
736            @trigger_error(sprintf('Firewall "%s" is configured as "stateless" but the "switch_user.stateless" key is set to false. Both should have the same value, the firewall\'s "stateless" value will be used as default value for the "switch_user.stateless" key in 4.0.', $id), \E_USER_DEPRECATED);
737        }
738
739        $switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
740        $listener = $container->setDefinition($switchUserListenerId, new ChildDefinition('security.authentication.switchuser_listener'));
741        $listener->replaceArgument(1, new Reference($userProvider));
742        $listener->replaceArgument(2, new Reference('security.user_checker.'.$id));
743        $listener->replaceArgument(3, $id);
744        $listener->replaceArgument(6, $config['parameter']);
745        $listener->replaceArgument(7, $config['role']);
746        $listener->replaceArgument(9, $config['stateless']);
747
748        return $switchUserListenerId;
749    }
750
751    private function createExpression($container, $expression)
752    {
753        if (isset($this->expressions[$id = 'security.expression.'.ContainerBuilder::hash($expression)])) {
754            return $this->expressions[$id];
755        }
756
757        $container
758            ->register($id, 'Symfony\Component\ExpressionLanguage\SerializedParsedExpression')
759            ->setPublic(false)
760            ->addArgument($expression)
761            ->addArgument(serialize($this->getExpressionLanguage()->parse($expression, ['token', 'user', 'object', 'roles', 'request', 'trust_resolver'])->getNodes()))
762        ;
763
764        return $this->expressions[$id] = new Reference($id);
765    }
766
767    private function createRequestMatcher($container, $path = null, $host = null, $methods = [], $ip = null, array $attributes = [])
768    {
769        if ($methods) {
770            $methods = array_map('strtoupper', (array) $methods);
771        }
772
773        $id = 'security.request_matcher.'.ContainerBuilder::hash([$path, $host, $methods, $ip, $attributes]);
774
775        if (isset($this->requestMatchers[$id])) {
776            return $this->requestMatchers[$id];
777        }
778
779        // only add arguments that are necessary
780        $arguments = [$path, $host, $methods, $ip, $attributes];
781        while (\count($arguments) > 0 && !end($arguments)) {
782            array_pop($arguments);
783        }
784
785        $container
786            ->register($id, 'Symfony\Component\HttpFoundation\RequestMatcher')
787            ->setPublic(false)
788            ->setArguments($arguments)
789        ;
790
791        return $this->requestMatchers[$id] = new Reference($id);
792    }
793
794    public function addSecurityListenerFactory(SecurityFactoryInterface $factory)
795    {
796        $this->factories[$factory->getPosition()][] = $factory;
797    }
798
799    public function addUserProviderFactory(UserProviderFactoryInterface $factory)
800    {
801        $this->userProviderFactories[] = $factory;
802    }
803
804    /**
805     * {@inheritdoc}
806     */
807    public function getXsdValidationBasePath()
808    {
809        return __DIR__.'/../Resources/config/schema';
810    }
811
812    public function getNamespace()
813    {
814        return 'http://symfony.com/schema/dic/security';
815    }
816
817    public function getConfiguration(array $config, ContainerBuilder $container)
818    {
819        // first assemble the factories
820        return new MainConfiguration($this->factories, $this->userProviderFactories);
821    }
822
823    private function getExpressionLanguage()
824    {
825        if (null === $this->expressionLanguage) {
826            if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
827                throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
828            }
829            $this->expressionLanguage = new ExpressionLanguage();
830        }
831
832        return $this->expressionLanguage;
833    }
834
835    /**
836     * @deprecated since version 3.4, to be removed in 4.0
837     */
838    private function getFirstProvider($firewallName, $listenerName, array $providerIds)
839    {
840        @trigger_error(sprintf('Listener "%s" on firewall "%s" has no "provider" set but multiple providers exist. Using the first configured provider (%s) is deprecated since Symfony 3.4 and will throw an exception in 4.0, set the "provider" key on the firewall instead.', $listenerName, $firewallName, $first = array_keys($providerIds)[0]), \E_USER_DEPRECATED);
841
842        return $providerIds[$first];
843    }
844}
845