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