Merge pull request #47701 from nextcloud/backport/47635/stable28

[stable28] [oauth2] Store hashed secret instead of encrypted
This commit is contained in:
Andy Scherzinger 2024-09-03 17:25:33 +02:00 committed by GitHub
commit c55ca78816
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 127 additions and 64 deletions

View file

@ -5,7 +5,7 @@
<name>OAuth 2.0</name>
<summary>Allows OAuth2 compatible authentication from other web applications.</summary>
<description>The OAuth2 app allows administrators to configure the built-in authentication workflow to also allow OAuth2 compatible authentication from other web applications.</description>
<version>1.16.3</version>
<version>1.16.4</version>
<licence>agpl</licence>
<author>Lukas Reschke</author>
<namespace>OAuth2</namespace>

View file

@ -23,5 +23,6 @@ return array(
'OCA\\OAuth2\\Migration\\Version011601Date20230522143227' => $baseDir . '/../lib/Migration/Version011601Date20230522143227.php',
'OCA\\OAuth2\\Migration\\Version011602Date20230613160650' => $baseDir . '/../lib/Migration/Version011602Date20230613160650.php',
'OCA\\OAuth2\\Migration\\Version011603Date20230620111039' => $baseDir . '/../lib/Migration/Version011603Date20230620111039.php',
'OCA\\OAuth2\\Migration\\Version011901Date20240829164356' => $baseDir . '/../lib/Migration/Version011901Date20240829164356.php',
'OCA\\OAuth2\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
);

View file

@ -38,6 +38,7 @@ class ComposerStaticInitOAuth2
'OCA\\OAuth2\\Migration\\Version011601Date20230522143227' => __DIR__ . '/..' . '/../lib/Migration/Version011601Date20230522143227.php',
'OCA\\OAuth2\\Migration\\Version011602Date20230613160650' => __DIR__ . '/..' . '/../lib/Migration/Version011602Date20230613160650.php',
'OCA\\OAuth2\\Migration\\Version011603Date20230620111039' => __DIR__ . '/..' . '/../lib/Migration/Version011603Date20230620111039.php',
'OCA\\OAuth2\\Migration\\Version011901Date20240829164356' => __DIR__ . '/..' . '/../lib/Migration/Version011901Date20240829164356.php',
'OCA\\OAuth2\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
);

View file

@ -156,7 +156,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
@ -165,7 +166,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);

View file

@ -72,8 +72,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);

View file

@ -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();
}
}

View file

@ -30,7 +30,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;
@ -40,7 +39,6 @@ class Admin implements ISettings {
private IInitialState $initialState,
private ClientMapper $clientMapper,
private IURLGenerator $urlGenerator,
private ICrypto $crypto,
private LoggerInterface $logger,
) {
}
@ -51,13 +49,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]);

View file

