RememberMeFactory.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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\Security\Factory;
  11. use Symfony\Component\Config\Definition\Builder\NodeDefinition;
  12. use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
  13. use Symfony\Component\DependencyInjection\ChildDefinition;
  14. use Symfony\Component\DependencyInjection\ContainerBuilder;
  15. use Symfony\Component\DependencyInjection\Definition;
  16. use Symfony\Component\DependencyInjection\Reference;
  17. use Symfony\Component\HttpFoundation\Cookie;
  18. use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;
  19. /**
  20. * @internal
  21. */
  22. class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
  23. {
  24. protected $options = [
  25. 'name' => 'REMEMBERME',
  26. 'lifetime' => 31536000,
  27. 'path' => '/',
  28. 'domain' => null,
  29. 'secure' => false,
  30. 'httponly' => true,
  31. 'samesite' => null,
  32. 'always_remember_me' => false,
  33. 'remember_me_parameter' => '_remember_me',
  34. ];
  35. public function create(ContainerBuilder $container, string $id, array $config, ?string $userProvider, ?string $defaultEntryPoint)
  36. {
  37. // authentication provider
  38. $authProviderId = 'security.authentication.provider.rememberme.'.$id;
  39. $container
  40. ->setDefinition($authProviderId, new ChildDefinition('security.authentication.provider.rememberme'))
  41. ->replaceArgument(0, new Reference('security.user_checker.'.$id))
  42. ->addArgument($config['secret'])
  43. ->addArgument($id)
  44. ;
  45. // remember me services
  46. $templateId = $this->generateRememberMeServicesTemplateId($config, $id);
  47. $rememberMeServicesId = $templateId.'.'.$id;
  48. // attach to remember-me aware listeners
  49. $userProviders = [];
  50. foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) {
  51. foreach ($attributes as $attribute) {
  52. if (!isset($attribute['id']) || $attribute['id'] !== $id) {
  53. continue;
  54. }
  55. if (!isset($attribute['provider'])) {
  56. throw new \RuntimeException('Each "security.remember_me_aware" tag must have a provider attribute.');
  57. }
  58. // context listeners don't need a provider
  59. if ('none' !== $attribute['provider']) {
  60. $userProviders[] = new Reference($attribute['provider']);
  61. }
  62. $container
  63. ->getDefinition($serviceId)
  64. ->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)])
  65. ;
  66. }
  67. }
  68. $this->createRememberMeServices($container, $id, $templateId, $userProviders, $config);
  69. // remember-me listener
  70. $listenerId = 'security.authentication.listener.rememberme.'.$id;
  71. $listener = $container->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.rememberme'));
  72. $listener->replaceArgument(1, new Reference($rememberMeServicesId));
  73. $listener->replaceArgument(5, $config['catch_exceptions']);
  74. // remember-me logout listener
  75. $container->setDefinition('security.logout.listener.remember_me.'.$id, new Definition(RememberMeLogoutListener::class))
  76. ->addArgument(new Reference($rememberMeServicesId))
  77. ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id]);
  78. return [$authProviderId, $listenerId, $defaultEntryPoint];
  79. }
  80. public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
  81. {
  82. $templateId = $this->generateRememberMeServicesTemplateId($config, $firewallName);
  83. $rememberMeServicesId = $templateId.'.'.$firewallName;
  84. // create remember me services (which manage the remember me cookies)
  85. $this->createRememberMeServices($container, $firewallName, $templateId, [new Reference($userProviderId)], $config);
  86. // create remember me listener (which executes the remember me services for other authenticators and logout)
  87. $this->createRememberMeListener($container, $firewallName, $rememberMeServicesId);
  88. // create remember me authenticator (which re-authenticates the user based on the remember me cookie)
  89. $authenticatorId = 'security.authenticator.remember_me.'.$firewallName;
  90. $container
  91. ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me'))
  92. ->replaceArgument(0, new Reference($rememberMeServicesId))
  93. ->replaceArgument(3, $container->getDefinition($rememberMeServicesId)->getArgument(3))
  94. ;
  95. foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) {
  96. // register ContextListener
  97. if ('security.context_listener' === substr($serviceId, 0, 25)) {
  98. $container
  99. ->getDefinition($serviceId)
  100. ->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)])
  101. ;
  102. continue;
  103. }
  104. throw new \LogicException(sprintf('Symfony Authenticator Security dropped support for the "security.remember_me_aware" tag, service "%s" will no longer work as expected.', $serviceId));
  105. }
  106. return $authenticatorId;
  107. }
  108. public function getPosition()
  109. {
  110. return 'remember_me';
  111. }
  112. public function getKey()
  113. {
  114. return 'remember-me';
  115. }
  116. public function addConfiguration(NodeDefinition $node)
  117. {
  118. $builder = $node
  119. ->fixXmlConfig('user_provider')
  120. ->children()
  121. ;
  122. $builder
  123. ->scalarNode('secret')->isRequired()->cannotBeEmpty()->end()
  124. ->scalarNode('service')->end()
  125. ->scalarNode('token_provider')->end()
  126. ->arrayNode('user_providers')
  127. ->beforeNormalization()
  128. ->ifString()->then(function ($v) { return [$v]; })
  129. ->end()
  130. ->prototype('scalar')->end()
  131. ->end()
  132. ->booleanNode('catch_exceptions')->defaultTrue()->end()
  133. ;
  134. foreach ($this->options as $name => $value) {
  135. if ('secure' === $name) {
  136. $builder->enumNode($name)->values([true, false, 'auto'])->defaultValue('auto' === $value ? null : $value);
  137. } elseif ('samesite' === $name) {
  138. $builder->enumNode($name)->values([null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT, Cookie::SAMESITE_NONE])->defaultValue($value);
  139. } elseif (\is_bool($value)) {
  140. $builder->booleanNode($name)->defaultValue($value);
  141. } elseif (\is_int($value)) {
  142. $builder->integerNode($name)->defaultValue($value);
  143. } else {
  144. $builder->scalarNode($name)->defaultValue($value);
  145. }
  146. }
  147. }
  148. private function generateRememberMeServicesTemplateId(array $config, string $id): string
  149. {
  150. if (isset($config['service'])) {
  151. return $config['service'];
  152. }
  153. if (isset($config['token_provider'])) {
  154. return 'security.authentication.rememberme.services.persistent';
  155. }
  156. return 'security.authentication.rememberme.services.simplehash';
  157. }
  158. private function createRememberMeServices(ContainerBuilder $container, string $id, string $templateId, array $userProviders, array $config): void
  159. {
  160. $rememberMeServicesId = $templateId.'.'.$id;
  161. $rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId));
  162. $rememberMeServices->replaceArgument(1, $config['secret']);
  163. $rememberMeServices->replaceArgument(2, $id);
  164. if (isset($config['token_provider'])) {
  165. $rememberMeServices->addMethodCall('setTokenProvider', [
  166. new Reference($config['token_provider']),
  167. ]);
  168. }
  169. // remember-me options
  170. $mergedOptions = array_intersect_key($config, $this->options);
  171. if ('auto' === $mergedOptions['secure']) {
  172. $mergedOptions['secure'] = null;
  173. }
  174. $rememberMeServices->replaceArgument(3, $mergedOptions);
  175. if ($config['user_providers']) {
  176. $userProviders = [];
  177. foreach ($config['user_providers'] as $providerName) {
  178. $userProviders[] = new Reference('security.user.provider.concrete.'.$providerName);
  179. }
  180. }
  181. if (0 === \count($userProviders)) {
  182. throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.');
  183. }
  184. $rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders)));
  185. }
  186. private function createRememberMeListener(ContainerBuilder $container, string $id, string $rememberMeServicesId): void
  187. {
  188. $container
  189. ->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me'))
  190. ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id])
  191. ->replaceArgument(0, new Reference($rememberMeServicesId))
  192. ;
  193. $container
  194. ->setDefinition('security.logout.listener.remember_me.'.$id, new Definition(RememberMeLogoutListener::class))
  195. ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id])
  196. ->addArgument(new Reference($rememberMeServicesId));
  197. }
  198. }