aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Steinmetz <richard@steinmetz.cloud>2025-02-17 14:34:01 +0100
committerRichard Steinmetz <richard@steinmetz.cloud>2025-04-02 12:23:45 +0200
commit550d8d9fce5a4339c582186e07ebcb7427af9a1b (patch)
tree978469bb01690d211f5174df59530b91985d34c1
parentc50698abfbc22f0ef3a8b6f9ab195504c03026f3 (diff)
downloadnextcloud-server-backport/50858/stable30.tar.gz
nextcloud-server-backport/50858/stable30.zip
fix(oauth2): retain support for legacy ownCloud clientsbackport/50858/stable30
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
-rw-r--r--apps/dav/appinfo/v1/webdav.php3
-rw-r--r--apps/dav/lib/Connector/Sabre/BearerAuth.php12
-rw-r--r--apps/dav/lib/Server.php3
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php8
-rw-r--r--apps/oauth2/appinfo/info.xml4
-rw-r--r--apps/oauth2/composer/composer/autoload_classmap.php1
-rw-r--r--apps/oauth2/composer/composer/autoload_static.php1
-rw-r--r--apps/oauth2/lib/Command/ImportLegacyOcClient.php76
-rw-r--r--apps/oauth2/lib/Controller/LoginRedirectorController.php20
-rw-r--r--apps/oauth2/openapi.json9
-rw-r--r--apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php75
-rw-r--r--core/Controller/ClientFlowLoginController.php24
-rw-r--r--core/templates/loginflow/authpicker.php2
-rw-r--r--core/templates/loginflow/grant.php1
-rw-r--r--lib/private/Repair/Owncloud/MigrateOauthTables.php21
-rw-r--r--tests/Core/Controller/ClientFlowLoginControllerTest.php13
16 files changed, 255 insertions, 18 deletions
diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php
index 1683c29ca80..b3d2e45eba0 100644
--- a/apps/dav/appinfo/v1/webdav.php
+++ b/apps/dav/appinfo/v1/webdav.php
@@ -44,7 +44,8 @@ $authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
$bearerAuthPlugin = new \OCA\DAV\Connector\Sabre\BearerAuth(
\OC::$server->getUserSession(),
\OC::$server->getSession(),
- \OC::$server->getRequest()
+ \OC::$server->getRequest(),
+ \OC::$server->getConfig(),
);
$authPlugin->addBackend($bearerAuthPlugin);
diff --git a/apps/dav/lib/Connector/Sabre/BearerAuth.php b/apps/dav/lib/Connector/Sabre/BearerAuth.php
index 8caae8dced9..ac25160901e 100644
--- a/apps/dav/lib/Connector/Sabre/BearerAuth.php
+++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php
@@ -5,6 +5,7 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
@@ -16,15 +17,18 @@ class BearerAuth extends AbstractBearer {
private IUserSession $userSession;
private ISession $session;
private IRequest $request;
+ private IConfig $config;
private string $principalPrefix;
public function __construct(IUserSession $userSession,
ISession $session,
IRequest $request,
+ IConfig $config,
$principalPrefix = 'principals/users/') {
$this->userSession = $userSession;
$this->session = $session;
$this->request = $request;
+ $this->config = $config;
$this->principalPrefix = $principalPrefix;
// setup realm
@@ -63,6 +67,14 @@ class BearerAuth extends AbstractBearer {
* @param ResponseInterface $response
*/
public function challenge(RequestInterface $request, ResponseInterface $response): void {
+ // Legacy ownCloud clients still authenticate via OAuth2
+ $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false);
+ $userAgent = $request->getHeader('User-Agent');
+ if ($enableOcClients && $userAgent !== null && str_contains($userAgent, 'mirall')) {
+ parent::challenge($request, $response);
+ return;
+ }
+
$response->setStatus(401);
}
}
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index 8e3c6cef6a5..6ead145b8aa 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -128,7 +128,8 @@ class Server {
$bearerAuthBackend = new BearerAuth(
\OC::$server->getUserSession(),
\OC::$server->getSession(),
- \OC::$server->getRequest()
+ \OC::$server->getRequest(),
+ \OC::$server->getConfig(),
);
$authPlugin->addBackend($bearerAuthBackend);
// because we are throwing exceptions this plugin has to be the last one
diff --git a/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
index 9922dee5e6e..92d5c95f4c7 100644
--- a/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
@@ -6,10 +6,12 @@
namespace OCA\DAV\Tests\unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\BearerAuth;
+use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Test\TestCase;
@@ -27,17 +29,21 @@ class BearerAuthTest extends TestCase {
/** @var BearerAuth */
private $bearerAuth;
+ private IConfig&MockObject $config;
+
protected function setUp(): void {
parent::setUp();
$this->userSession = $this->createMock(\OC\User\Session::class);
$this->session = $this->createMock(ISession::class);
$this->request = $this->createMock(IRequest::class);
+ $this->config = $this->createMock(IConfig::class);
$this->bearerAuth = new BearerAuth(
$this->userSession,
$this->session,
- $this->request
+ $this->request,
+ $this->config,
);
}
diff --git a/apps/oauth2/appinfo/info.xml b/apps/oauth2/appinfo/info.xml
index 0d0128b8c13..b06932902bf 100644
--- a/apps/oauth2/appinfo/info.xml
+++ b/apps/oauth2/appinfo/info.xml
@@ -33,6 +33,10 @@
</post-migration>
</repair-steps>
+ <commands>
+ <command>OCA\OAuth2\Command\ImportLegacyOcClient</command>
+ </commands>
+
<settings>
<admin>OCA\OAuth2\Settings\Admin</admin>
</settings>
diff --git a/apps/oauth2/composer/composer/autoload_classmap.php b/apps/oauth2/composer/composer/autoload_classmap.php
index cba0ef43dfc..f5fc5bfe251 100644
--- a/apps/oauth2/composer/composer/autoload_classmap.php
+++ b/apps/oauth2/composer/composer/autoload_classmap.php
@@ -8,6 +8,7 @@ $baseDir = $vendorDir;
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\OAuth2\\BackgroundJob\\CleanupExpiredAuthorizationCode' => $baseDir . '/../lib/BackgroundJob/CleanupExpiredAuthorizationCode.php',
+ 'OCA\\OAuth2\\Command\\ImportLegacyOcClient' => $baseDir . '/../lib/Command/ImportLegacyOcClient.php',
'OCA\\OAuth2\\Controller\\LoginRedirectorController' => $baseDir . '/../lib/Controller/LoginRedirectorController.php',
'OCA\\OAuth2\\Controller\\OauthApiController' => $baseDir . '/../lib/Controller/OauthApiController.php',
'OCA\\OAuth2\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php',
diff --git a/apps/oauth2/composer/composer/autoload_static.php b/apps/oauth2/composer/composer/autoload_static.php
index c4905bc96c4..c4843fa1990 100644
--- a/apps/oauth2/composer/composer/autoload_static.php
+++ b/apps/oauth2/composer/composer/autoload_static.php
@@ -23,6 +23,7 @@ class ComposerStaticInitOAuth2
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\OAuth2\\BackgroundJob\\CleanupExpiredAuthorizationCode' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupExpiredAuthorizationCode.php',
+ 'OCA\\OAuth2\\Command\\ImportLegacyOcClient' => __DIR__ . '/..' . '/../lib/Command/ImportLegacyOcClient.php',
'OCA\\OAuth2\\Controller\\LoginRedirectorController' => __DIR__ . '/..' . '/../lib/Controller/LoginRedirectorController.php',
'OCA\\OAuth2\\Controller\\OauthApiController' => __DIR__ . '/..' . '/../lib/Controller/OauthApiController.php',
'OCA\\OAuth2\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php',
diff --git a/apps/oauth2/lib/Command/ImportLegacyOcClient.php b/apps/oauth2/lib/Command/ImportLegacyOcClient.php
new file mode 100644
index 00000000000..acdc57cf991
--- /dev/null
+++ b/apps/oauth2/lib/Command/ImportLegacyOcClient.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\OAuth2\Command;
+
+use OCA\OAuth2\Db\Client;
+use OCA\OAuth2\Db\ClientMapper;
+use OCP\IConfig;
+use OCP\Security\ICrypto;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ImportLegacyOcClient extends Command {
+ private const ARGUMENT_CLIENT_ID = 'client-id';
+ private const ARGUMENT_CLIENT_SECRET = 'client-secret';
+
+ public function __construct(
+ private readonly IConfig $config,
+ private readonly ICrypto $crypto,
+ private readonly ClientMapper $clientMapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->setName('oauth2:import-legacy-oc-client');
+ $this->setDescription('This command is only required to be run on instances which were migrated from ownCloud without the oauth2.enable_oc_clients system config! Import a legacy Oauth2 client from an ownCloud instance and migrate it. The data is expected to be straight out of the database table oc_oauth2_clients.');
+ $this->addArgument(
+ self::ARGUMENT_CLIENT_ID,
+ InputArgument::REQUIRED,
+ 'Value of the "identifier" column',
+ );
+ $this->addArgument(
+ self::ARGUMENT_CLIENT_SECRET,
+ InputArgument::REQUIRED,
+ 'Value of the "secret" column',
+ );
+ }
+
+ public function isEnabled(): bool {
+ return $this->config->getSystemValueBool('oauth2.enable_oc_clients', false);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ /** @var string $clientId */
+ $clientId = $input->getArgument(self::ARGUMENT_CLIENT_ID);
+
+ /** @var string $clientSecret */
+ $clientSecret = $input->getArgument(self::ARGUMENT_CLIENT_SECRET);
+
+ // Should not happen but just to be sure
+ if (empty($clientId) || empty($clientSecret)) {
+ return 1;
+ }
+
+ $hashedClientSecret = bin2hex($this->crypto->calculateHMAC($clientSecret));
+
+ $client = new Client();
+ $client->setName('ownCloud Desktop Client');
+ $client->setRedirectUri('http://localhost:*');
+ $client->setClientIdentifier($clientId);
+ $client->setSecret($hashedClientSecret);
+ $this->clientMapper->insert($client);
+
+ $output->writeln('<info>Client imported successfully</info>');
+ return 0;
+ }
+}
diff --git a/apps/oauth2/lib/Controller/LoginRedirectorController.php b/apps/oauth2/lib/Controller/LoginRedirectorController.php
index d9a9ed5c5d0..7dbe6d902e4 100644
--- a/apps/oauth2/lib/Controller/LoginRedirectorController.php
+++ b/apps/oauth2/lib/Controller/LoginRedirectorController.php
@@ -17,6 +17,7 @@ use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\UseSession;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
@@ -31,6 +32,8 @@ class LoginRedirectorController extends Controller {
private $session;
/** @var IL10N */
private $l;
+ /** @var IConfig */
+ private $config;
/**
* @param string $appName
@@ -39,18 +42,21 @@ class LoginRedirectorController extends Controller {
* @param ClientMapper $clientMapper
* @param ISession $session
* @param IL10N $l
+ * @param IConfig $config
*/
public function __construct(string $appName,
IRequest $request,
IURLGenerator $urlGenerator,
ClientMapper $clientMapper,
ISession $session,
- IL10N $l) {
+ IL10N $l,
+ IConfig $config) {
parent::__construct($appName, $request);
$this->urlGenerator = $urlGenerator;
$this->clientMapper = $clientMapper;
$this->session = $session;
$this->l = $l;
+ $this->config = $config;
}
/**
@@ -59,6 +65,7 @@ class LoginRedirectorController extends Controller {
* @param string $client_id Client ID
* @param string $state State of the flow
* @param string $response_type Response type for the flow
+ * @param string $redirect_uri URI to redirect to after the flow (is only used for legacy ownCloud clients)
* @return TemplateResponse<Http::STATUS_OK, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
*
* 200: Client not found
@@ -69,7 +76,8 @@ class LoginRedirectorController extends Controller {
#[UseSession]
public function authorize($client_id,
$state,
- $response_type): TemplateResponse|RedirectResponse {
+ $response_type,
+ string $redirect_uri = ''): TemplateResponse|RedirectResponse {
try {
$client = $this->clientMapper->getByIdentifier($client_id);
} catch (ClientNotFoundException $e) {
@@ -85,12 +93,20 @@ class LoginRedirectorController extends Controller {
return new RedirectResponse($url);
}
+ $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false);
+
+ $providedRedirectUri = '';
+ if ($enableOcClients && $client->getRedirectUri() === 'http://localhost:*') {
+ $providedRedirectUri = $redirect_uri;
+ }
+
$this->session->set('oauth.state', $state);
$targetUrl = $this->urlGenerator->linkToRouteAbsolute(
'core.ClientFlowLogin.showAuthPickerPage',
[
'clientIdentifier' => $client->getClientIdentifier(),
+ 'providedRedirectUri' => $providedRedirectUri,
]
);
return new RedirectResponse($targetUrl);
diff --git a/apps/oauth2/openapi.json b/apps/oauth2/openapi.json
index cdf52de3f5e..76cd2da3150 100644
--- a/apps/oauth2/openapi.json
+++ b/apps/oauth2/openapi.json
@@ -65,6 +65,15 @@
"schema": {
"type": "string"
}
+ },
+ {
+ "name": "redirect_uri",
+ "in": "query",
+ "description": "URI to redirect to after the flow (is only used for legacy ownCloud clients)",
+ "schema": {
+ "type": "string",
+ "default": ""
+ }
}
],
"responses": {
diff --git a/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php b/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php
index eb8e47a102f..dde50374cdf 100644
--- a/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php
+++ b/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php
@@ -11,6 +11,7 @@ use OCA\OAuth2\Db\ClientMapper;
use OCA\OAuth2\Exceptions\ClientNotFoundException;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
@@ -33,6 +34,8 @@ class LoginRedirectorControllerTest extends TestCase {
private $loginRedirectorController;
/** @var IL10N */
private $l;
+ /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
+ private $config;
protected function setUp(): void {
parent::setUp();
@@ -42,6 +45,7 @@ class LoginRedirectorControllerTest extends TestCase {
$this->clientMapper = $this->createMock(ClientMapper::class);
$this->session = $this->createMock(ISession::class);
$this->l = $this->createMock(IL10N::class);
+ $this->config = $this->createMock(IConfig::class);
$this->loginRedirectorController = new LoginRedirectorController(
'oauth2',
@@ -49,7 +53,8 @@ class LoginRedirectorControllerTest extends TestCase {
$this->urlGenerator,
$this->clientMapper,
$this->session,
- $this->l
+ $this->l,
+ $this->config,
);
}
@@ -72,6 +77,7 @@ class LoginRedirectorControllerTest extends TestCase {
'core.ClientFlowLogin.showAuthPickerPage',
[
'clientIdentifier' => 'MyClientIdentifier',
+ 'providedRedirectUri' => '',
]
)
->willReturn('https://example.com/?clientIdentifier=foo');
@@ -98,6 +104,73 @@ class LoginRedirectorControllerTest extends TestCase {
$this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'wrongcode'));
}
+ public function testAuthorizeWithLegacyOcClient(): void {
+ $client = new Client();
+ $client->setClientIdentifier('MyClientIdentifier');
+ $client->setRedirectUri('http://localhost:*');
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('getByIdentifier')
+ ->with('MyClientId')
+ ->willReturn($client);
+ $this->session
+ ->expects($this->once())
+ ->method('set')
+ ->with('oauth.state', 'MyState');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with(
+ 'core.ClientFlowLogin.showAuthPickerPage',
+ [
+ 'clientIdentifier' => 'MyClientIdentifier',
+ 'providedRedirectUri' => 'http://localhost:30000',
+ ]
+ )
+ ->willReturn('https://example.com/?clientIdentifier=foo&providedRedirectUri=http://localhost:30000');
+ $this->config
+ ->expects($this->once())
+ ->method('getSystemValueBool')
+ ->with('oauth2.enable_oc_clients', false)
+ ->willReturn(true);
+
+ $expected = new RedirectResponse('https://example.com/?clientIdentifier=foo&providedRedirectUri=http://localhost:30000');
+ $this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'code', 'http://localhost:30000'));
+ }
+
+ public function testAuthorizeNotForwardingUntrustedURIs(): void {
+ $client = new Client();
+ $client->setClientIdentifier('MyClientIdentifier');
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('getByIdentifier')
+ ->with('MyClientId')
+ ->willReturn($client);
+ $this->session
+ ->expects($this->once())
+ ->method('set')
+ ->with('oauth.state', 'MyState');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with(
+ 'core.ClientFlowLogin.showAuthPickerPage',
+ [
+ 'clientIdentifier' => 'MyClientIdentifier',
+ 'providedRedirectUri' => '',
+ ]
+ )
+ ->willReturn('https://example.com/?clientIdentifier=foo');
+ $this->config
+ ->expects($this->once())
+ ->method('getSystemValueBool')
+ ->with('oauth2.enable_oc_clients', false)
+ ->willReturn(false);
+
+ $expected = new RedirectResponse('https://example.com/?clientIdentifier=foo');
+ $this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'code', 'http://untrusted-uri.com'));
+ }
+
public function testClientNotFound() {
$clientNotFound = new ClientNotFoundException('could not find client test123', 0);
$this->clientMapper
diff --git a/core/Controller/ClientFlowLoginController.php b/core/Controller/ClientFlowLoginController.php
index ccf70cc9d30..fa4b4d83d00 100644
--- a/core/Controller/ClientFlowLoginController.php
+++ b/core/Controller/ClientFlowLoginController.php
@@ -26,6 +26,7 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
@@ -55,6 +56,7 @@ class ClientFlowLoginController extends Controller {
private ICrypto $crypto,
private IEventDispatcher $eventDispatcher,
private ITimeFactory $timeFactory,
+ private IConfig $config,
) {
parent::__construct($appName, $request);
}
@@ -89,7 +91,7 @@ class ClientFlowLoginController extends Controller {
#[NoCSRFRequired]
#[UseSession]
#[FrontpageRoute(verb: 'GET', url: '/login/flow')]
- public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0): StandaloneTemplateResponse {
+ public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0, string $providedRedirectUri = ''): StandaloneTemplateResponse {
$clientName = $this->getClientName();
$client = null;
if ($clientIdentifier !== '') {
@@ -142,6 +144,7 @@ class ClientFlowLoginController extends Controller {
'oauthState' => $this->session->get('oauth.state'),
'user' => $user,
'direct' => $direct,
+ 'providedRedirectUri' => $providedRedirectUri,
],
'guest'
);
@@ -159,7 +162,8 @@ class ClientFlowLoginController extends Controller {
#[FrontpageRoute(verb: 'GET', url: '/login/flow/grant')]
public function grantPage(string $stateToken = '',
string $clientIdentifier = '',
- int $direct = 0): StandaloneTemplateResponse {
+ int $direct = 0,
+ string $providedRedirectUri = ''): StandaloneTemplateResponse {
if (!$this->isValidToken($stateToken)) {
return $this->stateTokenForbiddenResponse();
}
@@ -195,6 +199,7 @@ class ClientFlowLoginController extends Controller {
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'direct' => $direct,
+ 'providedRedirectUri' => $providedRedirectUri,
],
'guest'
);
@@ -210,7 +215,8 @@ class ClientFlowLoginController extends Controller {
#[UseSession]
#[FrontpageRoute(verb: 'POST', url: '/login/flow')]
public function generateAppPassword(string $stateToken,
- string $clientIdentifier = '') {
+ string $clientIdentifier = '',
+ string $providedRedirectUri = '') {
if (!$this->isValidToken($stateToken)) {
$this->session->remove(self::STATE_NAME);
return $this->stateTokenForbiddenResponse();
@@ -269,7 +275,19 @@ class ClientFlowLoginController extends Controller {
$accessToken->setCodeCreatedAt($this->timeFactory->now()->getTimestamp());
$this->accessTokenMapper->insert($accessToken);
+ $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false);
+
$redirectUri = $client->getRedirectUri();
+ if ($enableOcClients && $redirectUri === 'http://localhost:*') {
+ // Sanity check untrusted redirect URI provided by the client first
+ if (!preg_match('/^http:\/\/localhost:[0-9]+$/', $providedRedirectUri)) {
+ $response = new Response();
+ $response->setStatus(Http::STATUS_FORBIDDEN);
+ return $response;
+ }
+
+ $redirectUri = $providedRedirectUri;
+ }
if (parse_url($redirectUri, PHP_URL_QUERY)) {
$redirectUri .= '&';
diff --git a/core/templates/loginflow/authpicker.php b/core/templates/loginflow/authpicker.php
index 94aace60cba..47e3113604d 100644
--- a/core/templates/loginflow/authpicker.php
+++ b/core/templates/loginflow/authpicker.php
@@ -31,7 +31,7 @@ $urlGenerator = $_['urlGenerator'];
<br/>
<p id="redirect-link">
- <form id="login-form" action="<?php p($urlGenerator->linkToRoute('core.ClientFlowLogin.grantPage', ['stateToken' => $_['stateToken'], 'clientIdentifier' => $_['clientIdentifier'], 'oauthState' => $_['oauthState'], 'user' => $_['user'], 'direct' => $_['direct']])) ?>" method="get">
+ <form id="login-form" action="<?php p($urlGenerator->linkToRoute('core.ClientFlowLogin.grantPage', ['stateToken' => $_['stateToken'], 'clientIdentifier' => $_['clientIdentifier'], 'oauthState' => $_['oauthState'], 'user' => $_['user'], 'direct' => $_['direct'], 'providedRedirectUri' => $_['providedRedirectUri']])) ?>" method="get">
<input type="submit" class="login primary icon-confirm-white" value="<?php p($l->t('Log in')) ?>" disabled>
</form>
</p>
diff --git a/core/templates/loginflow/grant.php b/core/templates/loginflow/grant.php
index dd2fb8d1a8e..6beafccc96e 100644
--- a/core/templates/loginflow/grant.php
+++ b/core/templates/loginflow/grant.php
@@ -35,6 +35,7 @@ $urlGenerator = $_['urlGenerator'];
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" />
<input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" />
<input type="hidden" name="oauthState" value="<?php p($_['oauthState']) ?>" />
+ <input type="hidden" name="providedRedirectUri" value="<?php p($_['providedRedirectUri']) ?>">
<?php if ($_['direct']) { ?>
<input type="hidden" name="direct" value="1" />
<?php } ?>
diff --git a/lib/private/Repair/Owncloud/MigrateOauthTables.php b/lib/private/Repair/Owncloud/MigrateOauthTables.php
index 79f76cf1c9f..466b8d7adb7 100644
--- a/lib/private/Repair/Owncloud/MigrateOauthTables.php
+++ b/lib/private/Repair/Owncloud/MigrateOauthTables.php
@@ -15,6 +15,7 @@ use OCA\OAuth2\Db\AccessTokenMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Token\IToken;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use OCP\Security\ICrypto;
@@ -29,6 +30,7 @@ class MigrateOauthTables implements IRepairStep {
private ISecureRandom $random,
private ITimeFactory $timeFactory,
private ICrypto $crypto,
+ private IConfig $config,
) {
}
@@ -169,7 +171,12 @@ class MigrateOauthTables implements IRepairStep {
$schema = new SchemaWrapper($this->db);
}
- $output->info('Delete clients (and their related access tokens) with the redirect_uri starting with oc:// or ending with *');
+ $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false);
+ if ($enableOcClients) {
+ $output->info('Delete clients (and their related access tokens) with the redirect_uri starting with oc://');
+ } else {
+ $output->info('Delete clients (and their related access tokens) with the redirect_uri starting with oc:// or ending with *');
+ }
// delete the access tokens
$qbDeleteAccessTokens = $this->db->getQueryBuilder();
@@ -178,10 +185,12 @@ class MigrateOauthTables implements IRepairStep {
->from('oauth2_clients')
->where(
$qbSelectClientId->expr()->iLike('redirect_uri', $qbDeleteAccessTokens->createNamedParameter('oc://%', IQueryBuilder::PARAM_STR))
- )
- ->orWhere(
+ );
+ if (!$enableOcClients) {
+ $qbSelectClientId->orWhere(
$qbSelectClientId->expr()->iLike('redirect_uri', $qbDeleteAccessTokens->createNamedParameter('%*', IQueryBuilder::PARAM_STR))
);
+ }
$qbDeleteAccessTokens->delete('oauth2_access_tokens')
->where(
@@ -194,10 +203,12 @@ class MigrateOauthTables implements IRepairStep {
$qbDeleteClients->delete('oauth2_clients')
->where(
$qbDeleteClients->expr()->iLike('redirect_uri', $qbDeleteClients->createNamedParameter('oc://%', IQueryBuilder::PARAM_STR))
- )
- ->orWhere(
+ );
+ if (!$enableOcClients) {
+ $qbDeleteClients->orWhere(
$qbDeleteClients->expr()->iLike('redirect_uri', $qbDeleteClients->createNamedParameter('%*', IQueryBuilder::PARAM_STR))
);
+ }
$qbDeleteClients->executeStatement();
// Migrate legacy refresh tokens from oc
diff --git a/tests/Core/Controller/ClientFlowLoginControllerTest.php b/tests/Core/Controller/ClientFlowLoginControllerTest.php
index 98f09f0f18c..377cfbf2dda 100644
--- a/tests/Core/Controller/ClientFlowLoginControllerTest.php
+++ b/tests/Core/Controller/ClientFlowLoginControllerTest.php
@@ -19,6 +19,7 @@ use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
@@ -57,6 +58,8 @@ class ClientFlowLoginControllerTest extends TestCase {
private $eventDispatcher;
/** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
private $timeFactory;
+ /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
+ private $config;
/** @var ClientFlowLoginController */
private $clientFlowLoginController;
@@ -83,6 +86,7 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->crypto = $this->createMock(ICrypto::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->config = $this->createMock(IConfig::class);
$this->clientFlowLoginController = new ClientFlowLoginController(
'core',
@@ -98,7 +102,8 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->accessTokenMapper,
$this->crypto,
$this->eventDispatcher,
- $this->timeFactory
+ $this->timeFactory,
+ $this->config,
);
}
@@ -173,7 +178,8 @@ class ClientFlowLoginControllerTest extends TestCase {
'serverHost' => 'https://example.com',
'oauthState' => 'OauthStateToken',
'user' => '',
- 'direct' => 0
+ 'direct' => 0,
+ 'providedRedirectUri' => '',
],
'guest'
);
@@ -243,7 +249,8 @@ class ClientFlowLoginControllerTest extends TestCase {
'serverHost' => 'https://example.com',
'oauthState' => 'OauthStateToken',
'user' => '',
- 'direct' => 0
+ 'direct' => 0,
+ 'providedRedirectUri' => '',
],
'guest'
);