LoginLinkHandler.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  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\LoginLink;
  11. use Symfony\Component\HttpFoundation\Request;
  12. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  13. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  14. use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
  15. use Symfony\Component\Security\Core\User\UserInterface;
  16. use Symfony\Component\Security\Core\User\UserProviderInterface;
  17. use Symfony\Component\Security\Http\LoginLink\Exception\ExpiredLoginLinkException;
  18. use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkException;
  19. /**
  20. * @author Ryan Weaver <ryan@symfonycasts.com>
  21. * @experimental in 5.2
  22. */
  23. final class LoginLinkHandler implements LoginLinkHandlerInterface
  24. {
  25. private $urlGenerator;
  26. private $userProvider;
  27. private $propertyAccessor;
  28. private $signatureProperties;
  29. private $secret;
  30. private $options;
  31. private $expiredStorage;
  32. public function __construct(UrlGeneratorInterface $urlGenerator, UserProviderInterface $userProvider, PropertyAccessorInterface $propertyAccessor, array $signatureProperties, string $secret, array $options, ?ExpiredLoginLinkStorage $expiredStorage)
  33. {
  34. $this->urlGenerator = $urlGenerator;
  35. $this->userProvider = $userProvider;
  36. $this->propertyAccessor = $propertyAccessor;
  37. $this->signatureProperties = $signatureProperties;
  38. $this->secret = $secret;
  39. $this->options = array_merge([
  40. 'route_name' => null,
  41. 'lifetime' => 600,
  42. 'max_uses' => null,
  43. ], $options);
  44. $this->expiredStorage = $expiredStorage;
  45. }
  46. public function createLoginLink(UserInterface $user): LoginLinkDetails
  47. {
  48. $expiresAt = new \DateTimeImmutable(sprintf('+%d seconds', $this->options['lifetime']));
  49. $expires = $expiresAt->format('U');
  50. $parameters = [
  51. 'user' => $user->getUsername(),
  52. 'expires' => $expires,
  53. 'hash' => $this->computeSignatureHash($user, $expires),
  54. ];
  55. $url = $this->urlGenerator->generate(
  56. $this->options['route_name'],
  57. $parameters,
  58. UrlGeneratorInterface::ABSOLUTE_URL
  59. );
  60. return new LoginLinkDetails($url, $expiresAt);
  61. }
  62. public function consumeLoginLink(Request $request): UserInterface
  63. {
  64. $username = $request->get('user');
  65. try {
  66. $user = $this->userProvider->loadUserByUsername($username);
  67. } catch (UsernameNotFoundException $exception) {
  68. throw new InvalidLoginLinkException('User not found.', 0, $exception);
  69. }
  70. $hash = $request->get('hash');
  71. $expires = $request->get('expires');
  72. if (false === hash_equals($hash, $this->computeSignatureHash($user, $expires))) {
  73. throw new InvalidLoginLinkException('Invalid or expired signature.');
  74. }
  75. if ($expires < time()) {
  76. throw new ExpiredLoginLinkException('Login link has expired.');
  77. }
  78. if ($this->expiredStorage && $this->options['max_uses']) {
  79. $hash = $request->get('hash');
  80. if ($this->expiredStorage->countUsages($hash) >= $this->options['max_uses']) {
  81. throw new ExpiredLoginLinkException(sprintf('Login link can only be used "%d" times.', $this->options['max_uses']));
  82. }
  83. $this->expiredStorage->incrementUsages($hash);
  84. }
  85. return $user;
  86. }
  87. private function computeSignatureHash(UserInterface $user, int $expires): string
  88. {
  89. $signatureFields = [base64_encode($user->getUsername()), $expires];
  90. foreach ($this->signatureProperties as $property) {
  91. $value = $this->propertyAccessor->getValue($user, $property) ?? '';
  92. if ($value instanceof \DateTimeInterface) {
  93. $value = $value->format('c');
  94. }
  95. if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
  96. throw new \InvalidArgumentException(sprintf('The property path "%s" on the user object "%s" must return a value that can be cast to a string, but "%s" was returned.', $property, \get_class($user), get_debug_type($value)));
  97. }
  98. $signatureFields[] = base64_encode($value);
  99. }
  100. return base64_encode(hash_hmac('sha256', implode(':', $signatureFields), $this->secret));
  101. }
  102. }