Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
c2ec5bc
feat: Bearer auth aware Sabre HTTP client
enriquepablo Dec 23, 2025
345c198
feat(dav): Add token endpoint to exchange refresh tokens for access t…
enriquepablo Dec 23, 2025
78f752e
feat(dav): Add Bearer auth backend for webdav requests
enriquepablo Dec 23, 2025
4ca65de
feat(dav): New method doTryTokenLogin to allow to try token login wit…
enriquepablo Dec 23, 2025
7e998e7
feat(federatedfilesharing): Create permanent refresh token when creat…
enriquepablo Dec 23, 2025
e5387f4
feat(federatedfilesharing): When a remote requests a share with a tok…
enriquepablo Dec 23, 2025
1bd0dad
feat(files_sharing): When requesting a remote share with bearer auth,…
enriquepablo Dec 23, 2025
ed02c1f
feat: adapt to guzzle api
enriquepablo Dec 23, 2025
f8c13fe
feat(cloud_federation_api): adapt to new format for share creation
enriquepablo Dec 23, 2025
7073b01
feat(cloud_federation_api): support multi protocol for share creation
enriquepablo Jan 8, 2026
86a1bd8
fix(dav): data sent to token endpoint must be application/x-www-form-…
enriquepablo Jan 8, 2026
a1d4dc1
fix(dav): when receiving a share, account for the must-exchange-token…
enriquepablo Jan 9, 2026
2f186bf
fix(federatedfilesharing): POSTs to token endpoint should be signed
enriquepablo Jan 12, 2026
f877cb3
fix(federatedfilesharing): POSTs to token endpoint MUST be signed
enriquepablo Jan 12, 2026
b237bc2
fix: federated share provider tests
enriquepablo Jan 8, 2026
a8511ab
fix: share manager test
enriquepablo Jan 8, 2026
f711f5a
fix(federatedfilesharing): fix federated share provider tests
enriquepablo Jan 19, 2026
8d2e2ab
fix(federatedfilesharing): fixing federated share provider tests
enriquepablo Jan 19, 2026
beb7e9e
fix(federatedfilesharing): fixing federated share provider tests
enriquepablo Jan 19, 2026
34a6149
fix: fixing code style
enriquepablo Jan 19, 2026
c396801
fix: fixing openapi specs
enriquepablo Jan 19, 2026
095e9e5
fix: fix psalm issues
enriquepablo Jan 19, 2026
c61eb44
fix: reorder import
enriquepablo Jan 19, 2026
0133f83
fix(dav): do not import from NCU ns
enriquepablo Jan 19, 2026
18cfcfc
fix: fix sqlite integration tests
enriquepablo Jan 23, 2026
6c79511
fix(federatedfilesharing): order of imports
enriquepablo Jan 23, 2026
f8fce1c
fix: fix session tests using Session::loginWithToken
enriquepablo Jan 23, 2026
b1b535d
fix: fix public key token provider test
enriquepablo Jan 23, 2026
e0e0df7
fix: Fixed undefined $request variable
enriquepablo Jan 26, 2026
ce02995
fix(files_external): Added missing doTryTokenLogin() method to implem…
enriquepablo Jan 26, 2026
1f6e483
fix: Fixed parent::getType() to use ->getter('type') to avoid Psalm m…
enriquepablo Jan 26, 2026
f3d72ff
fix: Added getTokenEndPoint() and setTokenEndPoint() methods that sho…
enriquepablo Jan 26, 2026
0a7bc64
fix: fix session tests
enriquepablo Jan 28, 2026
45f4b53
fix(federatedfilesharing): remove unused import
enriquepablo Jan 29, 2026
d176331
fix: fix user session tests
enriquepablo Jan 29, 2026
ba8321b
test: test token controller
enriquepablo Jan 29, 2026
b6083c6
test: test doTryTokenLogin method
enriquepablo Jan 29, 2026
3de9f1b
fix(dav): remove unused import in TokenController test
enriquepablo Feb 2, 2026
74c276a
feat(dav): refresh expired tokens
enriquepablo Feb 2, 2026
8f03056
fix(files_sharing): refactor refreshing access tokens
enriquepablo Feb 4, 2026
f16e356
fix(dav): keep refresh tokens in its own db table
enriquepablo Mar 6, 2026
63f7dcf
fix(dav): validate token exchange response before using it
enriquepablo Mar 6, 2026
891d9fa
fix(dav): keep refresh tokens in its own db table, add migration for …
enriquepablo Mar 6, 2026
800f415
fix(files_sharing): keep access tokens in its own field in the extern…
enriquepablo Mar 6, 2026
a2b2ec3
fix(files_sharing): prevent concurrent requests to refresh token inde…
enriquepablo Mar 6, 2026
da9227e
fix(dav): cleanup expired access tokens
enriquepablo Mar 6, 2026
ec06f3c
fix(files_sharing): prevent infinite loop trying unsuccessfully to re…
enriquepablo Mar 6, 2026
39b36bc
fix(files_sharing): missing autoloads
enriquepablo Mar 10, 2026
9a97620
test(dav): test bearer token login
enriquepablo Mar 20, 2026
820d1e8
test(dav): test cleanup of expired access tokens
enriquepablo Mar 20, 2026
3326f9e
test(federatedfilesharing): test federated shares
enriquepablo Mar 20, 2026
7d2cf7b
test(files_sharing): test access tokens
enriquepablo Mar 20, 2026
c07011e
fix(files_sharing): correct access level for appConfig
enriquepablo Mar 20, 2026
bd35874
fix: backwards compatibility for shares from instances before upgrading
enriquepablo Mar 25, 2026
4288574
fix: backwards compatibility for shares for instances before upgrading
enriquepablo Mar 25, 2026
8679743
fix: backwards compatibility for shares for instances before upgrading
enriquepablo Mar 26, 2026
22e9ef8
fix: re-order imports
enriquepablo Mar 26, 2026
ccdf77c
fix: avoid changing the IUserSession interface
enriquepablo Mar 26, 2026
2f0d28e
fix[dav]: composer autoload
enriquepablo Apr 20, 2026
83fd9d0
fix[federatedfilesharing]: replace deprecated import
enriquepablo Apr 20, 2026
a1a53bc
chore: bump 3rdparty and update psalm baseline
mickenordin Apr 24, 2026
fb23f93
fix(dav): respect storage scheme when discovering token endpoint
mickenordin Apr 26, 2026
89f640d
fix(auth): allow OCM access tokens via Bearer header
mickenordin Apr 26, 2026
f04aefb
chore: add missing `Override` attributes flagged by psalm
mickenordin May 4, 2026
b479af5
fix(dav): regenerate composer autoloader for new migration
mickenordin May 4, 2026
9e293af
feat(JWT): Switch the access_token to a JWT
mickenordin May 6, 2026
9918b54
feat[dav]: add bearer auth backend to dav endpoints
enriquepablo May 21, 2026
3e3d9e5
fix[cloud_federation_api]: move access token machinery to cloud_feder…
enriquepablo May 21, 2026
aaea1f6
fix(dav): revert useless change
enriquepablo May 21, 2026
9ac299b
fix(cloud_federation_api): pick token endpoint params in the method's…
enriquepablo May 21, 2026
5c97992
fix(files_sahring, federatedfilesharing): rename share token to refre…
enriquepablo May 21, 2026
d7d2313
fix(files_sharing): do not log (a fragment of) the refresh token
enriquepablo May 21, 2026
a8f7519
fix: regex for token string was matching 15 - 32 instead of 15 | 32
enriquepablo May 21, 2026
9359868
feat(federatedfilesharing): add OCMCapabilities class with OCM capabi…
enriquepablo May 21, 2026
e2b11a3
fix(dav): add types to DAV properties
enriquepablo May 21, 2026
e7ddbd6
fix(dav): better variable name for tokens
enriquepablo May 21, 2026
60f6033
fix(dav): remove semicolon accidentally added in previous commit
enriquepablo May 21, 2026
8080007
feat(files_sharing): make sabre http client that transparently handle…
enriquepablo May 21, 2026
4b134de
fix(dav): cleanup of code no longer needed
enriquepablo May 21, 2026
a90d003
fix(files_sharing): the access_token field in the shares_external db …
enriquepablo May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3rdparty
4 changes: 4 additions & 0 deletions apps/cloud_federation_api/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@
<dependencies>
<nextcloud min-version="35" max-version="35"/>
</dependencies>

