|
9 | 9 |
|
10 | 10 | use OC\AppFramework\Http; |
11 | 11 | use OC\Files\Filesystem; |
| 12 | +use OC\OCM\OCMSignatoryManager; |
| 13 | +use OC\OCM\Rfc9421SignatoryManager; |
12 | 14 | use OCA\FederatedFileSharing\AddressHandler; |
13 | 15 | use OCA\FederatedFileSharing\FederatedShareProvider; |
14 | 16 | use OCA\Federation\TrustedServers; |
|
34 | 36 | use OCP\Files\ISetupManager; |
35 | 37 | use OCP\Files\NotFoundException; |
36 | 38 | use OCP\HintException; |
| 39 | +use OCP\Http\Client\IClientService; |
| 40 | +use OCP\IAppConfig; |
37 | 41 | use OCP\IConfig; |
38 | 42 | use OCP\IGroupManager; |
39 | 43 | use OCP\IURLGenerator; |
40 | 44 | use OCP\IUser; |
41 | 45 | use OCP\IUserManager; |
42 | 46 | use OCP\Notification\IManager as INotificationManager; |
| 47 | +use OCP\OCM\IOCMDiscoveryService; |
| 48 | +use OCP\Security\Signature\ISignatureManager; |
43 | 49 | use OCP\Server; |
44 | 50 | use OCP\Share\Exceptions\ShareNotFound; |
45 | 51 | use OCP\Share\IManager; |
@@ -71,6 +77,11 @@ public function __construct( |
71 | 77 | private readonly IProviderFactory $shareProviderFactory, |
72 | 78 | private readonly ISetupManager $setupManager, |
73 | 79 | private readonly ExternalShareMapper $externalShareMapper, |
| 80 | + private readonly IOCMDiscoveryService $discoveryService, |
| 81 | + private readonly IClientService $clientService, |
| 82 | + private readonly ISignatureManager $signatureManager, |
| 83 | + private readonly OCMSignatoryManager $signatoryManager, |
| 84 | + private readonly IAppConfig $appConfig, |
74 | 85 | ) { |
75 | 86 | } |
76 | 87 |
|
@@ -107,6 +118,30 @@ public function shareReceived(ICloudFederationShare $share): string { |
107 | 118 | $ownerFederatedId = $share->getOwner(); |
108 | 119 | $shareType = $this->mapShareTypeToNextcloud($share->getShareType()); |
109 | 120 |
|
| 121 | + // Check for must-exchange-token requirement |
| 122 | + $requirements = $protocol['webdav']['requirements'] ?? $protocol['options']['requirements'] ?? []; |
| 123 | + $mustExchangeToken = in_array('must-exchange-token', $requirements); |
| 124 | + $accessToken = ''; |
| 125 | + |
| 126 | + if ($mustExchangeToken) { |
| 127 | + // Exchange the sharedSecret for an access token (required) |
| 128 | + $accessToken = $this->exchangeToken($remote, $token); |
| 129 | + if ($accessToken === null) { |
| 130 | + throw new ProviderCouldNotAddShareException('Failed to exchange token as required by must-exchange-token', '', Http::STATUS_BAD_REQUEST); |
| 131 | + } |
| 132 | + } else { |
| 133 | + // Check if remote has exchange-token capability and try to exchange (optional) |
| 134 | + try { |
| 135 | + $ocmProvider = $this->discoveryService->discover(rtrim($remote, '/')); |
| 136 | + if ($ocmProvider->getCapabilities()->hasExchangeToken()) { |
| 137 | + $accessToken = $this->exchangeToken($remote, $token) ?? ''; |
| 138 | + $this->logger->debug('Exchanged token for remote with exchange-token capability', ['remote' => $remote, 'success' => !empty($accessToken)]); |
| 139 | + } |
| 140 | + } catch (\Exception $e) { |
| 141 | + $this->logger->debug('Could not discover remote capabilities for token exchange', ['remote' => $remote, 'exception' => $e]); |
| 142 | + } |
| 143 | + } |
| 144 | + |
110 | 145 | // if no explicit information about the person who created the share was sent |
111 | 146 | // we assume that the share comes from the owner |
112 | 147 | if ($sharedByFederatedId === null) { |
@@ -147,8 +182,8 @@ public function shareReceived(ICloudFederationShare $share): string { |
147 | 182 | $externalShare->generateId(); |
148 | 183 | $externalShare->setRemote($remote); |
149 | 184 | $externalShare->setRemoteId($remoteId); |
150 | | - $externalShare->setShareToken($token); |
151 | | - $externalShare->setPassword(''); |
| 185 | + $externalShare->setRefreshToken($token); // refresh token (sharedSecret) |
| 186 | + $externalShare->setAccessToken($accessToken ?: null); |
152 | 187 | $externalShare->setName($name); |
153 | 188 | $externalShare->setOwner($owner); |
154 | 189 | $externalShare->setShareType($shareType); |
@@ -685,4 +720,98 @@ public function getFederationIdFromSharedSecret( |
685 | 720 | return $share->getShareOwner(); |
686 | 721 | } |
687 | 722 | } |
| 723 | + |
| 724 | + /** |
| 725 | + * Exchange a sharedSecret (refresh token) for an access token via the remote server's token endpoint |
| 726 | + * |
| 727 | + * @param string $remote The remote server URL |
| 728 | + * @param string $sharedSecret The shared secret to exchange |
| 729 | + * @return string|null The access token, or null on failure |
| 730 | + */ |
| 731 | + private function exchangeToken(string $remote, #[SensitiveParameter] string $sharedSecret): ?string { |
| 732 | + try { |
| 733 | + $ocmProvider = $this->discoveryService->discover(rtrim($remote, '/')); |
| 734 | + $tokenEndpoint = $ocmProvider->getTokenEndPoint(); |
| 735 | + |
| 736 | + if ($tokenEndpoint === '') { |
| 737 | + $this->logger->warning('Remote server does not expose tokenEndPoint', ['remote' => $remote]); |
| 738 | + return null; |
| 739 | + } |
| 740 | + |
| 741 | + $client = $this->clientService->newClient(); |
| 742 | + $clientId = parse_url($this->urlGenerator->getAbsoluteURL('/'), PHP_URL_HOST); |
| 743 | + |
| 744 | + $payload = [ |
| 745 | + 'grant_type' => 'authorization_code', |
| 746 | + 'client_id' => $clientId, |
| 747 | + 'code' => $sharedSecret, |
| 748 | + ]; |
| 749 | + |
| 750 | + $options = [ |
| 751 | + 'body' => http_build_query($payload), |
| 752 | + 'headers' => [ |
| 753 | + 'Content-Type' => 'application/x-www-form-urlencoded', |
| 754 | + ], |
| 755 | + 'timeout' => 10, |
| 756 | + 'connect_timeout' => 10, |
| 757 | + ]; |
| 758 | + |
| 759 | + try { |
| 760 | + $options = $this->signatureManager->signOutgoingRequestIClientPayload( |
| 761 | + new Rfc9421SignatoryManager($this->signatoryManager), |
| 762 | + $options, |
| 763 | + 'post', |
| 764 | + $tokenEndpoint |
| 765 | + ); |
| 766 | + $this->logger->debug('Token request signed successfully', ['remote' => $remote]); |
| 767 | + } catch (\Exception $e) { |
| 768 | + $this->logger->error('Failed to sign token request', [ |
| 769 | + 'remote' => $remote, |
| 770 | + 'exception' => $e, |
| 771 | + 'endpoint' => $tokenEndpoint, |
| 772 | + ]); |
| 773 | + return null; |
| 774 | + } |
| 775 | + |
| 776 | + $response = $client->post($tokenEndpoint, $options); |
| 777 | + |
| 778 | + $statusCode = $response->getStatusCode(); |
| 779 | + if ($statusCode !== 200) { |
| 780 | + $this->logger->warning('Token exchange returned unexpected HTTP status', [ |
| 781 | + 'remote' => $remote, |
| 782 | + 'status' => $statusCode, |
| 783 | + ]); |
| 784 | + return null; |
| 785 | + } |
| 786 | + |
| 787 | + $data = json_decode($response->getBody(), true); |
| 788 | + |
| 789 | + if (!is_array($data)) { |
| 790 | + $this->logger->warning('Token exchange response is not valid JSON', ['remote' => $remote]); |
| 791 | + return null; |
| 792 | + } |
| 793 | + |
| 794 | + $accessToken = $data['access_token'] ?? null; |
| 795 | + $tokenType = $data['token_type'] ?? null; |
| 796 | + |
| 797 | + if (!is_string($accessToken) || $accessToken === '') { |
| 798 | + $this->logger->warning('Token exchange response missing or invalid access_token', ['remote' => $remote]); |
| 799 | + return null; |
| 800 | + } |
| 801 | + |
| 802 | + if (!is_string($tokenType) || strtolower($tokenType) !== 'bearer') { |
| 803 | + $this->logger->warning('Token exchange response has unexpected token_type', [ |
| 804 | + 'remote' => $remote, |
| 805 | + 'token_type' => $tokenType, |
| 806 | + ]); |
| 807 | + return null; |
| 808 | + } |
| 809 | + |
| 810 | + $this->logger->debug('Successfully exchanged token for access token', ['remote' => $remote]); |
| 811 | + return $accessToken; |
| 812 | + } catch (\Exception $e) { |
| 813 | + $this->logger->warning('Failed to exchange token', ['remote' => $remote, 'exception' => $e]); |
| 814 | + return null; |
| 815 | + } |
| 816 | + } |
688 | 817 | } |
0 commit comments