зеркало из https://github.com/nextcloud/server.git
feat(webauthn): Add user verification to webauthn challenges
Require user verification if all tokens are registered with UV flag, else discourage it Signed-off-by: S1m <git@sgougeon.fr> Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
This commit is contained in:
Родитель
e218d1f98e
Коммит
9189bc290b
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 S1m <git@sgougeon.fr>
|
||||
* SPDX-FileCopyrightText: 2024 Richard Steinmetz <richard@steinmetz.cloud>
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -1366,6 +1366,7 @@ return array(
|
|||
'OC\\Core\\Migrations\\Version30000Date20240708160048' => $baseDir . '/core/Migrations/Version30000Date20240708160048.php',
|
||||
'OC\\Core\\Migrations\\Version30000Date20240717111406' => $baseDir . '/core/Migrations/Version30000Date20240717111406.php',
|
||||
'OC\\Core\\Migrations\\Version30000Date20240814180800' => $baseDir . '/core/Migrations/Version30000Date20240814180800.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',
|
||||
|
|
|
@ -1399,6 +1399,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OC\\Core\\Migrations\\Version30000Date20240708160048' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240708160048.php',
|
||||
'OC\\Core\\Migrations\\Version30000Date20240717111406' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240717111406.php',
|
||||
'OC\\Core\\Migrations\\Version30000Date20240814180800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240814180800.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',
|
||||
|
|
|
@ -44,7 +44,7 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository {
|
|||
}, $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 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository {
|
|||
$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);
|
||||
|
|
|
@ -23,6 +23,10 @@ use Webauthn\PublicKeyCredentialSource;
|
|||
* @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;
|
||||
}
|
||||
|
|
|
@ -88,8 +88,8 @@ class Manager {
|
|||
];
|
||||
|
||||
$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 @@ class Manager {
|
|||
}
|
||||
|
||||
// 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 @@ class Manager {
|
|||
|
||||
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 @@ class Manager {
|
|||
random_bytes(32), // Challenge
|
||||
$this->stripPort($serverHost), // Relying Party ID
|
||||
$registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
|
||||
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
|
||||
$userVerificationRequirement,
|
||||
60000, // Timeout
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 = [31, 0, 0, 0];
|
||||
$OC_Version = [31, 0, 0, 1];
|
||||
|
||||
// The human-readable string
|
||||
$OC_VersionString = '31.0.0 dev';
|
||||
|
|
Загрузка…
Ссылка в новой задаче