<background-jobs>
<job>OCA\CloudFederationAPI\BackgroundJob\CleanupExpiredOcmTokensJob</job>
</background-jobs>
</info>
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\CloudFederationAPI\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\CloudFederationAPI\\BackgroundJob\\CleanupExpiredOcmTokensJob' => $baseDir . '/../lib/BackgroundJob/CleanupExpiredOcmTokensJob.php',
'OCA\\CloudFederationAPI\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
'OCA\\CloudFederationAPI\\Config' => $baseDir . '/../lib/Config.php',
'OCA\\CloudFederationAPI\\Controller\\OCMRequestController' => $baseDir . '/../lib/Controller/OCMRequestController.php',
'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => $baseDir . '/../lib/Controller/RequestHandlerController.php',
'OCA\\CloudFederationAPI\\Controller\\TokenController' => $baseDir . '/../lib/Controller/TokenController.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => $baseDir . '/../lib/Db/FederatedInvite.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => $baseDir . '/../lib/Db/FederatedInviteMapper.php',
'OCA\\CloudFederationAPI\\Db\\OcmTokenMap' => $baseDir . '/../lib/Db/OcmTokenMap.php',
'OCA\\CloudFederationAPI\\Db\\OcmTokenMapMapper' => $baseDir . '/../lib/Db/OcmTokenMapMapper.php',
'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => $baseDir . '/../lib/Events/FederatedInviteAcceptedEvent.php',
'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => $baseDir . '/../lib/Migration/Version1016Date202502262004.php',
'OCA\\CloudFederationAPI\\Migration\\Version1017Date20260306120000' => $baseDir . '/../lib/Migration/Version1017Date20260306120000.php',
'OCA\\CloudFederationAPI\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
);
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ class ComposerStaticInitCloudFederationAPI
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\CloudFederationAPI\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\CloudFederationAPI\\BackgroundJob\\CleanupExpiredOcmTokensJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupExpiredOcmTokensJob.php',
'OCA\\CloudFederationAPI\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
'OCA\\CloudFederationAPI\\Config' => __DIR__ . '/..' . '/../lib/Config.php',
'OCA\\CloudFederationAPI\\Controller\\OCMRequestController' => __DIR__ . '/..' . '/../lib/Controller/OCMRequestController.php',
'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => __DIR__ . '/..' . '/../lib/Controller/RequestHandlerController.php',
'OCA\\CloudFederationAPI\\Controller\\TokenController' => __DIR__ . '/..' . '/../lib/Controller/TokenController.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => __DIR__ . '/..' . '/../lib/Db/FederatedInvite.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => __DIR__ . '/..' . '/../lib/Db/FederatedInviteMapper.php',
'OCA\\CloudFederationAPI\\Db\\OcmTokenMap' => __DIR__ . '/..' . '/../lib/Db/OcmTokenMap.php',
'OCA\\CloudFederationAPI\\Db\\OcmTokenMapMapper' => __DIR__ . '/..' . '/../lib/Db/OcmTokenMapMapper.php',
'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/FederatedInviteAcceptedEvent.php',
'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date202502262004.php',
'OCA\\CloudFederationAPI\\Migration\\Version1017Date20260306120000' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20260306120000.php',
'OCA\\CloudFederationAPI\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\CloudFederationAPI\BackgroundJob;

