PasswordConfirmationMiddleware.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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 OCP\AppFramework\Controller;
  11. use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
  12. use OCP\AppFramework\Middleware;
  13. use OCP\AppFramework\Utility\ITimeFactory;
  14. use OCP\Authentication\Exceptions\ExpiredTokenException;
  15. use OCP\Authentication\Exceptions\InvalidTokenException;
  16. use OCP\Authentication\Exceptions\WipeTokenException;
  17. use OCP\Authentication\Token\IToken;
  18. use OCP\ISession;
  19. use OCP\IUserSession;
  20. use OCP\Session\Exceptions\SessionNotAvailableException;
  21. use OCP\User\Backend\IPasswordConfirmationBackend;
  22. use Psr\Log\LoggerInterface;
  23. use ReflectionMethod;
  24. class PasswordConfirmationMiddleware extends Middleware {
  25. /** @var ControllerMethodReflector */
  26. private $reflector;
  27. /** @var ISession */
  28. private $session;
  29. /** @var IUserSession */
  30. private $userSession;
  31. /** @var ITimeFactory */
  32. private $timeFactory;
  33. /** @var array */
  34. private $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true];
  35. private IProvider $tokenProvider;
  36. /**
  37. * PasswordConfirmationMiddleware constructor.
  38. *
  39. * @param ControllerMethodReflector $reflector
  40. * @param ISession $session
  41. * @param IUserSession $userSession
  42. * @param ITimeFactory $timeFactory
  43. */
  44. public function __construct(
  45. ControllerMethodReflector $reflector,
  46. ISession $session,
  47. IUserSession $userSession,
  48. ITimeFactory $timeFactory,
  49. IProvider $tokenProvider,
  50. private readonly LoggerInterface $logger,
  51. ) {
  52. $this->reflector = $reflector;
  53. $this->session = $session;
  54. $this->userSession = $userSession;
  55. $this->timeFactory = $timeFactory;
  56. $this->tokenProvider = $tokenProvider;
  57. }
  58. /**
  59. * @param Controller $controller
  60. * @param string $methodName
  61. * @throws NotConfirmedException
  62. */
  63. public function beforeController($controller, $methodName) {
  64. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  65. if ($this->hasAnnotationOrAttribute($reflectionMethod, 'PasswordConfirmationRequired', PasswordConfirmationRequired::class)) {
  66. $user = $this->userSession->getUser();
  67. $backendClassName = '';
  68. if ($user !== null) {
  69. $backend = $user->getBackend();
  70. if ($backend instanceof IPasswordConfirmationBackend) {
  71. if (!$backend->canConfirmPassword($user->getUID())) {
  72. return;
  73. }
  74. }
  75. $backendClassName = $user->getBackendClassName();
  76. }
  77. try {
  78. $sessionId = $this->session->getId();
  79. $token = $this->tokenProvider->getToken($sessionId);
  80. } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) {
  81. // States we do not deal with here.
  82. return;
  83. }
  84. $scope = $token->getScopeAsArray();
  85. if (isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) && $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === true) {
  86. // Users logging in from SSO backends cannot confirm their password by design
  87. return;
  88. }
  89. $lastConfirm = (int)$this->session->get('last-password-confirm');
  90. // TODO: confirm excludedUserBackEnds can go away and remove it
  91. if (!isset($this->excludedUserBackEnds[$backendClassName]) && $lastConfirm < ($this->timeFactory->getTime() - (30 * 60 + 15))) { // allow 15 seconds delay
  92. throw new NotConfirmedException();
  93. }
  94. }
  95. }
  96. /**
  97. * @template T
  98. *
  99. * @param ReflectionMethod $reflectionMethod
  100. * @param string $annotationName
  101. * @param class-string<T> $attributeClass
  102. * @return boolean
  103. */
  104. protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool {
  105. if (!empty($reflectionMethod->getAttributes($attributeClass))) {
  106. return true;
  107. }
  108. if ($this->reflector->hasAnnotation($annotationName)) {
  109. $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead');
  110. return true;
  111. }
  112. return false;
  113. }
  114. }