Skip to content

Commit b8adb8b

Browse files
Merge pull request #47255 from nextcloud/backport/47253/stable30
[stable30] feat(webauthn): Add user verification to webauthn challenges
2 parents b46f541 + b7bf8ec commit b8adb8b

File tree

7 files changed

+57
-9
lines changed

7 files changed

+57
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2024 S1m <git@sgougeon.fr>
6+
* SPDX-FileCopyrightText: 2024 Richard Steinmetz <richard@steinmetz.cloud>
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OC\Core\Migrations;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\IOutput;
16+
use OCP\Migration\SimpleMigrationStep;
17+
18+
class Version30000Date20240815080800 extends SimpleMigrationStep {
19+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
20+
/** @var ISchemaWrapper $schema */
21+
$schema = $schemaClosure();
22+
23+
$table = $schema->getTable('webauthn');
24+
$table->addColumn('user_verification', Types::BOOLEAN, ['notnull' => false, 'default' => false]);
25+
return $schema;
26+
}
27+
}

lib/composer/composer/autoload_classmap.php

+1
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,7 @@
13651365
'OC\\Core\\Migrations\\Version30000Date20240429122720' => $baseDir . '/core/Migrations/Version30000Date20240429122720.php',
13661366
'OC\\Core\\Migrations\\Version30000Date20240708160048' => $baseDir . '/core/Migrations/Version30000Date20240708160048.php',
13671367
'OC\\Core\\Migrations\\Version30000Date20240717111406' => $baseDir . '/core/Migrations/Version30000Date20240717111406.php',
1368+
'OC\\Core\\Migrations\\Version30000Date20240815080800' => $baseDir . '/core/Migrations/Version30000Date20240815080800.php',
13681369
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
13691370
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
13701371
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',

lib/composer/composer/autoload_static.php

+1
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
13981398
'OC\\Core\\Migrations\\Version30000Date20240429122720' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240429122720.php',
13991399
'OC\\Core\\Migrations\\Version30000Date20240708160048' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240708160048.php',
14001400
'OC\\Core\\Migrations\\Version30000Date20240717111406' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240717111406.php',
1401+
'OC\\Core\\Migrations\\Version30000Date20240815080800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240815080800.php',
14011402
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
14021403
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
14031404
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',

lib/private/Authentication/WebAuthn/CredentialRepository.php

+7-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCre
4444
}, $entities);
4545
}
4646

47-
public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null): PublicKeyCredentialEntity {
47+
public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity {
4848
$oldEntity = null;
4949

5050
try {
@@ -58,13 +58,18 @@ public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicK
5858
$name = 'default';
5959
}
6060

61-
$entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource);
61+
$entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource, $userVerification);
6262

6363
if ($oldEntity) {
6464
$entity->setId($oldEntity->getId());
6565
if ($defaultName) {
6666
$entity->setName($oldEntity->getName());
6767
}
68+
69+
// Don't downgrade UV just because it was skipped during a login due to another key
70+
if ($oldEntity->getUserVerification()) {
71+
$entity->setUserVerification(true);
72+
}
6873
}
6974

7075
return $this->credentialMapper->insertOrUpdate($entity);

lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
* @method void setPublicKeyCredentialId(string $id);
2424
* @method string getData();
2525
* @method void setData(string $data);
26+
*
27+
* @since 30.0.0 Add userVerification attribute
28+
* @method bool|null getUserVerification();
29+
* @method void setUserVerification(bool $userVerification);
2630
*/
2731
class PublicKeyCredentialEntity extends Entity implements JsonSerializable {
2832
/** @var string */
@@ -37,20 +41,25 @@ class PublicKeyCredentialEntity extends Entity implements JsonSerializable {
3741
/** @var string */
3842
protected $data;
3943

44+
/** @var bool|null */
45+
protected $userVerification;
46+
4047
public function __construct() {
4148
$this->addType('name', 'string');
4249
$this->addType('uid', 'string');
4350
$this->addType('publicKeyCredentialId', 'string');
4451
$this->addType('data', 'string');
52+
$this->addType('userVerification', 'boolean');
4553
}
4654

47-
public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource): PublicKeyCredentialEntity {
55+
public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource, bool $userVerification): PublicKeyCredentialEntity {
4856
$publicKeyCredentialEntity = new self();
4957

5058
$publicKeyCredentialEntity->setName($name);
5159
$publicKeyCredentialEntity->setUid($publicKeyCredentialSource->getUserHandle());
5260
$publicKeyCredentialEntity->setPublicKeyCredentialId(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()));
5361
$publicKeyCredentialEntity->setData(json_encode($publicKeyCredentialSource));
62+
$publicKeyCredentialEntity->setUserVerification($userVerification);
5463

5564
return $publicKeyCredentialEntity;
5665
}

lib/private/Authentication/WebAuthn/Manager.php

+10-5
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ public function startRegistration(IUser $user, string $serverHost): PublicKeyCre
8888
];
8989

9090
$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
91-
null,
92-
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
91+
AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
92+
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
9393
null,
9494
false,
9595
);
@@ -151,7 +151,8 @@ public function finishRegister(PublicKeyCredentialCreationOptions $publicKeyCred
151151
}
152152

153153
// Persist the data
154-
return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
154+
$userVerification = $response->attestationObject->authData->isUserVerified();
155+
return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification);
155156
}
156157

157158
private function stripPort(string $serverHost): string {
@@ -160,7 +161,11 @@ private function stripPort(string $serverHost): string {
160161

161162
public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions {
162163
// List of registered PublicKeyCredentialDescriptor classes associated to the user
163-
$registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) {
164+
$userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED;
165+
$registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) use (&$userVerificationRequirement) {
166+
if ($entity->getUserVerification() !== true) {
167+
$userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
168+
}
164169
$credential = $entity->toPublicKeyCredentialSource();
165170
return new PublicKeyCredentialDescriptor(
166171
$credential->type,
@@ -173,7 +178,7 @@ public function startAuthentication(string $uid, string $serverHost): PublicKeyC
173178
random_bytes(32), // Challenge
174179
$this->stripPort($serverHost), // Relying Party ID
175180
$registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
176-
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
181+
$userVerificationRequirement,
177182
60000, // Timeout
178183
);
179184
}

version.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level
1010
// when updating major/minor version number.
1111

12-
$OC_Version = [30, 0, 0, 8];
12+
$OC_Version = [30, 0, 0, 9];
1313

1414
// The human-readable string
1515
$OC_VersionString = '30.0.0 RC1';

0 commit comments

Comments
 (0)