use OCA\CloudFederationAPI\Db\OcmTokenMapMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;

/**
* Periodically purge expired OCM access token mappings from ocm_token_map.
*
* The corresponding oc_authtoken entries (TEMPORARY_TOKEN with an expires
* timestamp) are cleaned up by Nextcloud's own token expiry jobs.
*/
class CleanupExpiredOcmTokensJob extends TimedJob {
public function __construct(
ITimeFactory $timeFactory,
private readonly OcmTokenMapMapper $mapper,
) {
parent::__construct($timeFactory);

$this->setInterval(6 * 60 * 60); // run every 6 hours
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}

#[\Override]
protected function run($argument): void {
$this->mapper->deleteExpired($this->time->getTime());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

namespace OCA\CloudFederationAPI\Controller;

use OC\Authentication\Token\PublicKeyTokenProvider;
use OC\OCM\OCMSignatoryManager;
use OCA\CloudFederationAPI\Config;
use OCA\CloudFederationAPI\Db\FederatedInviteMapper;
use OCA\CloudFederationAPI\Db\OcmTokenMapMapper;
use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent;
use OCA\CloudFederationAPI\ResponseDefinitions;
use OCA\FederatedFileSharing\AddressHandler;
Expand Down Expand Up @@ -43,6 +45,7 @@
use OCP\Security\Signature\Exceptions\SignatoryNotFoundException;
use OCP\Security\Signature\IIncomingSignedRequest;
use OCP\Security\Signature\ISignatureManager;
use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Util;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -91,7 +94,7 @@ public function __construct(
* @param string|null $ownerDisplayName Display name of the user who shared the item
* @param string|null $sharedBy Provider specific UID of the user who shared the resource
* @param string|null $sharedByDisplayName Display name of the user who shared the resource
* @param array{name: list<string>, options: array<string, mixed>} $protocol e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]
* @param array{name: string, options?: array<string, mixed>, webdav?: array<string, mixed>} $protocol Old format: ['name' => 'webdav', 'options' => ['sharedSecret' => '...', 'permissions' => '...']] or New format: ['name' => 'webdav', 'webdav' => ['uri' => '...', 'sharedSecret' => '...', 'permissions' => [...]]] or Multi format: ['name' => 'multi', 'webdav' => [...]]
* @param string $shareType 'group' or 'user' share
* @param string $resourceType 'file', 'calendar',...
*
Expand Down Expand Up @@ -126,9 +129,6 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
|| $shareType === null
|| !is_array($protocol)
|| !isset($protocol['name'])
|| !isset($protocol['options'])
|| !is_array($protocol['options'])
|| !isset($protocol['options']['sharedSecret'])
) {
return new JSONResponse(
[
Expand All @@ -139,6 +139,33 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
);
}

$protocolName = $protocol['name'];
$hasOldFormat = isset($protocol['options']) && is_array($protocol['options']) && isset($protocol['options']['sharedSecret']);
$hasNewFormat = isset($protocol[$protocolName]) && is_array($protocol[$protocolName]) && isset($protocol[$protocolName]['sharedSecret']);

// For multi-protocol, we only consider webdav
$hasMultiFormat = false;
if ($protocolName === 'multi') {
if (isset($protocol['webdav']) && is_array($protocol['webdav']) && isset($protocol['webdav']['sharedSecret'])) {
$hasMultiFormat = true;
$protocol = [
'name' => 'webdav',
'webdav' => $protocol['webdav']
];
$protocolName = 'webdav';
}
}

if (!$hasOldFormat && !$hasNewFormat && !$hasMultiFormat) {
return new JSONResponse(
[
'message' => 'Missing sharedSecret in protocol',
'validationErrors' => [],
],
Http::STATUS_BAD_REQUEST
);
}

$supportedShareTypes = $this->config->getSupportedShareTypes($resourceType);
if (!in_array($shareType, $supportedShareTypes)) {
return new JSONResponse(
Expand All @@ -148,6 +175,7 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
}

$cloudId = $this->cloudIdManager->resolveCloudId($shareWith);
$shareWithCloudId = $shareWith; // preserve full cloud ID for factory capability discovery
$shareWith = $cloudId->getUser();

if ($shareType === 'user') {
Expand Down Expand Up @@ -192,7 +220,10 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $

try {
$provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType);
$share = $this->factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType);
// Pass the original cloud ID so the factory can discover capabilities without warning.
// Then reset shareWith to the local username that shareReceived() needs for user lookup.
$share = $this->factory->getCloudFederationShare($shareWithCloudId, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType);
$share->setShareWith($shareWith);
$share->setProtocol($protocol);
$provider->shareReceived($share);
} catch (ProviderDoesNotExistsException|ProviderCouldNotAddShareException $e) {
Expand Down Expand Up @@ -490,6 +521,12 @@ private function confirmNotificationIdentity(
$provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType);
if ($provider instanceof ISignedCloudFederationProvider || $provider instanceof \NCU\Federation\ISignedCloudFederationProvider) {
$identity = $provider->getFederationIdFromSharedSecret($sharedSecret, $notification);
if ($identity === '') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to know which type of auth it is, instead of guessing, maybe with token-exchange capability

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request we are receiving may be for a share that does not correspond with the remote's capabilities, for example because it is old

$tokenProvider = Server::get(PublicKeyTokenProvider::class);
$accessTokenDb = $tokenProvider->getToken($sharedSecret);
$mapping = Server::get(OcmTokenMapMapper::class)->getByAccessTokenId($accessTokenDb->getId());
$identity = $provider->getFederationIdFromSharedSecret($mapping->getRefreshToken(), $notification);
}
} else {
$this->logger->debug('cloud federation provider {provider} does not implements ISignedCloudFederationProvider', ['provider' => $provider::class]);
return;
Expand Down
Loading