Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/dav/appinfo/v1/webdav.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
$bearerAuthPlugin = new BearerAuth(
Server::get(IUserSession::class),
Server::get(ISession::class),
Server::get(IRequest::class)
Server::get(IRequest::class),
Server::get(IConfig::class),
);
$authPlugin->addBackend($bearerAuthPlugin);

Expand Down
10 changes: 10 additions & 0 deletions apps/dav/lib/Connector/Sabre/BearerAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use OCP\AppFramework\Http;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
Expand All @@ -19,6 +20,7 @@ public function __construct(
private IUserSession $userSession,
private ISession $session,
private IRequest $request,
private IConfig $config,
private string $principalPrefix = 'principals/users/',
) {
// setup realm
Expand Down Expand Up @@ -57,6 +59,14 @@ public function validateBearerToken($bearerToken) {
* @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(Http::STATUS_UNAUTHORIZED);
}
}
3 changes: 2 additions & 1 deletion apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ public function __construct(
$bearerAuthBackend = new BearerAuth(
\OCP\Server::get(IUserSession::class),
\OCP\Server::get(ISession::class),
\OCP\Server::get(IRequest::class)
\OCP\Server::get(IRequest::class),
\OCP\Server::get(IConfig::class),
);
$authPlugin->addBackend($bearerAuthBackend);
// because we are throwing exceptions this plugin has to be the last one
Expand Down
8 changes: 7 additions & 1 deletion apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

use OC\User\Session;
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;
Expand All @@ -28,17 +30,21 @@ class BearerAuthTest extends TestCase {
/** @var BearerAuth */
private $bearerAuth;

private IConfig&MockObject $config;

protected function setUp(): void {
parent::setUp();

$this->userSession = $this->createMock(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,
);
}

Expand Down
4 changes: 4 additions & 0 deletions apps/oauth2/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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,
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;
}
}
15 changes: 14 additions & 1 deletion apps/oauth2/lib/Controller/LoginRedirectorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
Expand All @@ -45,6 +46,7 @@ public function __construct(
private IL10N $l,
private ISecureRandom $random,
private IAppConfig $appConfig,
private IConfig $config,
) {
parent::__construct($appName, $request);
}
Expand All @@ -55,6 +57,7 @@ public function __construct(
* @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
Expand All @@ -65,7 +68,8 @@ public function __construct(
#[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) {
Expand All @@ -81,6 +85,13 @@ 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);

if (in_array($client->getName(), $this->appConfig->getValueArray('oauth2', 'skipAuthPickerApplications', []))) {
Expand All @@ -95,13 +106,15 @@ public function authorize($client_id,
[
'stateToken' => $stateToken,
'clientIdentifier' => $client->getClientIdentifier(),
'providedRedirectUri' => $providedRedirectUri,
]
);
} else {
$targetUrl = $this->urlGenerator->linkToRouteAbsolute(
'core.ClientFlowLogin.showAuthPickerPage',
[
'clientIdentifier' => $client->getClientIdentifier(),
'providedRedirectUri' => $providedRedirectUri,
]
);
}
Expand Down
9 changes: 9 additions & 0 deletions apps/oauth2/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Loading