vendor/symfony/security-http/RememberMe/AbstractRememberMeServices.php line 29

Open in your IDE?
  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\Component\Security\Http\RememberMe;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\HttpFoundation\Cookie;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpFoundation\Response;
  15. use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
  16. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  17. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  18. use Symfony\Component\Security\Core\Exception\CookieTheftException;
  19. use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
  20. use Symfony\Component\Security\Core\Exception\UserNotFoundException;
  21. use Symfony\Component\Security\Core\User\UserInterface;
  22. use Symfony\Component\Security\Core\User\UserProviderInterface;
  23. use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
  24. use Symfony\Component\Security\Http\ParameterBagUtils;
  25. trigger_deprecation('symfony/security-http', '5.4', 'The "%s" class is deprecated, use "%s" instead.', AbstractRememberMeServices::class, AbstractRememberMeHandler::class);
  26. /**
  27. * Base class implementing the RememberMeServicesInterface.
  28. *
  29. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  30. *
  31. * @deprecated since Symfony 5.4, use {@see AbstractRememberMeHandler} instead
  32. */
  33. abstract class AbstractRememberMeServices implements RememberMeServicesInterface, LogoutHandlerInterface
  34. {
  35. public const COOKIE_DELIMITER = ':';
  36. protected $logger;
  37. protected $options = [
  38. 'secure' => false,
  39. 'httponly' => true,
  40. 'samesite' => null,
  41. 'path' => null,
  42. 'domain' => null,
  43. ];
  44. private $firewallName;
  45. private $secret;
  46. private $userProviders;
  47. /**
  48. * @throws \InvalidArgumentException
  49. */
  50. public function __construct(iterable $userProviders, string $secret, string $firewallName, array $options = [], ?LoggerInterface $logger = null)
  51. {
  52. if (empty($secret)) {
  53. throw new \InvalidArgumentException('$secret must not be empty.');
  54. }
  55. if ('' === $firewallName) {
  56. throw new \InvalidArgumentException('$firewallName must not be empty.');
  57. }
  58. if (!\is_array($userProviders) && !$userProviders instanceof \Countable) {
  59. $userProviders = iterator_to_array($userProviders, false);
  60. }
  61. if (0 === \count($userProviders)) {
  62. throw new \InvalidArgumentException('You must provide at least one user provider.');
  63. }
  64. $this->userProviders = $userProviders;
  65. $this->secret = $secret;
  66. $this->firewallName = $firewallName;
  67. $this->options = array_merge($this->options, $options);
  68. $this->logger = $logger;
  69. }
  70. /**
  71. * Returns the parameter that is used for checking whether remember-me
  72. * services have been requested.
  73. *
  74. * @return string
  75. */
  76. public function getRememberMeParameter()
  77. {
  78. return $this->options['remember_me_parameter'];
  79. }
  80. /**
  81. * @return string
  82. */
  83. public function getSecret()
  84. {
  85. return $this->secret;
  86. }
  87. /**
  88. * Implementation of RememberMeServicesInterface. Detects whether a remember-me
  89. * cookie was set, decodes it, and hands it to subclasses for further processing.
  90. *
  91. * @throws CookieTheftException
  92. * @throws \RuntimeException
  93. */
  94. final public function autoLogin(Request $request): ?TokenInterface
  95. {
  96. if (($cookie = $request->attributes->get(self::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) {
  97. return null;
  98. }
  99. if (null === $cookie = $request->cookies->get($this->options['name'])) {
  100. return null;
  101. }
  102. if (null !== $this->logger) {
  103. $this->logger->debug('Remember-me cookie detected.');
  104. }
  105. $cookieParts = $this->decodeCookie($cookie);
  106. try {
  107. $user = $this->processAutoLoginCookie($cookieParts, $request);
  108. if (!$user instanceof UserInterface) {
  109. throw new \RuntimeException('processAutoLoginCookie() must return a UserInterface implementation.');
  110. }
  111. if (null !== $this->logger) {
  112. $this->logger->info('Remember-me cookie accepted.');
  113. }
  114. return new RememberMeToken($user, $this->firewallName, $this->secret);
  115. } catch (CookieTheftException $e) {
  116. $this->loginFail($request, $e);
  117. throw $e;
  118. } catch (UserNotFoundException $e) {
  119. if (null !== $this->logger) {
  120. $this->logger->info('User for remember-me cookie not found.', ['exception' => $e]);
  121. }
  122. $this->loginFail($request, $e);
  123. } catch (UnsupportedUserException $e) {
  124. if (null !== $this->logger) {
  125. $this->logger->warning('User class for remember-me cookie not supported.', ['exception' => $e]);
  126. }
  127. $this->loginFail($request, $e);
  128. } catch (AuthenticationException $e) {
  129. if (null !== $this->logger) {
  130. $this->logger->debug('Remember-Me authentication failed.', ['exception' => $e]);
  131. }
  132. $this->loginFail($request, $e);
  133. } catch (\Exception $e) {
  134. $this->loginFail($request, $e);
  135. throw $e;
  136. }
  137. return null;
  138. }
  139. /**
  140. * Implementation for LogoutHandlerInterface. Deletes the cookie.
  141. */
  142. public function logout(Request $request, Response $response, TokenInterface $token)
  143. {
  144. $this->cancelCookie($request);
  145. }
  146. /**
  147. * Implementation for RememberMeServicesInterface. Deletes the cookie when
  148. * an attempted authentication fails.
  149. */
  150. final public function loginFail(Request $request, ?\Exception $exception = null)
  151. {
  152. $this->cancelCookie($request);
  153. $this->onLoginFail($request, $exception);
  154. }
  155. /**
  156. * Implementation for RememberMeServicesInterface. This is called when an
  157. * authentication is successful.
  158. */
  159. final public function loginSuccess(Request $request, Response $response, TokenInterface $token)
  160. {
  161. // Make sure any old remember-me cookies are cancelled
  162. $this->cancelCookie($request);
  163. if (!$token->getUser() instanceof UserInterface) {
  164. if (null !== $this->logger) {
  165. $this->logger->debug('Remember-me ignores token since it does not contain a UserInterface implementation.');
  166. }
  167. return;
  168. }
  169. if (!$this->isRememberMeRequested($request)) {
  170. if (null !== $this->logger) {
  171. $this->logger->debug('Remember-me was not requested.');
  172. }
  173. return;
  174. }
  175. if (null !== $this->logger) {
  176. $this->logger->debug('Remember-me was requested; setting cookie.');
  177. }
  178. // Remove attribute from request that sets a NULL cookie.
  179. // It was set by $this->cancelCookie()
  180. // (cancelCookie does other things too for some RememberMeServices
  181. // so we should still call it at the start of this method)
  182. $request->attributes->remove(self::COOKIE_ATTR_NAME);
  183. $this->onLoginSuccess($request, $response, $token);
  184. }
  185. /**
  186. * Subclasses should validate the cookie and do any additional processing
  187. * that is required. This is called from autoLogin().
  188. *
  189. * @return UserInterface
  190. */
  191. abstract protected function processAutoLoginCookie(array $cookieParts, Request $request);
  192. protected function onLoginFail(Request $request, ?\Exception $exception = null)
  193. {
  194. }
  195. /**
  196. * This is called after a user has been logged in successfully, and has
  197. * requested remember-me capabilities. The implementation usually sets a
  198. * cookie and possibly stores a persistent record of it.
  199. */
  200. abstract protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token);
  201. final protected function getUserProvider(string $class): UserProviderInterface
  202. {
  203. foreach ($this->userProviders as $provider) {
  204. if ($provider->supportsClass($class)) {
  205. return $provider;
  206. }
  207. }
  208. throw new UnsupportedUserException(sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', $class));
  209. }
  210. /**
  211. * Decodes the raw cookie value.
  212. *
  213. * @return array
  214. */
  215. protected function decodeCookie(string $rawCookie)
  216. {
  217. return explode(self::COOKIE_DELIMITER, base64_decode($rawCookie));
  218. }
  219. /**
  220. * Encodes the cookie parts.
  221. *
  222. * @return string
  223. *
  224. * @throws \InvalidArgumentException When $cookieParts contain the cookie delimiter. Extending class should either remove or escape it.
  225. */
  226. protected function encodeCookie(array $cookieParts)
  227. {
  228. foreach ($cookieParts as $cookiePart) {
  229. if (str_contains($cookiePart, self::COOKIE_DELIMITER)) {
  230. throw new \InvalidArgumentException(sprintf('$cookieParts should not contain the cookie delimiter "%s".', self::COOKIE_DELIMITER));
  231. }
  232. }
  233. return base64_encode(implode(self::COOKIE_DELIMITER, $cookieParts));
  234. }
  235. /**
  236. * Deletes the remember-me cookie.
  237. */
  238. protected function cancelCookie(Request $request)
  239. {
  240. if (null !== $this->logger) {
  241. $this->logger->debug('Clearing remember-me cookie.', ['name' => $this->options['name']]);
  242. }
  243. $request->attributes->set(self::COOKIE_ATTR_NAME, new Cookie($this->options['name'], null, 1, $this->options['path'], $this->options['domain'], $this->options['secure'] ?? $request->isSecure(), $this->options['httponly'], false, $this->options['samesite']));
  244. }
  245. /**
  246. * Checks whether remember-me capabilities were requested.
  247. *
  248. * @return bool
  249. */
  250. protected function isRememberMeRequested(Request $request)
  251. {
  252. if (true === $this->options['always_remember_me']) {
  253. return true;
  254. }
  255. $parameter = ParameterBagUtils::getRequestParameterValue($request, $this->options['remember_me_parameter']);
  256. if (null === $parameter && null !== $this->logger) {
  257. $this->logger->debug('Did not send remember-me cookie.', ['parameter' => $this->options['remember_me_parameter']]);
  258. }
  259. return 'true' === $parameter || 'on' === $parameter || '1' === $parameter || 'yes' === $parameter || true === $parameter;
  260. }
  261. }