diff options
author | Richard Steinmetz <richard@steinmetz.cloud> | 2025-02-17 14:34:01 +0100 |
---|---|---|
committer | Richard Steinmetz <richard@steinmetz.cloud> | 2025-04-02 12:23:45 +0200 |
commit | 550d8d9fce5a4339c582186e07ebcb7427af9a1b (patch) | |
tree | 978469bb01690d211f5174df59530b91985d34c1 | |
parent | c50698abfbc22f0ef3a8b6f9ab195504c03026f3 (diff) | |
download | nextcloud-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.php | 3 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/BearerAuth.php | 12 | ||||
-rw-r--r-- | apps/dav/lib/Server.php | 3 | ||||
-rw-r--r-- | apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php | 8 | ||||
-rw-r--r-- | apps/oauth2/appinfo/info.xml | 4 | ||||
-rw-r--r-- | apps/oauth2/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/oauth2/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/oauth2/lib/Command/ImportLegacyOcClient.php | 76 | ||||
-rw-r--r-- | apps/oauth2/lib/Controller/LoginRedirectorController.php | 20 | ||||
-rw-r--r-- | apps/oauth2/openapi.json | 9 | ||||
-rw-r--r-- | apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php | 75 | ||||
-rw-r--r-- | core/Controller/ClientFlowLoginController.php | 24 | ||||
-rw-r--r-- | core/templates/loginflow/authpicker.php | 2 | ||||
-rw-r--r-- | core/templates/loginflow/grant.php | 1 | ||||
-rw-r--r-- | lib/private/Repair/Owncloud/MigrateOauthTables.php | 21 | ||||
-rw-r--r-- | tests/Core/Controller/ClientFlowLoginControllerTest.php | 13 |
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' ); |