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 10:28:21 +0200
commita34029b9dc5c0cb6a97ae64cc17f2948a3ba919a (patch)
tree70f4f5dd5cbf9ed1f740a6b1e2574b1f6bcc25a3
parent9505010d89ae31b34e4dcc9721c8b4fe28b0e271 (diff)
downloadnextcloud-server-backport/50858/stable29.tar.gz
nextcloud-server-backport/50858/stable29.zip
fix(oauth2): retain support for legacy ownCloud clientsbackport/50858/stable29
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.php7
-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.php19
-rw-r--r--apps/oauth2/openapi.json9
-rw-r--r--apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php80
-rw-r--r--core/Controller/ClientFlowLoginController.php22
-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, 257 insertions, 17 deletions
diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php
index ed02b4c259a..50c39436510 100644
--- a/apps/dav/appinfo/v1/webdav.php
+++ b/apps/dav/appinfo/v1/webdav.php
@@ -67,7 +67,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 6fd61c44b34..b05241c6c44 100644
--- a/apps/dav/lib/Connector/Sabre/BearerAuth.php
+++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php
@@ -23,6 +23,7 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
@@ -34,15 +35,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
@@ -81,6 +85,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 459db0c6699..9f68246583e 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -153,7 +153,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 bfc8d9f9c53..cf8b9a79574 100644
--- a/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
@@ -25,6 +25,7 @@
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;
@@ -45,6 +46,8 @@ class BearerAuthTest extends TestCase {
private $request;
/** @var BearerAuth */
private $bearerAuth;
+ /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
+ private $config;
protected function setUp(): void {
parent::setUp();
@@ -52,11 +55,13 @@ class BearerAuthTest extends TestCase {
$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 30b32312a44..40c816553a1 100644
--- a/apps/oauth2/appinfo/info.xml
+++ b/apps/oauth2/appinfo/info.xml
@@ -29,6 +29,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..ee95c556c4f
--- /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 IConfig $config,
+ private ICrypto $crypto,
+ private 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 4c5b905a1ee..7a85ad1b73a 100644
--- a/apps/oauth2/lib/Controller/LoginRedirectorController.php
+++ b/apps/oauth2/lib/Controller/LoginRedirectorController.php
@@ -34,6 +34,7 @@ use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
+use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
@@ -48,6 +49,7 @@ class LoginRedirectorController extends Controller {
private $session;
/** @var IL10N */
private $l;
+ private IConfig $config;
/**
* @param string $appName
@@ -56,18 +58,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;
}
/**
@@ -80,6 +85,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
@@ -87,7 +93,8 @@ class LoginRedirectorController extends Controller {
*/
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) {
@@ -103,12 +110,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 c745e73bf79..4cfba598580 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 7f45f9c5b4b..f53d23f567f 100644
--- a/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php
+++ b/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php
@@ -31,6 +31,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;
@@ -53,6 +54,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();
@@ -62,6 +65,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',
@@ -69,7 +73,8 @@ class LoginRedirectorControllerTest extends TestCase {
$this->urlGenerator,
$this->clientMapper,
$this->session,
- $this->l
+ $this->l,
+ $this->config,
);
}
@@ -92,9 +97,15 @@ class LoginRedirectorControllerTest extends TestCase {
'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'));
@@ -118,6 +129,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 76079e710e3..7c89618b401 100644
--- a/core/Controller/ClientFlowLoginController.php
+++ b/core/Controller/ClientFlowLoginController.php
@@ -50,6 +50,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;
@@ -79,6 +80,7 @@ class ClientFlowLoginController extends Controller {
private ICrypto $crypto,
private IEventDispatcher $eventDispatcher,
private ITimeFactory $timeFactory,
+ private IConfig $config,
) {
parent::__construct($appName, $request);
}
@@ -115,7 +117,7 @@ class ClientFlowLoginController extends Controller {
*/
#[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 !== '') {
@@ -168,6 +170,7 @@ class ClientFlowLoginController extends Controller {
'oauthState' => $this->session->get('oauth.state'),
'user' => $user,
'direct' => $direct,
+ 'providedRedirectUri' => $providedRedirectUri,
],
'guest'
);
@@ -185,6 +188,7 @@ class ClientFlowLoginController extends Controller {
#[FrontpageRoute(verb: 'GET', url: '/login/flow/grant')]
public function grantPage(string $stateToken = '',
string $clientIdentifier = '',
+ string $providedRedirectUri = '',
int $direct = 0): StandaloneTemplateResponse {
if (!$this->isValidToken($stateToken)) {
return $this->stateTokenForbiddenResponse();
@@ -221,6 +225,7 @@ class ClientFlowLoginController extends Controller {
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'direct' => $direct,
+ 'providedRedirectUri' => $providedRedirectUri,
],
'guest'
);
@@ -237,7 +242,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();
@@ -296,7 +302,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 b0a2efe32bd..eb32b025401 100644
--- a/core/templates/loginflow/authpicker.php
+++ b/core/templates/loginflow/authpicker.php
@@ -46,7 +46,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 6e09be17eeb..b595b788636 100644
--- a/core/templates/loginflow/grant.php
+++ b/core/templates/loginflow/grant.php
@@ -50,6 +50,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 01b8e386833..db366941653 100644
--- a/lib/private/Repair/Owncloud/MigrateOauthTables.php
+++ b/lib/private/Repair/Owncloud/MigrateOauthTables.php
@@ -30,6 +30,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;
@@ -44,6 +45,7 @@ class MigrateOauthTables implements IRepairStep {
private ISecureRandom $random,
private ITimeFactory $timeFactory,
private ICrypto $crypto,
+ private IConfig $config,
) {
}
@@ -184,7 +186,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();
@@ -193,10 +200,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(
@@ -209,10 +218,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 1a3d6ba109a..f2c06dbf073 100644
--- a/tests/Core/Controller/ClientFlowLoginControllerTest.php
+++ b/tests/Core/Controller/ClientFlowLoginControllerTest.php
@@ -34,6 +34,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;
@@ -72,6 +73,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;
@@ -98,6 +101,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',
@@ -113,7 +117,8 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->accessTokenMapper,
$this->crypto,
$this->eventDispatcher,
- $this->timeFactory
+ $this->timeFactory,
+ $this->config,
);
}
@@ -188,7 +193,8 @@ class ClientFlowLoginControllerTest extends TestCase {
'serverHost' => 'https://example.com',
'oauthState' => 'OauthStateToken',
'user' => '',
- 'direct' => 0
+ 'direct' => 0,
+ 'providedRedirectUri' => '',
],
'guest'
);
@@ -258,7 +264,8 @@ class ClientFlowLoginControllerTest extends TestCase {
'serverHost' => 'https://example.com',
'oauthState' => 'OauthStateToken',
'user' => '',
- 'direct' => 0
+ 'direct' => 0,
+ 'providedRedirectUri' => '',
],
'guest'
);