Authenticator.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace App\Security;
  20. use function App\Core\I18n\_m;
  21. use App\Core\Router\Router;
  22. use App\Entity\LocalUser;
  23. use App\Util\Common;
  24. use App\Util\Exception\NoSuchActorException;
  25. use App\Util\Exception\NotFoundException;
  26. use App\Util\Exception\ServerException;
  27. use App\Util\Nickname;
  28. use Stringable;
  29. use Symfony\Component\HttpFoundation\RedirectResponse;
  30. use Symfony\Component\HttpFoundation\Request;
  31. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  32. use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  33. use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
  34. use Symfony\Component\Security\Core\Security;
  35. use Symfony\Component\Security\Core\User\UserInterface;
  36. use Symfony\Component\Security\Core\User\UserProviderInterface;
  37. use Symfony\Component\Security\Csrf\CsrfToken;
  38. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  39. use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
  40. use Symfony\Component\Security\Guard\AuthenticatorInterface;
  41. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
  42. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  43. use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
  44. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  45. use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
  46. use Symfony\Component\Security\Http\Util\TargetPathTrait;
  47. /**
  48. * User authenticator
  49. *
  50. * @category Authentication
  51. * @package GNUsocial
  52. *
  53. * @author Hugo Sales <hugo@hsal.es>
  54. * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
  55. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  56. */
  57. class Authenticator extends AbstractFormLoginAuthenticator implements AuthenticatorInterface
  58. {
  59. use TargetPathTrait;
  60. public const LOGIN_ROUTE = 'security_login';
  61. private CsrfTokenManagerInterface $csrfTokenManager;
  62. public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
  63. {
  64. $this->csrfTokenManager = $csrfTokenManager;
  65. }
  66. public function supports(Request $request): bool
  67. {
  68. return (self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST'))
  69. || ('oauth2_authorize' === $request->attributes->get('_route'));
  70. }
  71. /**
  72. * @return array<string, string>
  73. */
  74. public function getCredentials(Request $request): array
  75. {
  76. return [
  77. 'nickname_or_email' => $request->request->get('_username'),
  78. 'password' => $request->request->get('_password'),
  79. 'csrf_token' => $request->request->get('_csrf_token'),
  80. ];
  81. }
  82. /**
  83. * Get a user given credentials and a CSRF token
  84. *
  85. * @param array<string, string> $credentials result of self::getCredentials
  86. *
  87. * @throws NoSuchActorException
  88. * @throws ServerException
  89. *
  90. * @return ?LocalUser
  91. */
  92. public function getUser($credentials, UserProviderInterface $userProvider): ?LocalUser
  93. {
  94. $token = new CsrfToken('authenticate', $credentials['csrf_token']);
  95. if (!$this->csrfTokenManager->isTokenValid($token)) {
  96. throw new InvalidCsrfTokenException();
  97. }
  98. $user = null;
  99. try {
  100. if (Common::isValidEmail($credentials['nickname_or_email'])) {
  101. $user = LocalUser::getByEmail($credentials['nickname_or_email']);
  102. } elseif (Nickname::isValid($credentials['nickname_or_email'])) {
  103. $user = LocalUser::getByNickname($credentials['nickname_or_email']);
  104. }
  105. if (\is_null($user)) {
  106. throw new NoSuchActorException('No such local user.');
  107. }
  108. $credentials['nickname'] = $user->getNickname();
  109. } catch (NoSuchActorException|NotFoundException) {
  110. throw new CustomUserMessageAuthenticationException(
  111. _m('Invalid login credentials.'),
  112. );
  113. }
  114. return $user;
  115. }
  116. /**
  117. * @param array<string, string> $credentials result of self::getCredentials
  118. * @param LocalUser $user
  119. *
  120. * @throws ServerException
  121. */
  122. public function checkCredentials($credentials, $user): bool
  123. {
  124. if (!$user->checkPassword($credentials['password'])) {
  125. throw new CustomUserMessageAuthenticationException(_m('Invalid login credentials.'));
  126. } else {
  127. return true;
  128. }
  129. }
  130. /**
  131. * After a successful login, redirect user to the path saved in their session or to the root of the website
  132. */
  133. public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): RedirectResponse
  134. {
  135. $nickname = $token->getUser();
  136. if ($nickname instanceof Stringable) {
  137. $nickname = (string) $nickname;
  138. } elseif ($nickname instanceof UserInterface) {
  139. $nickname = $nickname->getUserIdentifier();
  140. }
  141. $request->getSession()->set(
  142. Security::LAST_USERNAME,
  143. $nickname,
  144. );
  145. // TODO: Fix the Open Redirect security flaw here.
  146. $targetPath = $request->query->get('returnUrl');
  147. if ($targetPath ??= $this->getTargetPath($request->getSession(), $providerKey)) {
  148. return new RedirectResponse($targetPath);
  149. }
  150. return new RedirectResponse(Router::url('root'));
  151. }
  152. public function authenticate(Request $request): PassportInterface
  153. {
  154. $nickname = $request->request->get('nickname', '');
  155. $request->getSession()->set(Security::LAST_USERNAME, $nickname);
  156. return new Passport(
  157. new UserBadge($nickname),
  158. new PasswordCredentials($request->request->get('password', '')),
  159. [
  160. new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
  161. ],
  162. );
  163. }
  164. protected function getLoginUrl(int $type = Router::ABSOLUTE_PATH): string
  165. {
  166. return Router::url(self::LOGIN_ROUTE, type: $type);
  167. }
  168. }