PersistentTokenBasedRememberMeServices.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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 Symfony\Component\HttpFoundation\Cookie;
  12. use Symfony\Component\HttpFoundation\Request;
  13. use Symfony\Component\HttpFoundation\Response;
  14. use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
  15. use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface;
  16. use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
  17. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  18. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  19. use Symfony\Component\Security\Core\Exception\CookieTheftException;
  20. /**
  21. * Concrete implementation of the RememberMeServicesInterface which needs
  22. * an implementation of TokenProviderInterface for providing remember-me
  23. * capabilities.
  24. *
  25. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  26. */
  27. class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices
  28. {
  29. private const HASHED_TOKEN_PREFIX = 'sha256_';
  30. /** @var TokenProviderInterface */
  31. private $tokenProvider;
  32. public function setTokenProvider(TokenProviderInterface $tokenProvider)
  33. {
  34. $this->tokenProvider = $tokenProvider;
  35. }
  36. /**
  37. * {@inheritdoc}
  38. */
  39. protected function cancelCookie(Request $request)
  40. {
  41. // Delete cookie on the client
  42. parent::cancelCookie($request);
  43. // Delete cookie from the tokenProvider
  44. if (null !== ($cookie = $request->cookies->get($this->options['name']))
  45. && 2 === \count($parts = $this->decodeCookie($cookie))
  46. ) {
  47. [$series] = $parts;
  48. $this->tokenProvider->deleteTokenBySeries($series);
  49. }
  50. }
  51. /**
  52. * {@inheritdoc}
  53. */
  54. protected function processAutoLoginCookie(array $cookieParts, Request $request)
  55. {
  56. if (2 !== \count($cookieParts)) {
  57. throw new AuthenticationException('The cookie is invalid.');
  58. }
  59. [$series, $tokenValue] = $cookieParts;
  60. $persistentToken = $this->tokenProvider->loadTokenBySeries($series);
  61. if (!$this->isTokenValueValid($persistentToken, $tokenValue)) {
  62. throw new CookieTheftException('This token was already used. The account is possibly compromised.');
  63. }
  64. if ($persistentToken->getLastUsed()->getTimestamp() + $this->options['lifetime'] < time()) {
  65. throw new AuthenticationException('The cookie has expired.');
  66. }
  67. $tokenValue = base64_encode(random_bytes(64));
  68. $this->tokenProvider->updateToken($series, $this->generateHash($tokenValue), new \DateTime());
  69. $request->attributes->set(self::COOKIE_ATTR_NAME,
  70. new Cookie(
  71. $this->options['name'],
  72. $this->encodeCookie([$series, $tokenValue]),
  73. time() + $this->options['lifetime'],
  74. $this->options['path'],
  75. $this->options['domain'],
  76. $this->options['secure'] ?? $request->isSecure(),
  77. $this->options['httponly'],
  78. false,
  79. $this->options['samesite']
  80. )
  81. );
  82. return $this->getUserProvider($persistentToken->getClass())->loadUserByUsername($persistentToken->getUsername());
  83. }
  84. /**
  85. * {@inheritdoc}
  86. */
  87. protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token)
  88. {
  89. $series = base64_encode(random_bytes(64));
  90. $tokenValue = base64_encode(random_bytes(64));
  91. $this->tokenProvider->createNewToken(
  92. new PersistentToken(
  93. \get_class($user = $token->getUser()),
  94. $user->getUsername(),
  95. $series,
  96. $this->generateHash($tokenValue),
  97. new \DateTime()
  98. )
  99. );
  100. $response->headers->setCookie(
  101. new Cookie(
  102. $this->options['name'],
  103. $this->encodeCookie([$series, $tokenValue]),
  104. time() + $this->options['lifetime'],
  105. $this->options['path'],
  106. $this->options['domain'],
  107. $this->options['secure'] ?? $request->isSecure(),
  108. $this->options['httponly'],
  109. false,
  110. $this->options['samesite']
  111. )
  112. );
  113. }
  114. private function generateHash(string $tokenValue): string
  115. {
  116. return self::HASHED_TOKEN_PREFIX.hash_hmac('sha256', $tokenValue, $this->getSecret());
  117. }
  118. private function isTokenValueValid(PersistentTokenInterface $persistentToken, string $tokenValue): bool
  119. {
  120. if (0 === strpos($persistentToken->getTokenValue(), self::HASHED_TOKEN_PREFIX)) {
  121. return hash_equals($persistentToken->getTokenValue(), $this->generateHash($tokenValue));
  122. }
  123. return hash_equals($persistentToken->getTokenValue(), $tokenValue);
  124. }
  125. }