aboutsummaryrefslogtreecommitdiffstats
path: root/apps/oauth2/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/oauth2/lib')
-rw-r--r--apps/oauth2/lib/BackgroundJob/CleanupExpiredAuthorizationCode.php4
-rw-r--r--apps/oauth2/lib/Command/ImportLegacyOcClient.php76
-rw-r--r--apps/oauth2/lib/Controller/LoginRedirectorController.php85
-rw-r--r--apps/oauth2/lib/Controller/OauthApiController.php19
-rw-r--r--apps/oauth2/lib/Controller/SettingsController.php8
-rw-r--r--apps/oauth2/lib/Db/AccessToken.php12
-rw-r--r--apps/oauth2/lib/Db/Client.php4
-rw-r--r--apps/oauth2/lib/Db/ClientMapper.php4
-rw-r--r--apps/oauth2/lib/Migration/SetTokenExpiration.php22
-rw-r--r--apps/oauth2/lib/Migration/Version011901Date20240829164356.php49
-rw-r--r--apps/oauth2/lib/Settings/Admin.php5
11 files changed, 217 insertions, 71 deletions
diff --git a/apps/oauth2/lib/BackgroundJob/CleanupExpiredAuthorizationCode.php b/apps/oauth2/lib/BackgroundJob/CleanupExpiredAuthorizationCode.php
index 84d62ab6b45..b819a45ace2 100644
--- a/apps/oauth2/lib/BackgroundJob/CleanupExpiredAuthorizationCode.php
+++ b/apps/oauth2/lib/BackgroundJob/CleanupExpiredAuthorizationCode.php
@@ -12,7 +12,6 @@ namespace OCA\OAuth2\BackgroundJob;
use OCA\OAuth2\Db\AccessTokenMapper;
use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\Exception;
use Psr\Log\LoggerInterface;
@@ -23,12 +22,11 @@ class CleanupExpiredAuthorizationCode extends TimedJob {
ITimeFactory $timeFactory,
private AccessTokenMapper $accessTokenMapper,
private LoggerInterface $logger,
-
) {
parent::__construct($timeFactory);
// 30 days
$this->setInterval(60 * 60 * 24 * 30);
- $this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
/**
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 f326b821a60..7241b35cdcf 100644
--- a/apps/oauth2/lib/Controller/LoginRedirectorController.php
+++ b/apps/oauth2/lib/Controller/LoginRedirectorController.php
@@ -8,27 +8,27 @@ declare(strict_types=1);
*/
namespace OCA\OAuth2\Controller;
+use OC\Core\Controller\ClientFlowLoginController;
use OCA\OAuth2\Db\ClientMapper;
use OCA\OAuth2\Exceptions\ClientNotFoundException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+use OCP\AppFramework\Http\Attribute\UseSession;
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;
use OCP\IURLGenerator;
+use OCP\Security\ISecureRandom;
+#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
class LoginRedirectorController extends Controller {
- /** @var IURLGenerator */
- private $urlGenerator;
- /** @var ClientMapper */
- private $clientMapper;
- /** @var ISession */
- private $session;
- /** @var IL10N */
- private $l;
-
/**
* @param string $appName
* @param IRequest $request
@@ -37,37 +37,39 @@ class LoginRedirectorController extends Controller {
* @param ISession $session
* @param IL10N $l
*/
- public function __construct(string $appName,
+ public function __construct(
+ string $appName,
IRequest $request,
- IURLGenerator $urlGenerator,
- ClientMapper $clientMapper,
- ISession $session,
- IL10N $l) {
+ private IURLGenerator $urlGenerator,
+ private ClientMapper $clientMapper,
+ private ISession $session,
+ private IL10N $l,
+ private ISecureRandom $random,
+ private IAppConfig $appConfig,
+ private IConfig $config,
+ ) {
parent::__construct($appName, $request);
- $this->urlGenerator = $urlGenerator;
- $this->clientMapper = $clientMapper;
- $this->session = $session;
- $this->l = $l;
}
/**
- * @PublicPage
- * @NoCSRFRequired
- * @UseSession
- *
* Authorize the user
*
* @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
* 303: Redirect to login URL
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[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) {
@@ -83,14 +85,39 @@ 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(),
- ]
- );
+ if (in_array($client->getName(), $this->appConfig->getValueArray('oauth2', 'skipAuthPickerApplications', []))) {
+ /** @see ClientFlowLoginController::showAuthPickerPage **/
+ $stateToken = $this->random->generate(
+ 64,
+ ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS
+ );
+ $this->session->set(ClientFlowLoginController::STATE_NAME, $stateToken);
+ $targetUrl = $this->urlGenerator->linkToRouteAbsolute(
+ 'core.ClientFlowLogin.grantPage',
+ [
+ 'stateToken' => $stateToken,
+ 'clientIdentifier' => $client->getClientIdentifier(),
+ 'providedRedirectUri' => $providedRedirectUri,
+ ]
+ );
+ } else {
+ $targetUrl = $this->urlGenerator->linkToRouteAbsolute(
+ 'core.ClientFlowLogin.showAuthPickerPage',
+ [
+ 'clientIdentifier' => $client->getClientIdentifier(),
+ 'providedRedirectUri' => $providedRedirectUri,
+ ]
+ );
+ }
return new RedirectResponse($targetUrl);
}
}
diff --git a/apps/oauth2/lib/Controller/OauthApiController.php b/apps/oauth2/lib/Controller/OauthApiController.php
index 389bc73811b..11f17fda4bf 100644
--- a/apps/oauth2/lib/Controller/OauthApiController.php
+++ b/apps/oauth2/lib/Controller/OauthApiController.php
@@ -15,6 +15,10 @@ use OCA\OAuth2\Exceptions\AccessTokenNotFoundException;
use OCA\OAuth2\Exceptions\ClientNotFoundException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\BruteForceProtection;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Exceptions\ExpiredTokenException;
@@ -26,6 +30,7 @@ use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;
+#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
class OauthApiController extends Controller {
// the authorization code expires after 10 minutes
public const AUTHORIZATION_CODE_EXPIRES_AFTER = 10 * 60;
@@ -47,10 +52,6 @@ class OauthApiController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
- * @BruteForceProtection(action=oauth2GetToken)
- *
* Get a token
*
* @param string $grant_type Token type that should be granted
@@ -64,9 +65,12 @@ class OauthApiController extends Controller {
* 200: Token returned
* 400: Getting token is not possible
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[BruteForceProtection(action: 'oauth2GetToken')]
public function getToken(
string $grant_type, ?string $code, ?string $refresh_token,
- ?string $client_id, ?string $client_secret
+ ?string $client_id, ?string $client_secret,
): JSONResponse {
// We only handle two types
@@ -136,7 +140,8 @@ class OauthApiController extends Controller {
}
try {
- $storedClientSecret = $this->crypto->decrypt($client->getSecret());
+ $storedClientSecretHash = $client->getSecret();
+ $clientSecretHash = bin2hex($this->crypto->calculateHMAC($client_secret));
} catch (\Exception $e) {
$this->logger->error('OAuth client secret decryption error', ['exception' => $e]);
// we don't throttle here because it might not be a bruteforce attack
@@ -145,7 +150,7 @@ class OauthApiController extends Controller {
], Http::STATUS_BAD_REQUEST);
}
// The client id and secret must match. Else we don't provide an access token!
- if ($client->getClientIdentifier() !== $client_id || $storedClientSecret !== $client_secret) {
+ if ($client->getClientIdentifier() !== $client_id || $storedClientSecretHash !== $clientSecretHash) {
$response = new JSONResponse([
'error' => 'invalid_client',
], Http::STATUS_BAD_REQUEST);
diff --git a/apps/oauth2/lib/Controller/SettingsController.php b/apps/oauth2/lib/Controller/SettingsController.php
index 81e728bd693..9bd02c8a2cd 100644
--- a/apps/oauth2/lib/Controller/SettingsController.php
+++ b/apps/oauth2/lib/Controller/SettingsController.php
@@ -35,7 +35,7 @@ class SettingsController extends Controller {
private IL10N $l,
private IAuthTokenProvider $tokenProvider,
private IUserManager $userManager,
- private ICrypto $crypto
+ private ICrypto $crypto,
) {
parent::__construct($appName, $request);
}
@@ -50,8 +50,8 @@ class SettingsController extends Controller {
$client->setName($name);
$client->setRedirectUri($redirectUri);
$secret = $this->secureRandom->generate(64, self::validChars);
- $encryptedSecret = $this->crypto->encrypt($secret);
- $client->setSecret($encryptedSecret);
+ $hashedSecret = bin2hex($this->crypto->calculateHMAC($secret));
+ $client->setSecret($hashedSecret);
$client->setClientIdentifier($this->secureRandom->generate(64, self::validChars));
$client = $this->clientMapper->insert($client);
@@ -69,7 +69,7 @@ class SettingsController extends Controller {
public function deleteClient(int $id): JSONResponse {
$client = $this->clientMapper->getByUid($id);
- $this->userManager->callForAllUsers(function (IUser $user) use ($client) {
+ $this->userManager->callForSeenUsers(function (IUser $user) use ($client): void {
$this->tokenProvider->invalidateTokensOfUser($user->getUID(), $client->getName());
});
diff --git a/apps/oauth2/lib/Db/AccessToken.php b/apps/oauth2/lib/Db/AccessToken.php
index 543d512e0ce..34adc4f4797 100644
--- a/apps/oauth2/lib/Db/AccessToken.php
+++ b/apps/oauth2/lib/Db/AccessToken.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -6,6 +7,7 @@
namespace OCA\OAuth2\Db;
use OCP\AppFramework\Db\Entity;
+use OCP\DB\Types;
/**
* @method int getTokenId()
@@ -36,12 +38,12 @@ class AccessToken extends Entity {
protected $tokenCount;
public function __construct() {
- $this->addType('id', 'int');
- $this->addType('tokenId', 'int');
- $this->addType('clientId', 'int');
+ $this->addType('id', Types::INTEGER);
+ $this->addType('tokenId', Types::INTEGER);
+ $this->addType('clientId', Types::INTEGER);
$this->addType('hashedCode', 'string');
$this->addType('encryptedToken', 'string');
- $this->addType('codeCreatedAt', 'int');
- $this->addType('tokenCount', 'int');
+ $this->addType('codeCreatedAt', Types::INTEGER);
+ $this->addType('tokenCount', Types::INTEGER);
}
}
diff --git a/apps/oauth2/lib/Db/Client.php b/apps/oauth2/lib/Db/Client.php
index c48ca4edfc0..8fce0040c96 100644
--- a/apps/oauth2/lib/Db/Client.php
+++ b/apps/oauth2/lib/Db/Client.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -6,6 +7,7 @@
namespace OCA\OAuth2\Db;
use OCP\AppFramework\Db\Entity;
+use OCP\DB\Types;
/**
* @method string getClientIdentifier()
@@ -28,7 +30,7 @@ class Client extends Entity {
protected $secret;
public function __construct() {
- $this->addType('id', 'int');
+ $this->addType('id', Types::INTEGER);
$this->addType('name', 'string');
$this->addType('redirectUri', 'string');
$this->addType('clientIdentifier', 'string');
diff --git a/apps/oauth2/lib/Db/ClientMapper.php b/apps/oauth2/lib/Db/ClientMapper.php
index dc19c93c4e1..c5ca2989d0f 100644
--- a/apps/oauth2/lib/Db/ClientMapper.php
+++ b/apps/oauth2/lib/Db/ClientMapper.php
@@ -41,7 +41,7 @@ class ClientMapper extends QBMapper {
try {
$client = $this->findEntity($qb);
} catch (IMapperException $e) {
- throw new ClientNotFoundException('could not find client '.$clientIdentifier, 0, $e);
+ throw new ClientNotFoundException('could not find client ' . $clientIdentifier, 0, $e);
}
return $client;
}
@@ -61,7 +61,7 @@ class ClientMapper extends QBMapper {
try {
$client = $this->findEntity($qb);
} catch (IMapperException $e) {
- throw new ClientNotFoundException('could not find client with id '.$id, 0, $e);
+ throw new ClientNotFoundException('could not find client with id ' . $id, 0, $e);
}
return $client;
}
diff --git a/apps/oauth2/lib/Migration/SetTokenExpiration.php b/apps/oauth2/lib/Migration/SetTokenExpiration.php
index 5077e74be87..dc925e26bb2 100644
--- a/apps/oauth2/lib/Migration/SetTokenExpiration.php
+++ b/apps/oauth2/lib/Migration/SetTokenExpiration.php
@@ -18,21 +18,11 @@ use OCP\Migration\IRepairStep;
class SetTokenExpiration implements IRepairStep {
- /** @var IDBConnection */
- private $connection;
-
- /** @var ITimeFactory */
- private $time;
-
- /** @var TokenProvider */
- private $tokenProvider;
-
- public function __construct(IDBConnection $connection,
- ITimeFactory $timeFactory,
- TokenProvider $tokenProvider) {
- $this->connection = $connection;
- $this->time = $timeFactory;
- $this->tokenProvider = $tokenProvider;
+ public function __construct(
+ private IDBConnection $connection,
+ private ITimeFactory $time,
+ private TokenProvider $tokenProvider,
+ ) {
}
public function getName(): string {
@@ -44,7 +34,7 @@ class SetTokenExpiration implements IRepairStep {
$qb->select('*')
->from('oauth2_access_tokens');
- $cursor = $qb->execute();
+ $cursor = $qb->executeQuery();
while ($row = $cursor->fetch()) {
$token = AccessToken::fromRow($row);
diff --git a/apps/oauth2/lib/Migration/Version011901Date20240829164356.php b/apps/oauth2/lib/Migration/Version011901Date20240829164356.php
new file mode 100644
index 00000000000..20f5754bf11
--- /dev/null
+++ b/apps/oauth2/lib/Migration/Version011901Date20240829164356.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\OAuth2\Migration;
+
+use Closure;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+use OCP\Security\ICrypto;
+
+class Version011901Date20240829164356 extends SimpleMigrationStep {
+
+ public function __construct(
+ private IDBConnection $connection,
+ private ICrypto $crypto,
+ ) {
+ }
+
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ $qbUpdate = $this->connection->getQueryBuilder();
+ $qbUpdate->update('oauth2_clients')
+ ->set('secret', $qbUpdate->createParameter('updateSecret'))
+ ->where(
+ $qbUpdate->expr()->eq('id', $qbUpdate->createParameter('updateId'))
+ );
+
+ $qbSelect = $this->connection->getQueryBuilder();
+ $qbSelect->select('id', 'secret')
+ ->from('oauth2_clients');
+ $req = $qbSelect->executeQuery();
+ while ($row = $req->fetch()) {
+ $id = $row['id'];
+ $storedEncryptedSecret = $row['secret'];
+ $secret = $this->crypto->decrypt($storedEncryptedSecret);
+ $hashedSecret = bin2hex($this->crypto->calculateHMAC($secret));
+ $qbUpdate->setParameter('updateSecret', $hashedSecret, IQueryBuilder::PARAM_STR);
+ $qbUpdate->setParameter('updateId', $id, IQueryBuilder::PARAM_INT);
+ $qbUpdate->executeStatement();
+ }
+ $req->closeCursor();
+ }
+}
diff --git a/apps/oauth2/lib/Settings/Admin.php b/apps/oauth2/lib/Settings/Admin.php
index eae10098a59..93b6b7bcc3f 100644
--- a/apps/oauth2/lib/Settings/Admin.php
+++ b/apps/oauth2/lib/Settings/Admin.php
@@ -12,7 +12,6 @@ use OCA\OAuth2\Db\ClientMapper;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IURLGenerator;
-use OCP\Security\ICrypto;
use OCP\Settings\ISettings;
use Psr\Log\LoggerInterface;
@@ -22,7 +21,6 @@ class Admin implements ISettings {
private IInitialState $initialState,
private ClientMapper $clientMapper,
private IURLGenerator $urlGenerator,
- private ICrypto $crypto,
private LoggerInterface $logger,
) {
}
@@ -33,13 +31,12 @@ class Admin implements ISettings {
foreach ($clients as $client) {
try {
- $secret = $this->crypto->decrypt($client->getSecret());
$result[] = [
'id' => $client->getId(),
'name' => $client->getName(),
'redirectUri' => $client->getRedirectUri(),
'clientId' => $client->getClientIdentifier(),
- 'clientSecret' => $secret,
+ 'clientSecret' => '',
];
} catch (\Exception $e) {
$this->logger->error('[Settings] OAuth client secret decryption error', ['exception' => $e]);