PasswordConfirmationMiddleware.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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(ControllerMethodReflector $reflector,
  45. ISession $session,
  46. IUserSession $userSession,
  47. ITimeFactory $timeFactory,
  48. IProvider $tokenProvider,
  49. private readonly LoggerInterface $logger,
  50. ) {
  51. $this->reflector = $reflector;
  52. $this->session = $session;
  53. $this->userSession = $userSession;
  54. $this->timeFactory = $timeFactory;
  55. $this->tokenProvider = $tokenProvider;
  56. }
  57. /**
  58. * @param Controller $controller
  59. * @param string $methodName
  60. * @throws NotConfirmedException
  61. */
  62. public function beforeController($controller, $methodName) {
  63. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  64. if ($this->hasAnnotationOrAttribute($reflectionMethod, 'PasswordConfirmationRequired', PasswordConfirmationRequired::class)) {
  65. $user = $this->userSession->getUser();
  66. $backendClassName = '';
  67. if ($user !== null) {
  68. $backend = $user->getBackend();
  69. if ($backend instanceof IPasswordConfirmationBackend) {
  70. if (!$backend->canConfirmPassword($user->getUID())) {
  71. return;
  72. }
  73. }
  74. $backendClassName = $user->getBackendClassName();
  75. }
  76. try {
  77. $sessionId = $this->session->getId();
  78. $token = $this->tokenProvider->getToken($sessionId);
  79. } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) {
  80. // States we do not deal with here.
  81. return;
  82. }
  83. $scope = $token->getScopeAsArray();
  84. if (isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) && $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === true) {
  85. // Users logging in from SSO backends cannot confirm their password by design
  86. return;
  87. }
  88. $lastConfirm = (int)$this->session->get('last-password-confirm');
  89. // TODO: confirm excludedUserBackEnds can go away and remove it
  90. if (!isset($this->excludedUserBackEnds[$backendClassName]) && $lastConfirm < ($this->timeFactory->getTime() - (30 * 60 + 15))) { // allow 15 seconds delay
  91. throw new NotConfirmedException();
  92. }
  93. }
  94. }
  95. /**
  96. * @template T
  97. *
  98. * @param ReflectionMethod $reflectionMethod
  99. * @param string $annotationName
  100. * @param class-string<T> $attributeClass
  101. * @return boolean
  102. */
  103. protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool {
  104. if (!empty($reflectionMethod->getAttributes($attributeClass))) {
  105. return true;
  106. }
  107. if ($this->reflector->hasAnnotation($annotationName)) {
  108. $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead');
  109. return true;
  110. }
  111. return false;
  112. }
  113. }