diff --git a/core/Migrations/Version30000Date20240815080800.php b/core/Migrations/Version30000Date20240815080800.php new file mode 100644 index 0000000000000..c65cd6830c753 --- /dev/null +++ b/core/Migrations/Version30000Date20240815080800.php @@ -0,0 +1,27 @@ + + * SPDX-FileCopyrightText: 2024 Richard Steinmetz + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Migrations; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version30000Date20240815080800 extends SimpleMigrationStep { + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('webauthn'); + $table->addColumn('user_verification', Types::BOOLEAN, ['notnull' => false, 'default' => false]); + return $schema; + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 7c44d95422349..b58b51e8cbf6d 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1365,6 +1365,7 @@ 'OC\\Core\\Migrations\\Version30000Date20240429122720' => $baseDir . '/core/Migrations/Version30000Date20240429122720.php', 'OC\\Core\\Migrations\\Version30000Date20240708160048' => $baseDir . '/core/Migrations/Version30000Date20240708160048.php', 'OC\\Core\\Migrations\\Version30000Date20240717111406' => $baseDir . '/core/Migrations/Version30000Date20240717111406.php', + 'OC\\Core\\Migrations\\Version30000Date20240815080800' => $baseDir . '/core/Migrations/Version30000Date20240815080800.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 63fa40d39bf78..c0e18224e9cfe 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1398,6 +1398,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Migrations\\Version30000Date20240429122720' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240429122720.php', 'OC\\Core\\Migrations\\Version30000Date20240708160048' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240708160048.php', 'OC\\Core\\Migrations\\Version30000Date20240717111406' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240717111406.php', + 'OC\\Core\\Migrations\\Version30000Date20240815080800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240815080800.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', diff --git a/lib/private/Authentication/WebAuthn/CredentialRepository.php b/lib/private/Authentication/WebAuthn/CredentialRepository.php index f32136f9594c9..203f2ef902011 100644 --- a/lib/private/Authentication/WebAuthn/CredentialRepository.php +++ b/lib/private/Authentication/WebAuthn/CredentialRepository.php @@ -44,7 +44,7 @@ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCre }, $entities); } - public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null): PublicKeyCredentialEntity { + public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity { $oldEntity = null; try { @@ -58,13 +58,18 @@ public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicK $name = 'default'; } - $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource); + $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource, $userVerification); if ($oldEntity) { $entity->setId($oldEntity->getId()); if ($defaultName) { $entity->setName($oldEntity->getName()); } + + // Don't downgrade UV just because it was skipped during a login due to another key + if ($oldEntity->getUserVerification()) { + $entity->setUserVerification(true); + } } return $this->credentialMapper->insertOrUpdate($entity); diff --git a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php index 443a7985cae83..6c4bc3ca81b6c 100644 --- a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php +++ b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php @@ -23,6 +23,10 @@ * @method void setPublicKeyCredentialId(string $id); * @method string getData(); * @method void setData(string $data); + * + * @since 30.0.0 Add userVerification attribute + * @method bool|null getUserVerification(); + * @method void setUserVerification(bool $userVerification); */ class PublicKeyCredentialEntity extends Entity implements JsonSerializable { /** @var string */ @@ -37,20 +41,25 @@ class PublicKeyCredentialEntity extends Entity implements JsonSerializable { /** @var string */ protected $data; + /** @var bool|null */ + protected $userVerification; + public function __construct() { $this->addType('name', 'string'); $this->addType('uid', 'string'); $this->addType('publicKeyCredentialId', 'string'); $this->addType('data', 'string'); + $this->addType('userVerification', 'boolean'); } - public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource): PublicKeyCredentialEntity { + public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource, bool $userVerification): PublicKeyCredentialEntity { $publicKeyCredentialEntity = new self(); $publicKeyCredentialEntity->setName($name); $publicKeyCredentialEntity->setUid($publicKeyCredentialSource->getUserHandle()); $publicKeyCredentialEntity->setPublicKeyCredentialId(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId())); $publicKeyCredentialEntity->setData(json_encode($publicKeyCredentialSource)); + $publicKeyCredentialEntity->setUserVerification($userVerification); return $publicKeyCredentialEntity; } diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php index 007be24599257..7aa7a3c8f3a20 100644 --- a/lib/private/Authentication/WebAuthn/Manager.php +++ b/lib/private/Authentication/WebAuthn/Manager.php @@ -88,8 +88,8 @@ public function startRegistration(IUser $user, string $serverHost): PublicKeyCre ]; $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria( - null, - AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, + AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED, null, false, ); @@ -151,7 +151,8 @@ public function finishRegister(PublicKeyCredentialCreationOptions $publicKeyCred } // Persist the data - return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name); + $userVerification = $response->attestationObject->authData->isUserVerified(); + return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification); } private function stripPort(string $serverHost): string { @@ -160,7 +161,11 @@ private function stripPort(string $serverHost): string { public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions { // List of registered PublicKeyCredentialDescriptor classes associated to the user - $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) { + $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED; + $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) use (&$userVerificationRequirement) { + if ($entity->getUserVerification() !== true) { + $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED; + } $credential = $entity->toPublicKeyCredentialSource(); return new PublicKeyCredentialDescriptor( $credential->type, @@ -173,7 +178,7 @@ public function startAuthentication(string $uid, string $serverHost): PublicKeyC random_bytes(32), // Challenge $this->stripPort($serverHost), // Relying Party ID $registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes - AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + $userVerificationRequirement, 60000, // Timeout ); } diff --git a/version.php b/version.php index e10c7a3985b53..db2f25a6a7d64 100644 --- a/version.php +++ b/version.php @@ -9,7 +9,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level // when updating major/minor version number. -$OC_Version = [30, 0, 0, 8]; +$OC_Version = [30, 0, 0, 9]; // The human-readable string $OC_VersionString = '30.0.0 RC1';