Skip to content

Commit

Permalink
fix(oauth2): retain support for legacy ownCloud clients
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Steinmetz <[email protected]>
  • Loading branch information
st3iny committed Feb 19, 2025
1 parent d43d2b1 commit ed775d1
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 11 deletions.
4 changes: 4 additions & 0 deletions apps/oauth2/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
</post-migration>
</repair-steps>

<commands>
<command>OCA\OAuth2\Command\ImportLegacyOcClient</command>
</commands>

<settings>
<admin>OCA\OAuth2\Settings\Admin</admin>
</settings>
Expand Down
1 change: 1 addition & 0 deletions apps/oauth2/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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',
Expand Down
1 change: 1 addition & 0 deletions apps/oauth2/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
76 changes: 76 additions & 0 deletions apps/oauth2/lib/Command/ImportLegacyOcClient.php
Original file line number Diff line number Diff line change
@@ -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,

Check failure

Code scanning / Psalm

InvalidDocblock Error

Param1 of OCA\OAuth2\Command\ImportLegacyOcClient::__construct has invalid syntax

Check failure

Code scanning / Psalm

ParseError Error

Syntax error, unexpected T_STRING, expecting T_VARIABLE on line 26

Check failure on line 26 in apps/oauth2/lib/Command/ImportLegacyOcClient.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidDocblock

apps/oauth2/lib/Command/ImportLegacyOcClient.php:26:3: InvalidDocblock: Param1 of OCA\OAuth2\Command\ImportLegacyOcClient::__construct has invalid syntax (see https://psalm.dev/008)

Check failure on line 26 in apps/oauth2/lib/Command/ImportLegacyOcClient.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

ParseError

apps/oauth2/lib/Command/ImportLegacyOcClient.php:26:20: ParseError: Syntax error, unexpected T_STRING, expecting T_VARIABLE on line 26 (see https://psalm.dev/173)
private readonly ICrypto $crypto,
private readonly ClientMapper $clientMapper,
) {
parent::__construct();
}

protected function configure(): void {
$this->setName('oauth2:import-legacy-oc-client');
$this->setDescription('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);

Check failure

Code scanning / Psalm

UndefinedThisPropertyFetch Error

Instance property OCA\OAuth2\Command\ImportLegacyOcClient::$config is not defined

Check failure on line 49 in apps/oauth2/lib/Command/ImportLegacyOcClient.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedThisPropertyFetch

apps/oauth2/lib/Command/ImportLegacyOcClient.php:49:10: UndefinedThisPropertyFetch: Instance property OCA\OAuth2\Command\ImportLegacyOcClient::$config is not defined (see https://psalm.dev/041)
}

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));

Check failure

Code scanning / Psalm

UndefinedThisPropertyFetch Error

Instance property OCA\OAuth2\Command\ImportLegacyOcClient::$crypto is not defined

Check failure on line 64 in apps/oauth2/lib/Command/ImportLegacyOcClient.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedThisPropertyFetch

apps/oauth2/lib/Command/ImportLegacyOcClient.php:64:33: UndefinedThisPropertyFetch: Instance property OCA\OAuth2\Command\ImportLegacyOcClient::$crypto is not defined (see https://psalm.dev/041)

$client = new Client();
$client->setName('OwnCloud Desktop Client');
$client->setRedirectUri('http://localhost:*');
$client->setClientIdentifier($clientId);
$client->setSecret($hashedClientSecret);
$this->clientMapper->insert($client);

Check failure

Code scanning / Psalm

UndefinedThisPropertyFetch Error

Instance property OCA\OAuth2\Command\ImportLegacyOcClient::$clientMapper is not defined

Check failure on line 71 in apps/oauth2/lib/Command/ImportLegacyOcClient.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedThisPropertyFetch

apps/oauth2/lib/Command/ImportLegacyOcClient.php:71:3: UndefinedThisPropertyFetch: Instance property OCA\OAuth2\Command\ImportLegacyOcClient::$clientMapper is not defined (see https://psalm.dev/041)

$output->writeln('<info>Client imported successfully</info>');
return 0;
}
}
15 changes: 13 additions & 2 deletions apps/oauth2/lib/Controller/LoginRedirectorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
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;
Expand Down Expand Up @@ -62,7 +63,8 @@ public function __construct(string $appName,
IURLGenerator $urlGenerator,
ClientMapper $clientMapper,
ISession $session,
IL10N $l) {
IL10N $l,
private IConfig $config) {
parent::__construct($appName, $request);
$this->urlGenerator = $urlGenerator;
$this->clientMapper = $clientMapper;
Expand All @@ -87,7 +89,8 @@ public function __construct(string $appName,
*/
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) {
Expand All @@ -103,12 +106,20 @@ public function authorize($client_id,
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);
Expand Down
24 changes: 21 additions & 3 deletions core/Controller/ClientFlowLoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
Expand Down Expand Up @@ -79,6 +80,7 @@ public function __construct(
private ICrypto $crypto,
private IEventDispatcher $eventDispatcher,
private ITimeFactory $timeFactory,
private IConfig $config,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -115,7 +117,7 @@ private function stateTokenForbiddenResponse(): StandaloneTemplateResponse {
*/
#[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 !== '') {
Expand Down Expand Up @@ -168,6 +170,7 @@ public function showAuthPickerPage(string $clientIdentifier = '', string $user =
'oauthState' => $this->session->get('oauth.state'),
'user' => $user,
'direct' => $direct,
'providedRedirectUri' => $providedRedirectUri,
],
'guest'
);
Expand All @@ -185,7 +188,8 @@ public function showAuthPickerPage(string $clientIdentifier = '', string $user =
#[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();
}
Expand Down Expand Up @@ -221,6 +225,7 @@ public function grantPage(string $stateToken = '',
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'direct' => $direct,
'providedRedirectUri' => $providedRedirectUri,
],
'guest'
);
Expand All @@ -237,7 +242,8 @@ public function grantPage(string $stateToken = '',
#[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();
Expand Down Expand Up @@ -296,7 +302,19 @@ public function generateAppPassword(string $stateToken,
$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 .= '&';
Expand Down
3 changes: 2 additions & 1 deletion core/templates/loginflow/authpicker.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<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>
Expand All @@ -62,6 +62,7 @@
</p>
<input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" />
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>">
<input type="hidden" name="providedRedirectUri" value="<?php p($_['providedRedirectUri']) ?>">
<?php if ($_['direct'] !== 0) { ?>
<input type="hidden" name="direct" value="<?php p($_['direct']) ?>">
<?php } ?>
Expand Down
1 change: 1 addition & 0 deletions core/templates/loginflow/grant.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<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 } ?>
Expand Down
21 changes: 16 additions & 5 deletions lib/private/Repair/Owncloud/MigrateOauthTables.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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;
Expand All @@ -44,6 +45,7 @@ public function __construct(
private ISecureRandom $random,
private ITimeFactory $timeFactory,
private ICrypto $crypto,
private IConfig $config,
) {
}

Expand Down Expand Up @@ -184,7 +186,12 @@ public function run(IOutput $output) {
$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();

Expand All @@ -193,10 +200,12 @@ public function run(IOutput $output) {
->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(
Expand All @@ -209,10 +218,12 @@ public function run(IOutput $output) {
$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
Expand Down

0 comments on commit ed775d1

Please sign in to comment.