OCSAuthAPIController.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\Federation\Controller;
  8. use OCA\Federation\DbHandler;
  9. use OCA\Federation\TrustedServers;
  10. use OCP\AppFramework\Http;
  11. use OCP\AppFramework\Http\Attribute\BruteForceProtection;
  12. use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
  13. use OCP\AppFramework\Http\Attribute\OpenAPI;
  14. use OCP\AppFramework\Http\Attribute\PublicPage;
  15. use OCP\AppFramework\Http\DataResponse;
  16. use OCP\AppFramework\OCS\OCSForbiddenException;
  17. use OCP\AppFramework\OCSController;
  18. use OCP\AppFramework\Utility\ITimeFactory;
  19. use OCP\BackgroundJob\IJobList;
  20. use OCP\IRequest;
  21. use OCP\Security\Bruteforce\IThrottler;
  22. use OCP\Security\ISecureRandom;
  23. use Psr\Log\LoggerInterface;
  24. /**
  25. * Class OCSAuthAPI
  26. *
  27. * OCS API end-points to exchange shared secret between two connected Nextclouds
  28. *
  29. * @package OCA\Federation\Controller
  30. */
  31. #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
  32. class OCSAuthAPIController extends OCSController {
  33. private ISecureRandom $secureRandom;
  34. private IJobList $jobList;
  35. private TrustedServers $trustedServers;
  36. private DbHandler $dbHandler;
  37. private LoggerInterface $logger;
  38. private ITimeFactory $timeFactory;
  39. private IThrottler $throttler;
  40. public function __construct(
  41. string $appName,
  42. IRequest $request,
  43. ISecureRandom $secureRandom,
  44. IJobList $jobList,
  45. TrustedServers $trustedServers,
  46. DbHandler $dbHandler,
  47. LoggerInterface $logger,
  48. ITimeFactory $timeFactory,
  49. IThrottler $throttler
  50. ) {
  51. parent::__construct($appName, $request);
  52. $this->secureRandom = $secureRandom;
  53. $this->jobList = $jobList;
  54. $this->trustedServers = $trustedServers;
  55. $this->dbHandler = $dbHandler;
  56. $this->logger = $logger;
  57. $this->timeFactory = $timeFactory;
  58. $this->throttler = $throttler;
  59. }
  60. /**
  61. * Request received to ask remote server for a shared secret, for legacy end-points
  62. *
  63. * @param string $url URL of the server
  64. * @param string $token Token of the server
  65. * @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
  66. * @throws OCSForbiddenException Requesting shared secret is not allowed
  67. *
  68. * 200: Shared secret requested successfully
  69. */
  70. #[NoCSRFRequired]
  71. #[PublicPage]
  72. #[BruteForceProtection(action: 'federationSharedSecret')]
  73. public function requestSharedSecretLegacy(string $url, string $token): DataResponse {
  74. return $this->requestSharedSecret($url, $token);
  75. }
  76. /**
  77. * Create shared secret and return it, for legacy end-points
  78. *
  79. * @param string $url URL of the server
  80. * @param string $token Token of the server
  81. * @return DataResponse<Http::STATUS_OK, array{sharedSecret: string}, array{}>
  82. * @throws OCSForbiddenException Getting shared secret is not allowed
  83. *
  84. * 200: Shared secret returned
  85. */
  86. #[NoCSRFRequired]
  87. #[PublicPage]
  88. #[BruteForceProtection(action: 'federationSharedSecret')]
  89. public function getSharedSecretLegacy(string $url, string $token): DataResponse {
  90. return $this->getSharedSecret($url, $token);
  91. }
  92. /**
  93. * Request received to ask remote server for a shared secret
  94. *
  95. * @param string $url URL of the server
  96. * @param string $token Token of the server
  97. * @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
  98. * @throws OCSForbiddenException Requesting shared secret is not allowed
  99. *
  100. * 200: Shared secret requested successfully
  101. */
  102. #[NoCSRFRequired]
  103. #[PublicPage]
  104. #[BruteForceProtection(action: 'federationSharedSecret')]
  105. public function requestSharedSecret(string $url, string $token): DataResponse {
  106. if ($this->trustedServers->isTrustedServer($url) === false) {
  107. $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
  108. $this->logger->error('remote server not trusted (' . $url . ') while requesting shared secret', ['app' => 'federation']);
  109. throw new OCSForbiddenException();
  110. }
  111. // if both server initiated the exchange of the shared secret the greater
  112. // token wins
  113. $localToken = $this->dbHandler->getToken($url);
  114. if (strcmp($localToken, $token) > 0) {
  115. $this->logger->info(
  116. 'remote server (' . $url . ') presented lower token. We will initiate the exchange of the shared secret.',
  117. ['app' => 'federation']
  118. );
  119. throw new OCSForbiddenException();
  120. }
  121. $this->jobList->add(
  122. 'OCA\Federation\BackgroundJob\GetSharedSecret',
  123. [
  124. 'url' => $url,
  125. 'token' => $token,
  126. 'created' => $this->timeFactory->getTime()
  127. ]
  128. );
  129. return new DataResponse();
  130. }
  131. /**
  132. * Create shared secret and return it
  133. *
  134. * @param string $url URL of the server
  135. * @param string $token Token of the server
  136. * @return DataResponse<Http::STATUS_OK, array{sharedSecret: string}, array{}>
  137. * @throws OCSForbiddenException Getting shared secret is not allowed
  138. *
  139. * 200: Shared secret returned
  140. */
  141. #[NoCSRFRequired]
  142. #[PublicPage]
  143. #[BruteForceProtection(action: 'federationSharedSecret')]
  144. public function getSharedSecret(string $url, string $token): DataResponse {
  145. if ($this->trustedServers->isTrustedServer($url) === false) {
  146. $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
  147. $this->logger->error('remote server not trusted (' . $url . ') while getting shared secret', ['app' => 'federation']);
  148. throw new OCSForbiddenException();
  149. }
  150. if ($this->isValidToken($url, $token) === false) {
  151. $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
  152. $expectedToken = $this->dbHandler->getToken($url);
  153. $this->logger->error(
  154. 'remote server (' . $url . ') didn\'t send a valid token (got "' . $token . '" but expected "'. $expectedToken . '") while getting shared secret',
  155. ['app' => 'federation']
  156. );
  157. throw new OCSForbiddenException();
  158. }
  159. $sharedSecret = $this->secureRandom->generate(32);
  160. $this->trustedServers->addSharedSecret($url, $sharedSecret);
  161. return new DataResponse([
  162. 'sharedSecret' => $sharedSecret
  163. ]);
  164. }
  165. protected function isValidToken(string $url, string $token): bool {
  166. $storedToken = $this->dbHandler->getToken($url);
  167. return hash_equals($storedToken, $token);
  168. }
  169. }