2
0

FormAuthAuthHandler.php 5.4 KB

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