From 9b7fddd10c1ded1e19ccb3bd625c178b2128d15f Mon Sep 17 00:00:00 2001 From: Wouter J Date: Sun, 8 Sep 2019 15:55:27 +0200 Subject: [PATCH] Integrated GuardAuthenticationManager in the SecurityBundle --- .../DependencyInjection/MainConfiguration.php | 1 + .../Factory/CustomAuthenticatorFactory.php | 56 +++++++++++++ .../Factory/GuardFactoryInterface.php | 27 ++++++ .../Security/Factory/HttpBasicFactory.php | 13 ++- .../DependencyInjection/SecurityExtension.php | 84 ++++++++++++++----- .../Resources/config/authenticators.xml | 16 ++++ .../Resources/config/security.xml | 11 ++- .../Bundle/SecurityBundle/SecurityBundle.php | 2 + 8 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 15ff8246f787..b0d7e5c342e4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -73,6 +73,7 @@ public function getConfigTreeBuilder() ->booleanNode('hide_user_not_found')->defaultTrue()->end() ->booleanNode('always_authenticate_before_granting')->defaultFalse()->end() ->booleanNode('erase_credentials')->defaultTrue()->end() + ->booleanNode('guard_authentication_manager')->defaultFalse()->end() ->arrayNode('access_decision_manager') ->addDefaultsIfNotSet() ->children() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php new file mode 100644 index 000000000000..43c236fcfaf6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class CustomAuthenticatorFactory implements AuthenticatorFactoryInterface, SecurityFactoryInterface +{ + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + { + throw new \LogicException('Custom authenticators are not supported when "security.enable_authenticator_manager" is not set to true.'); + } + + public function getPosition(): string + { + return 'pre_auth'; + } + + public function getKey(): string + { + return 'custom_authenticator'; + } + + /** + * @param ArrayNodeDefinition $builder + */ + public function addConfiguration(NodeDefinition $builder) + { + $builder + ->fixXmlConfig('service') + ->children() + ->arrayNode('services') + ->info('An array of service ids for all of your "authenticators"') + ->requiresAtLeastOneElement() + ->prototype('scalar')->end() + ->end() + ->end() + ; + } + + public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): array + { + return $config['services']; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php new file mode 100644 index 000000000000..312f73499ab0 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardFactoryInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Wouter de Jong + */ +interface GuardFactoryInterface +{ + /** + * Creates the Guard service(s) for the provided configuration. + * + * @return string|string[] The Guard service ID(s) to be used by the firewall + */ + public function createGuard(ContainerBuilder $container, string $id, array $config, string $userProviderId); +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index f731469520b4..f50698fc67b2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -21,7 +21,7 @@ * * @author Fabien Potencier */ -class HttpBasicFactory implements SecurityFactoryInterface +class HttpBasicFactory implements SecurityFactoryInterface, GuardFactoryInterface { public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) { @@ -46,6 +46,17 @@ public function create(ContainerBuilder $container, string $id, array $config, s return [$provider, $listenerId, $entryPointId]; } + public function createGuard(ContainerBuilder $container, string $id, array $config, string $userProviderId): string + { + $authenticatorId = 'security.authenticator.http_basic.'.$id; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.http_basic')) + ->replaceArgument(0, $config['realm']) + ->replaceArgument(1, new Reference($userProviderId)); + + return $authenticatorId; + } + public function getPosition() { return 'http'; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 924013306522..73b9a55a7cf5 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; @@ -52,6 +53,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface private $userProviderFactories = []; private $statelessFirewallKeys = []; + private $guardAuthenticationManagerEnabled = false; + public function __construct() { foreach ($this->listenerPositions as $position) { @@ -135,6 +138,8 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); + $this->guardAuthenticationManagerEnabled = $config['guard_authentication_manager']; + $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); @@ -258,8 +263,13 @@ private function createFirewalls(array $config, ContainerBuilder $container) $authenticationProviders = array_map(function ($id) { return new Reference($id); }, array_values(array_unique($authenticationProviders))); + $authenticationManagerId = 'security.authentication.manager.provider'; + if ($this->guardAuthenticationManagerEnabled) { + $authenticationManagerId = 'security.authentication.manager.guard'; + $container->setAlias('security.authentication.manager', new Alias($authenticationManagerId)); + } $container - ->getDefinition('security.authentication.manager') + ->getDefinition($authenticationManagerId) ->replaceArgument(0, new IteratorArgument($authenticationProviders)) ; @@ -467,31 +477,27 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri $key = str_replace('-', '_', $factory->getKey()); if (isset($firewall[$key])) { - if (isset($firewall[$key]['provider'])) { - if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$key]['provider'])])) { - throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$key]['provider'])); + $userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds, $contextListenerId); + + if ($this->guardAuthenticationManagerEnabled) { + if (!$factory instanceof GuardFactoryInterface) { + throw new InvalidConfigurationException(sprintf('Cannot configure GuardAuthenticationManager as %s authentication does not support it, set security.guard_authentication_manager to `false`.', $key)); } - $userProvider = $providerIds[$normalizedName]; - } elseif ('remember_me' === $key || 'anonymous' === $key) { - // RememberMeFactory will use the firewall secret when created, AnonymousAuthenticationListener does not load users. - $userProvider = null; - if ('remember_me' === $key && $contextListenerId) { - $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); + $authenticators = $factory->createGuard($container, $id, $firewall[$key], $userProvider); + if (\is_array($authenticators)) { + foreach ($authenticators as $i => $authenticator) { + $authenticationProviders[$id.'_'.$key.$i] = $authenticator; + } + } else { + $authenticationProviders[$id.'_'.$key] = $authenticators; } - } elseif ($defaultProvider) { - $userProvider = $defaultProvider; - } elseif (empty($providerIds)) { - $userProvider = sprintf('security.user.provider.missing.%s', $key); - $container->setDefinition($userProvider, (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)); } else { - 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.', $key, $id)); - } - - list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); + list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); - $listeners[] = new Reference($listenerId); - $authenticationProviders[] = $provider; + $listeners[] = new Reference($listenerId); + $authenticationProviders[] = $provider; + } $hasListeners = true; } } @@ -504,6 +510,42 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri return [$listeners, $defaultEntryPoint]; } + private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): ?string + { + if (isset($firewall[$factoryKey]['provider'])) { + if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) { + throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$factoryKey]['provider'])); + } + + return $providerIds[$normalizedName]; + } + + if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { + if ('remember_me' === $factoryKey && $contextListenerId) { + $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); + } + + // RememberMeFactory will use the firewall secret when created + return null; + } + + if ($defaultProvider) { + return $defaultProvider; + } + + if (!$providerIds) { + $userProvider = sprintf('security.user.provider.missing.%s', $factoryKey); + $container->setDefinition( + $userProvider, + (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id) + ); + + return $userProvider; + } + + 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)); + } + private function createEncoders(array $encoders, ContainerBuilder $container) { $encoderMap = []; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml new file mode 100644 index 000000000000..4022eafd9d8f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/authenticators.xml @@ -0,0 +1,16 @@ + + + + + + realm name + user provider + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 7219210597ee..0992a92499c8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -45,13 +45,22 @@ - + %security.authentication.manager.erase_credentials% + + + + %security.authentication.manager.erase_credentials% + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index b3243c83d7da..d8e6590736a3 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -17,6 +17,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory; @@ -63,6 +64,7 @@ public function build(ContainerBuilder $container) $extension->addSecurityListenerFactory(new RemoteUserFactory()); $extension->addSecurityListenerFactory(new GuardAuthenticationFactory()); $extension->addSecurityListenerFactory(new AnonymousFactory()); + $extension->addSecurityListenerFactory(new CustomAuthenticatorFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); $extension->addUserProviderFactory(new LdapFactory());