PasswordConfirmationMiddleware.php 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OC\AppFramework\Middleware\Security;
  7. use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException;
  8. use OC\AppFramework\Utility\ControllerMethodReflector;
  9. use OC\Authentication\Token\IProvider;
  10. use OC\User\Manager;
  11. use OCP\AppFramework\Controller;
  12. use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
  13. use OCP\AppFramework\Middleware;
  14. use OCP\AppFramework\Utility\ITimeFactory;
  15. use OCP\Authentication\Exceptions\ExpiredTokenException;
  16. use OCP\Authentication\Exceptions\InvalidTokenException;
  17. use OCP\Authentication\Exceptions\WipeTokenException;
  18. use OCP\Authentication\Token\IToken;
  19. use OCP\IRequest;
  20. use OCP\ISession;
  21. use OCP\IUserSession;
  22. use OCP\Session\Exceptions\SessionNotAvailableException;
  23. use OCP\User\Backend\IPasswordConfirmationBackend;
  24. use Psr\Log\LoggerInterface;
  25. use ReflectionMethod;
  26. class PasswordConfirmationMiddleware extends Middleware {
  27. private array $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true];
  28. public function __construct(
  29. private ControllerMethodReflector $reflector,
  30. private ISession $session,
  31. private IUserSession $userSession,
  32. private ITimeFactory $timeFactory,
  33. private IProvider $tokenProvider,
  34. private readonly LoggerInterface $logger,
  35. private readonly IRequest $request,
  36. private readonly Manager $userManager,
  37. ) {
  38. }
  39. /**
  40. * @throws NotConfirmedException
  41. */
  42. public function beforeController(Controller $controller, string $methodName) {
  43. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  44. if (!$this->needsPasswordConfirmation($reflectionMethod)) {
  45. return;
  46. }
  47. $user = $this->userSession->getUser();
  48. $backendClassName = '';
  49. if ($user !== null) {
  50. $backend = $user->getBackend();
  51. if ($backend instanceof IPasswordConfirmationBackend) {
  52. if (!$backend->canConfirmPassword($user->getUID())) {
  53. return;
  54. }
  55. }
  56. $backendClassName = $user->getBackendClassName();
  57. }
  58. try {
  59. $sessionId = $this->session->getId();
  60. $token = $this->tokenProvider->getToken($sessionId);
  61. } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) {
  62. // States we do not deal with here.
  63. return;
  64. }
  65. $scope = $token->getScopeAsArray();
  66. if (isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) && $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === true) {
  67. // Users logging in from SSO backends cannot confirm their password by design
  68. return;
  69. }
  70. if ($this->isPasswordConfirmationStrict($reflectionMethod)) {
  71. $authHeader = $this->request->getHeader('Authorization');
  72. [, $password] = explode(':', base64_decode(substr($authHeader, 6)), 2);
  73. $loginResult = $this->userManager->checkPassword($user->getUid(), $password);
  74. if ($loginResult === false) {
  75. throw new NotConfirmedException();
  76. }
  77. $this->session->set('last-password-confirm', $this->timeFactory->getTime());
  78. } else {
  79. $lastConfirm = (int)$this->session->get('last-password-confirm');
  80. // TODO: confirm excludedUserBackEnds can go away and remove it
  81. if (!isset($this->excludedUserBackEnds[$backendClassName]) && $lastConfirm < ($this->timeFactory->getTime() - (30 * 60 + 15))) { // allow 15 seconds delay
  82. throw new NotConfirmedException();
  83. }
  84. }
  85. }
  86. private function needsPasswordConfirmation(ReflectionMethod $reflectionMethod): bool {
  87. $attributes = $reflectionMethod->getAttributes(PasswordConfirmationRequired::class);
  88. if (!empty($attributes)) {
  89. return true;
  90. }
  91. if ($this->reflector->hasAnnotation('PasswordConfirmationRequired')) {
  92. $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . 'PasswordConfirmationRequired' . ' annotation and should use the #[PasswordConfirmationRequired] attribute instead');
  93. return true;
  94. }
  95. return false;
  96. }
  97. private function isPasswordConfirmationStrict(ReflectionMethod $reflectionMethod): bool {
  98. $attributes = $reflectionMethod->getAttributes(PasswordConfirmationRequired::class);
  99. return !empty($attributes) && ($attributes[0]->newInstance()->getStrict());
  100. }
  101. }