@ -50,6 +50,10 @@
@delete="deleteClient" />
</tbody>
</table>
<NcNoteCard v-if="showSecretWarning"
type="warning">
{{ t('oauth2', 'Make sure you store the secret key, it cannot be recovered.') }}
</NcNoteCard>
<br>
<h3>{{ t('oauth2', 'Add client') }}</h3>
@ -83,6 +87,7 @@ import { generateUrl } from '@nextcloud/router'
import { getCapabilities } from '@nextcloud/capabilities'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import { loadState } from '@nextcloud/initial-state'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
@ -93,6 +98,7 @@ export default {
NcSettingsSection,
NcButton,
NcTextField,
NcNoteCard,
},
props: {
clients: {
@ -109,6 +115,7 @@ export default {
error: false,
},
oauthDocLink: loadState('oauth2', 'oauth2-doc-link'),
showSecretWarning: false,
}
},
computed: {
@ -136,6 +143,7 @@ export default {
).then(response => {
// eslint-disable-next-line vue/no-mutating-props
this.clients.push(response.data)
this.showSecretWarning = true
this.newClient.name = ''
this.newClient.redirectUri = ''

View file

@ -27,7 +27,8 @@
<td>
<div class="action-secret">
<code>{{ renderedSecret }}</code>
<NcButton type="tertiary-no-background"
<NcButton v-if="clientSecret !== ''"
type="tertiary-no-background"
:aria-label="toggleAriaLabel"
@click="toggleSecret">
<template #icon>

View file

@ -269,9 +269,20 @@ class OauthApiControllerTest extends TestCase {
->with('validrefresh')
->willReturn($accessToken);
$this->crypto
->method('calculateHMAC')
->with($this->callback(function (string $text) {
return $text === 'clientSecret' || $text === 'invalidClientSecret';
}))
->willReturnCallback(function (string $text) {
return $text === 'clientSecret'
? 'hashedClientSecret'
: 'hashedInvalidClientSecret';
});
$client = new Client();
$client->setClientIdentifier('clientId');
$client->setSecret('clientSecret');
$client->setSecret(bin2hex('hashedClientSecret'));
$this->clientMapper->method('getByUid')
->with(42)
->willReturn($client);
@ -296,21 +307,20 @@ class OauthApiControllerTest extends TestCase {
$client = new Client();
$client->setClientIdentifier('clientId');
$client->setSecret('encryptedClientSecret');
$client->setSecret(bin2hex('hashedClientSecret'));
$this->clientMapper->method('getByUid')
->with(42)
->willReturn($client);
$this->crypto
->method('decrypt')
->with($this->callback(function (string $text) {
return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
}))
->willReturnCallback(function (string $text) {
return $text === 'encryptedClientSecret'
? 'clientSecret'
: ($text === 'encryptedToken' ? 'decryptedToken' : '');
});
->with('encryptedToken')
->willReturn('decryptedToken');
$this->crypto
->method('calculateHMAC')
->with('clientSecret')
->willReturn('hashedClientSecret');
$this->tokenProvider->method('getTokenById')
->with(1337)
@ -335,21 +345,20 @@ class OauthApiControllerTest extends TestCase {
$client = new Client();
$client->setClientIdentifier('clientId');
$client->setSecret('encryptedClientSecret');
$client->setSecret(bin2hex('hashedClientSecret'));
$this->clientMapper->method('getByUid')
->with(42)
->willReturn($client);
$this->crypto
->method('decrypt')
->with($this->callback(function (string $text) {
return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
}))
->willReturnCallback(function (string $text) {
return $text === 'encryptedClientSecret'
? 'clientSecret'
: ($text === 'encryptedToken' ? 'decryptedToken' : '');
});
->with('encryptedToken')
->willReturn('decryptedToken');
$this->crypto
->method('calculateHMAC')
->with('clientSecret')
->willReturn('hashedClientSecret');
$appToken = new PublicKeyToken();
$appToken->setUid('userId');
@ -432,21 +441,20 @@ class OauthApiControllerTest extends TestCase {
$client = new Client();
$client->setClientIdentifier('clientId');
$client->setSecret('encryptedClientSecret');
$client->setSecret(bin2hex('hashedClientSecret'));
$this->clientMapper->method('getByUid')
->with(42)
->willReturn($client);
$this->crypto
->method('decrypt')
->with($this->callback(function (string $text) {
return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
}))
->willReturnCallback(function (string $text) {
return $text === 'encryptedClientSecret'
? 'clientSecret'
: ($text === 'encryptedToken' ? 'decryptedToken' : '');
});
->with('encryptedToken')
->willReturn('decryptedToken');
$this->crypto
->method('calculateHMAC')
->with('clientSecret')
->willReturn('hashedClientSecret');
$appToken = new PublicKeyToken();
$appToken->setUid('userId');
@ -532,21 +540,20 @@ class OauthApiControllerTest extends TestCase {
$client = new Client();
$client->setClientIdentifier('clientId');
$client->setSecret('encryptedClientSecret');
$client->setSecret(bin2hex('hashedClientSecret'));
$this->clientMapper->method('getByUid')
->with(42)
->willReturn($client);
$this->crypto
->method('decrypt')
->with($this->callback(function (string $text) {
return $text === 'encryptedClientSecret' || $text === 'encryptedToken';
}))
->willReturnCallback(function (string $text) {
return $text === 'encryptedClientSecret'
? 'clientSecret'
: ($text === 'encryptedToken' ? 'decryptedToken' : '');
});
->with('encryptedToken')
->willReturn('decryptedToken');
$this->crypto
->method('calculateHMAC')
->with('clientSecret')
->willReturn('hashedClientSecret');
$appToken = new PublicKeyToken();
$appToken->setUid('userId');

View file

@ -102,13 +102,13 @@ class SettingsControllerTest extends TestCase {
$this->crypto
->expects($this->once())
->method('encrypt')
->willReturn('MyEncryptedSecret');
->method('calculateHMAC')
->willReturn('MyHashedSecret');
$client = new Client();
$client->setName('My Client Name');
$client->setRedirectUri('https://example.com/');
$client->setSecret('MySecret');
$client->setSecret(bin2hex('MyHashedSecret'));
$client->setClientIdentifier('MyClientIdentifier');
$this->clientMapper
@ -117,7 +117,7 @@ class SettingsControllerTest extends TestCase {
->with($this->callback(function (Client $c) {
return $c->getName() === 'My Client Name' &&
$c->getRedirectUri() === 'https://example.com/' &&
$c->getSecret() === 'MyEncryptedSecret' &&
$c->getSecret() === bin2hex('MyHashedSecret') &&
$c->getClientIdentifier() === 'MyClientIdentifier';
}))->willReturnCallback(function (Client $c) {
$c->setId(42);
@ -160,7 +160,7 @@ class SettingsControllerTest extends TestCase {
$client->setId(123);
$client->setName('My Client Name');
$client->setRedirectUri('https://example.com/');
$client->setSecret('MySecret');
$client->setSecret(bin2hex('MyHashedSecret'));
$client->setClientIdentifier('MyClientIdentifier');
$this->clientMapper

View file

@ -28,7 +28,6 @@ use OCA\OAuth2\Settings\Admin;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IURLGenerator;
use OCP\Security\ICrypto;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
@ -38,7 +37,7 @@ class AdminTest extends TestCase {
/** @var Admin|MockObject */
private $admin;
/** @var IInitialStateService|MockObject */
/** @var IInitialState|MockObject */
private $initialState;
/** @var ClientMapper|MockObject */
@ -54,7 +53,6 @@ class AdminTest extends TestCase {
$this->initialState,
$this->clientMapper,
$this->createMock(IURLGenerator::class),
$this->createMock(ICrypto::class),
$this->createMock(LoggerInterface::class)
);
}

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long