OCSAuthAPIController.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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. public function __construct(
  34. string $appName,
  35. IRequest $request,
  36. private ISecureRandom $secureRandom,
  37. private IJobList $jobList,
  38. private TrustedServers $trustedServers,
  39. private DbHandler $dbHandler,
  40. private LoggerInterface $logger,
  41. private ITimeFactory $timeFactory,
  42. private IThrottler $throttler,
  43. ) {
  44. parent::__construct($appName, $request);
  45. }
  46. /**
  47. * Request received to ask remote server for a shared secret, for legacy end-points
  48. *
  49. * @param string $url URL of the server
  50. * @param string $token Token of the server
  51. * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
  52. * @throws OCSForbiddenException Requesting shared secret is not allowed
  53. *
  54. * 200: Shared secret requested successfully
  55. */
  56. #[NoCSRFRequired]
  57. #[PublicPage]
  58. #[BruteForceProtection(action: 'federationSharedSecret')]
  59. public function requestSharedSecretLegacy(string $url, string $token): DataResponse {
  60. return $this->requestSharedSecret($url, $token);
  61. }
  62. /**
  63. * Create shared secret and return it, for legacy end-points
  64. *
  65. * @param string $url URL of the server
  66. * @param string $token Token of the server
  67. * @return DataResponse<Http::STATUS_OK, array{sharedSecret: string}, array{}>
  68. * @throws OCSForbiddenException Getting shared secret is not allowed
  69. *
  70. * 200: Shared secret returned
  71. */
  72. #[NoCSRFRequired]
  73. #[PublicPage]
  74. #[BruteForceProtection(action: 'federationSharedSecret')]
  75. public function getSharedSecretLegacy(string $url, string $token): DataResponse {
  76. return $this->getSharedSecret($url, $token);
  77. }
  78. /**
  79. * Request received to ask remote server for a shared secret
  80. *
  81. * @param string $url URL of the server
  82. * @param string $token Token of the server
  83. * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
  84. * @throws OCSForbiddenException Requesting shared secret is not allowed
  85. *
  86. * 200: Shared secret requested successfully
  87. */
  88. #[NoCSRFRequired]
  89. #[PublicPage]
  90. #[BruteForceProtection(action: 'federationSharedSecret')]
  91. public function requestSharedSecret(string $url, string $token): DataResponse {
  92. if ($this->trustedServers->isTrustedServer($url) === false) {
  93. $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
  94. $this->logger->error('remote server not trusted (' . $url . ') while requesting shared secret', ['app' => 'federation']);
  95. throw new OCSForbiddenException();
  96. }
  97. // if both server initiated the exchange of the shared secret the greater
  98. // token wins
  99. $localToken = $this->dbHandler->getToken($url);
  100. if (strcmp($localToken, $token) > 0) {
  101. $this->logger->info(
  102. 'remote server (' . $url . ') presented lower token. We will initiate the exchange of the shared secret.',
  103. ['app' => 'federation']
  104. );
  105. throw new OCSForbiddenException();
  106. }
  107. $this->jobList->add(
  108. 'OCA\Federation\BackgroundJob\GetSharedSecret',
  109. [
  110. 'url' => $url,
  111. 'token' => $token,
  112. 'created' => $this->timeFactory->getTime()
  113. ]
  114. );
  115. return new DataResponse();
  116. }
  117. /**
  118. * Create shared secret and return it
  119. *
  120. * @param string $url URL of the server
  121. * @param string $token Token of the server
  122. * @return DataResponse<Http::STATUS_OK, array{sharedSecret: string}, array{}>
  123. * @throws OCSForbiddenException Getting shared secret is not allowed
  124. *
  125. * 200: Shared secret returned
  126. */
  127. #[NoCSRFRequired]
  128. #[PublicPage]
  129. #[BruteForceProtection(action: 'federationSharedSecret')]
  130. public function getSharedSecret(string $url, string $token): DataResponse {
  131. if ($this->trustedServers->isTrustedServer($url) === false) {
  132. $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
  133. $this->logger->error('remote server not trusted (' . $url . ') while getting shared secret', ['app' => 'federation']);
  134. throw new OCSForbiddenException();
  135. }
  136. if ($this->isValidToken($url, $token) === false) {
  137. $this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
  138. $expectedToken = $this->dbHandler->getToken($url);
  139. $this->logger->error(
  140. 'remote server (' . $url . ') didn\'t send a valid token (got "' . $token . '" but expected "' . $expectedToken . '") while getting shared secret',
  141. ['app' => 'federation']
  142. );
  143. throw new OCSForbiddenException();
  144. }
  145. $sharedSecret = $this->secureRandom->generate(32);
  146. $this->trustedServers->addSharedSecret($url, $sharedSecret);
  147. return new DataResponse([
  148. 'sharedSecret' => $sharedSecret
  149. ]);
  150. }
  151. protected function isValidToken(string $url, string $token): bool {
  152. $storedToken = $this->dbHandler->getToken($url);
  153. return hash_equals($storedToken, $token);
  154. }
  155. }