UserFormAuthHandler.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. <?php
  2. namespace Michel\Auth\Handler\Authentication;
  3. use Michel\Auth\AuthIdentity;
  4. use Michel\Auth\Exception\AuthenticationException;
  5. use Michel\Auth\Exception\InvalidCredentialsException;
  6. use Michel\Auth\Exception\LogoutException;
  7. use Michel\Auth\Exception\UserNotFoundException;
  8. use Michel\Auth\PasswordAuthenticatedUserInterface;
  9. use Michel\Auth\UserInterface;
  10. use Michel\Auth\UserProviderInterface;
  11. use Michel\Resolver\Option;
  12. use Michel\Resolver\OptionsResolver;
  13. use Michel\Session\Storage\SessionStorageInterface;
  14. use Psr\Http\Message\ResponseFactoryInterface;
  15. use Psr\Http\Message\ResponseInterface;
  16. use Psr\Http\Message\ServerRequestInterface;
  17. final class UserFormAuthHandler implements AuthHandlerInterface, StatefulAuthHandlerInterface
  18. {
  19. public const AUTHENTICATION_ERROR = '_form.last_error';
  20. public const LAST_USERNAME = '_form.last_username';
  21. private UserProviderInterface $userProvider;
  22. private SessionStorageInterface $sessionStorage;
  23. private string $loginPath;
  24. private string $logoutPath;
  25. private string $loginKey;
  26. private string $passwordKey;
  27. private string $targetPath;
  28. /**
  29. * @var callable
  30. */
  31. private $onFailure;
  32. public function __construct(
  33. UserProviderInterface $userProvider,
  34. SessionStorageInterface $sessionStorage,
  35. array $options = []
  36. )
  37. {
  38. $this->userProvider = $userProvider;
  39. $this->sessionStorage = $sessionStorage;
  40. $optionResolver = new OptionsResolver([
  41. Option::string('login_path', '/login')->min(1),
  42. Option::string('logout_path', '/logout')->min(1),
  43. Option::string('login_key', 'login')->min(1),
  44. Option::string('password_key', 'password')->min(1),
  45. Option::string('target_path', '/')->min(1),
  46. Option::mixed('on_failure')->validator(function ($value) {
  47. return is_callable($value) || $value === null;
  48. })->setOptional(null),
  49. ]);
  50. $options = $optionResolver->resolve($options);
  51. $this->loginPath = '/'.ltrim($options['login_path'], '/');
  52. $this->logoutPath = '/'.ltrim($options['logout_path'], '/');
  53. $this->loginKey = $options['login_key'];
  54. $this->passwordKey = $options['password_key'];
  55. $this->targetPath = '/'.ltrim($options['target_path'], '/');
  56. $this->onFailure = $options['on_failure'];
  57. }
  58. /**
  59. * @throws AuthenticationException
  60. * @throws UserNotFoundException
  61. * @throws InvalidCredentialsException
  62. */
  63. public function authenticate(ServerRequestInterface $request): ?AuthIdentity
  64. {
  65. $path = $request->getUri()->getPath();
  66. if ($path === $this->logoutPath) {
  67. $this->sessionStorage->remove('user_identifier');
  68. throw new LogoutException('User logged out.');
  69. }
  70. if ($this->sessionStorage->has('user_identifier')) {
  71. $identifier = $this->sessionStorage->get('user_identifier');
  72. $user = $this->userProvider->findByIdentifier($identifier);
  73. if ($user instanceof UserInterface) {
  74. return new AuthIdentity($user, false);
  75. }
  76. }
  77. $method = $request->getMethod();
  78. if ($path === $this->loginPath && $method === 'GET') {
  79. return null;
  80. }
  81. if ($path !== $this->loginPath) {
  82. throw new AuthenticationException('Authentication required.');
  83. }
  84. if ($method !== 'POST') {
  85. throw new AuthenticationException('Login form must be submitted using POST.');
  86. }
  87. list($login, $password) = self::extractCredentials($request, $this->loginKey, $this->passwordKey);
  88. if (empty($login) || empty($password)) {
  89. throw new InvalidCredentialsException("Credentials cannot be empty.");
  90. }
  91. $this->sessionStorage->put(self::LAST_USERNAME, $login);
  92. /**
  93. * @var PasswordAuthenticatedUserInterface|UserInterface|null $user
  94. */
  95. $user = $this->userProvider->findByIdentifier($login);
  96. if (!$user instanceof UserInterface) {
  97. throw new UserNotFoundException("Invalid username or password.");
  98. }
  99. if (!$user instanceof PasswordAuthenticatedUserInterface) {
  100. throw new AuthenticationException("The resolved user does not support password authentication.");
  101. }
  102. if (!$this->userProvider->isPasswordValid($user, $password)) {
  103. throw new InvalidCredentialsException("Invalid username or password.");
  104. }
  105. $this->sessionStorage->put('user_identifier', $user->getUserIdentifier());
  106. return new AuthIdentity($user, true);
  107. }
  108. public function onSuccess(ServerRequestInterface $request, ResponseFactoryInterface $responseFactory): ?ResponseInterface
  109. {
  110. $response = $responseFactory->createResponse(302);
  111. return $response->withHeader('Location', $this->targetPath);
  112. }
  113. public function onFailure(ServerRequestInterface $request, ResponseFactoryInterface $responseFactory, ?AuthenticationException $exception = null): ResponseInterface
  114. {
  115. if ($exception instanceof LogoutException) {
  116. return $responseFactory->createResponse(302)->withHeader('Location', $this->loginPath);
  117. }
  118. if ($exception && !empty($exception->getMessage())) {
  119. $this->sessionStorage->put(self::AUTHENTICATION_ERROR, $exception->getMessage());
  120. $request = $request->withAttribute(self::AUTHENTICATION_ERROR, $exception->getMessage());
  121. }
  122. if (!is_callable($this->onFailure)) {
  123. $response = $responseFactory->createResponse(302);
  124. return $response->withHeader('Location', $this->loginPath);
  125. }
  126. return ($this->onFailure)($request, $responseFactory, $exception);
  127. }
  128. private static function extractCredentials(ServerRequestInterface $request, string $keyLogin, string $keyPassword): array
  129. {
  130. $data = $request->getParsedBody();
  131. $login = $data[$keyLogin] ?? '';
  132. $pass = $data[$keyPassword] ?? '';
  133. return [
  134. $login,
  135. $pass
  136. ];
  137. }
  138. }