diff --git a/3rdparty b/3rdparty
index 34fbffa593cce..e762d06895c88 160000
--- a/3rdparty
+++ b/3rdparty
@@ -1 +1 @@
-Subproject commit 34fbffa593cced9b497267f3fa622da6e36304b2
+Subproject commit e762d06895c887834521c983fb14cf19f2cacaa9
diff --git a/apps/settings/lib/Controller/WebAuthnController.php b/apps/settings/lib/Controller/WebAuthnController.php
index 9efd386c02222..d36cc12913b74 100644
--- a/apps/settings/lib/Controller/WebAuthnController.php
+++ b/apps/settings/lib/Controller/WebAuthnController.php
@@ -19,12 +19,13 @@
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
use OCP\AppFramework\Http\Attribute\UseSession;
+use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Http\Response;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
-use Webauthn\PublicKeyCredentialCreationOptions;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class WebAuthnController extends Controller {
@@ -45,7 +46,7 @@ public function __construct(
#[PasswordConfirmationRequired]
#[UseSession]
#[NoCSRFRequired]
- public function startRegistration(): JSONResponse {
+ public function startRegistration(): Response {
$this->logger->debug('Starting WebAuthn registration');
$credentialOptions = $this->manager->startRegistration($this->userSession->getUser(), $this->request->getServerHost());
@@ -53,7 +54,9 @@ public function startRegistration(): JSONResponse {
// Set this in the session since we need it on finish
$this->session->set(self::WEBAUTHN_REGISTRATION, $credentialOptions);
- return new JSONResponse($credentialOptions);
+ $response = new DataDisplayResponse($credentialOptions);
+ $response->addHeader('Content-Type', 'application/json; charset=utf-8');
+ return $response;
}
#[NoSubAdminRequired]
@@ -69,11 +72,10 @@ public function finishRegistration(string $name, string $data): JSONResponse {
}
// Obtain the publicKeyCredentialOptions from when we started the registration
- $publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::createFromArray($this->session->get(self::WEBAUTHN_REGISTRATION));
-
+ $registrationOptions = $this->session->get(self::WEBAUTHN_REGISTRATION);
$this->session->remove(self::WEBAUTHN_REGISTRATION);
- return new JSONResponse($this->manager->finishRegister($publicKeyCredentialCreationOptions, $name, $data));
+ return new JSONResponse($this->manager->finishRegister($registrationOptions, $name, $data));
}
#[NoSubAdminRequired]
diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index 324b33a7ac673..6f440d2621058 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -2337,11 +2337,6 @@
-
-
- session->get(self::WEBAUTHN_REGISTRATION))]]>
-
-
@@ -3205,7 +3200,6 @@
- session->get(self::WEBAUTHN_LOGIN))]]>
logger->debug('Starting WebAuthn login');
$this->logger->debug('Converting login name to UID');
@@ -57,10 +58,12 @@ public function startAuthentication(string $loginName): JSONResponse {
$this->logger->debug('Got UID: ' . $uid);
$publicKeyCredentialRequestOptions = $this->webAuthnManger->startAuthentication($uid, $this->request->getServerHost());
- $this->session->set(self::WEBAUTHN_LOGIN, json_encode($publicKeyCredentialRequestOptions));
+ $this->session->set(self::WEBAUTHN_LOGIN, $publicKeyCredentialRequestOptions);
$this->session->set(self::WEBAUTHN_LOGIN_UID, $uid);
- return new JSONResponse($publicKeyCredentialRequestOptions);
+ $response = new DataDisplayResponse($publicKeyCredentialRequestOptions);
+ $response->addHeader('Content-Type', 'application/json; charset=utf-8');
+ return $response;
}
#[PublicPage]
@@ -75,7 +78,7 @@ public function finishAuthentication(string $data): JSONResponse {
}
// Obtain the publicKeyCredentialOptions from when we started the registration
- $publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($this->session->get(self::WEBAUTHN_LOGIN));
+ $publicKeyCredentialRequestOptions = $this->session->get(self::WEBAUTHN_LOGIN);
$uid = $this->session->get(self::WEBAUTHN_LOGIN_UID);
$authenticatorData = $this->webAuthnManger->finishAuthentication($publicKeyCredentialRequestOptions, $data, $uid);
diff --git a/lib/private/Authentication/WebAuthn/CredentialRepository.php b/lib/private/Authentication/WebAuthn/CredentialRepository.php
index a58dfa6444c74..4c592703bcfe1 100644
--- a/lib/private/Authentication/WebAuthn/CredentialRepository.php
+++ b/lib/private/Authentication/WebAuthn/CredentialRepository.php
@@ -12,44 +12,47 @@
use OC\Authentication\WebAuthn\Db\PublicKeyCredentialEntity;
use OC\Authentication\WebAuthn\Db\PublicKeyCredentialMapper;
use OCP\AppFramework\Db\IMapperException;
-use Webauthn\PublicKeyCredentialSource;
-use Webauthn\PublicKeyCredentialSourceRepository;
+use Webauthn\AttestationStatement\AttestationStatementSupportManager;
+use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
+use Webauthn\CredentialRecord;
+use Webauthn\Denormalizer\WebauthnSerializerFactory;
use Webauthn\PublicKeyCredentialUserEntity;
-class CredentialRepository implements PublicKeyCredentialSourceRepository {
+class CredentialRepository {
+ private WebauthnSerializerFactory $serializerFactory;
+
public function __construct(
private PublicKeyCredentialMapper $credentialMapper,
) {
+ $attestationStatementSupportManager = AttestationStatementSupportManager::create();
+ $attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
+ $this->serializerFactory = new WebauthnSerializerFactory($attestationStatementSupportManager);
}
- #[\Override]
- public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource {
+ public function findOneByCredentialId(string $publicKeyCredentialId): ?CredentialRecord {
try {
$entity = $this->credentialMapper->findOneByCredentialId($publicKeyCredentialId);
- return $entity->toPublicKeyCredentialSource();
- } catch (IMapperException $e) {
+ return $this->mapToCredentialRecord($entity);
+ } catch (IMapperException) {
return null;
}
}
/**
- * @return PublicKeyCredentialSource[]
+ * @return CredentialRecord[]
*/
- #[\Override]
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
- $uid = $publicKeyCredentialUserEntity->getId();
+ $uid = $publicKeyCredentialUserEntity->id;
$entities = $this->credentialMapper->findAllForUid($uid);
- return array_map(function (PublicKeyCredentialEntity $entity) {
- return $entity->toPublicKeyCredentialSource();
- }, $entities);
+ return array_map($this->mapToCredentialRecord(...), $entities);
}
- public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity {
+ public function saveCredentialSource(CredentialRecord $credentialRecord, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity {
$oldEntity = null;
try {
- $oldEntity = $this->credentialMapper->findOneByCredentialId($publicKeyCredentialSource->getPublicKeyCredentialId());
+ $oldEntity = $this->credentialMapper->findOneByCredentialId($credentialRecord->publicKeyCredentialId);
} catch (IMapperException $e) {
}
@@ -59,7 +62,13 @@ public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicK
$name = 'default';
}
- $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource, $userVerification);
+ $credentialId = base64_encode($credentialRecord->publicKeyCredentialId);
+ $entity = new PublicKeyCredentialEntity();
+ $entity->setName($name);
+ $entity->setUid($credentialRecord->userHandle);
+ $entity->setUserVerification($userVerification);
+ $entity->setPublicKeyCredentialId($credentialId);
+ $entity->setData($this->serializeCredentialRecord($credentialRecord));
if ($oldEntity) {
$entity->setId($oldEntity->getId());
@@ -76,8 +85,17 @@ public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicK
return $this->credentialMapper->insertOrUpdate($entity);
}
- #[\Override]
- public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null): void {
- $this->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
+ public function mapToCredentialRecord(PublicKeyCredentialEntity $entity): CredentialRecord {
+ $serializer = $this->serializerFactory->create();
+ return $serializer->deserialize(
+ $entity->getData(),
+ CredentialRecord::class,
+ 'json',
+ );
+ }
+
+ private function serializeCredentialRecord(CredentialRecord $credentialRecord): string {
+ $serializer = $this->serializerFactory->create();
+ return $serializer->serialize($credentialRecord, 'json');
}
}
diff --git a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php
index b3a780cb8e4b0..3c82d7c91e388 100644
--- a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php
+++ b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php
@@ -11,7 +11,6 @@
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
-use Webauthn\PublicKeyCredentialSource;
/**
* @since 19.0.0
@@ -24,26 +23,21 @@
* @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);
+ *
+ * @since 30.0.0 Add userVerification attribute
*/
class PublicKeyCredentialEntity extends Entity implements JsonSerializable {
- /** @var string */
- protected $name;
+ protected ?string $name = null;
- /** @var string */
- protected $uid;
+ protected ?string $uid = null;
- /** @var string */
- protected $publicKeyCredentialId;
+ protected ?string $publicKeyCredentialId = null;
- /** @var string */
- protected $data;
+ protected ?string $data = null;
- /** @var bool|null */
- protected $userVerification;
+ protected ?bool $userVerification = null;
public function __construct() {
$this->addType('name', 'string');
@@ -53,24 +47,6 @@ public function __construct() {
$this->addType('userVerification', 'boolean');
}
- 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;
- }
-
- public function toPublicKeyCredentialSource(): PublicKeyCredentialSource {
- return PublicKeyCredentialSource::createFromArray(
- json_decode($this->getData(), true)
- );
- }
-
/**
* @inheritDoc
*/
diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php
index b32184add015c..8c37fc825475a 100644
--- a/lib/private/Authentication/WebAuthn/Manager.php
+++ b/lib/private/Authentication/WebAuthn/Manager.php
@@ -9,8 +9,6 @@
namespace OC\Authentication\WebAuthn;
-use Cose\Algorithm\Signature\ECDSA\ES256;
-use Cose\Algorithm\Signature\RSA\RS256;
use Cose\Algorithms;
use GuzzleHttp\Psr7\ServerRequest;
use OC\Authentication\WebAuthn\Db\PublicKeyCredentialEntity;
@@ -19,103 +17,92 @@
use OCP\IConfig;
use OCP\IUser;
use Psr\Log\LoggerInterface;
-use Webauthn\AttestationStatement\AttestationObjectLoader;
+use RuntimeException;
+use Symfony\Component\Serializer\Encoder\JsonEncode;
+use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
-use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\AuthenticatorData;
use Webauthn\AuthenticatorSelectionCriteria;
+use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
+use Webauthn\Denormalizer\WebauthnSerializerFactory;
+use Webauthn\PublicKeyCredential;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialDescriptor;
-use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
-use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
class Manager {
+ private const int TIMEOUT = 60000;
+
+ private const array SUPPORTED_ALGORITHMS = [
+ // ECDSA using the P-256 curve and SHA-256; widely supported by hardware keys and platform authenticators.
+ Algorithms::COSE_ALGORITHM_ES256,
+ // EdDSA with the Ed25519 curve; modern and efficient signature algorithm, but less widely supported.
+ Algorithms::COSE_ALGORITHM_EDDSA,
+ // RSASSA-PSS with SHA-256; modern and more secure RSA alternative. (PSS padding is more secure than PKCS#1 v1.5)
+ Algorithms::COSE_ALGORITHM_PS256,
+ // RSA Signature with PKCS#1 v1.5 padding and SHA-256; legacy standard included for broader compatibility.
+ Algorithms::COSE_ALGORITHM_RS256,
+ ];
+
+ private WebauthnSerializerFactory $serializerFactory;
+
public function __construct(
private CredentialRepository $repository,
private PublicKeyCredentialMapper $credentialMapper,
private LoggerInterface $logger,
private IConfig $config,
) {
+ $attestationStatementSupportManager = AttestationStatementSupportManager::create();
+ $attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
+ $this->serializerFactory = new WebauthnSerializerFactory($attestationStatementSupportManager);
}
- public function startRegistration(IUser $user, string $serverHost): PublicKeyCredentialCreationOptions {
- $rpEntity = new PublicKeyCredentialRpEntity(
- 'Nextcloud', //Name
- $this->stripPort($serverHost), //ID
- null //Icon
- );
-
- $userEntity = new PublicKeyCredentialUserEntity(
- $user->getUID(), // Name
- $user->getUID(), // ID
- $user->getDisplayName() // Display name
- // 'https://foo.example.co/avatar/123e4567-e89b-12d3-a456-426655440000' //Icon
- );
-
- $challenge = random_bytes(32);
-
- $publicKeyCredentialParametersList = [
- new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256),
- new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_RS256),
- ];
-
- $timeout = 60000;
-
- $excludedPublicKeyDescriptors = [
- ];
-
- $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
- AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
- AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
- null,
- false,
- );
-
- return new PublicKeyCredentialCreationOptions(
- $rpEntity,
- $userEntity,
- $challenge,
- $publicKeyCredentialParametersList,
- $authenticatorSelectionCriteria,
- PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
- $excludedPublicKeyDescriptors,
- $timeout,
+ /**
+ * Start a Webauthn registration
+ *
+ * @param IUser $user - The user for which the registration is being started
+ * @param string $serverHost - The server host (used to determine the relying party ID)
+ * @return string The registration options to be sent to the client, serialized as a JSON string {@see https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson}
+ */
+ public function startRegistration(IUser $user, string $serverHost): string {
+ $options = $this->getRegistrationOptions($user, $serverHost, random_bytes(32));
+ $serializer = $this->serializerFactory->create();
+ return $serializer->serialize(
+ $options,
+ 'json',
+ [
+ AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
+ JsonEncode::OPTIONS => JSON_THROW_ON_ERROR,
+ ],
);
}
- public function finishRegister(PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, string $name, string $data): PublicKeyCredentialEntity {
- $tokenBindingHandler = new TokenBindingNotSupportedHandler();
-
- $attestationStatementSupportManager = new AttestationStatementSupportManager();
- $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
-
- $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
- $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
-
- // Extension Output Checker Handler
- $extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
-
- // Authenticator Attestation Response Validator
- $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
- $attestationStatementSupportManager,
- $this->repository,
- $tokenBindingHandler,
- $extensionOutputCheckerHandler
- );
+ /**
+ * Finish the Webauthn registration
+ *
+ * @param string $registrationOptions - The registration options that were sent to the client, serialized as a JSON string {@see https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson}
+ * @param string $name - The name of the credential to be saved
+ * @param string $data - The data returned from the client, serialized as a JSON string {@see https://w3c.github.io/webauthn/#typedefdef-publickeycredentialjson}
+ * @throws RuntimeException - If the registration options or data are invalid
+ */
+ public function finishRegister(string $registrationOptions, string $name, string $data): PublicKeyCredentialEntity {
+ $csmFactory = new CeremonyStepManagerFactory();
+ $creationCSM = $csmFactory->creationCeremony();
+ $authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create($creationCSM);
$authenticatorAttestationResponseValidator->setLogger($this->logger);
try {
- // Load the data
- $publicKeyCredential = $publicKeyCredentialLoader->load($data);
+ $serializer = $this->serializerFactory->create();
+ $registrationOptions = $serializer->deserialize($registrationOptions, PublicKeyCredentialCreationOptions::class, 'json');
+ $publicKeyCredential = $serializer->deserialize($data, PublicKeyCredential::class, 'json');
$response = $publicKeyCredential->response;
// Check if the response is an Authenticator Attestation Response
@@ -128,31 +115,33 @@ public function finishRegister(PublicKeyCredentialCreationOptions $publicKeyCred
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
$response,
- $publicKeyCredentialCreationOptions,
- $request,
- ['localhost'],
+ $registrationOptions,
+ $request->getUri()->getHost(),
);
- } catch (\Throwable $exception) {
- throw $exception;
+ } catch (\UnexpectedValueException $exception) {
+ throw new \RuntimeException('Invalid registration options or data', previous: $exception);
}
// Persist the data
$userVerification = $response->attestationObject->authData->isUserVerified();
- return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification);
- }
-
- private function stripPort(string $serverHost): string {
- return preg_replace('/(:\d+$)/', '', $serverHost);
+ return $this->repository->saveCredentialSource($publicKeyCredentialSource, $name, $userVerification);
}
- public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions {
+ /**
+ * Start Webauthn authentication
+ *
+ * @param string $uid - The user ID for which the authentication is being started
+ * @param string $serverHost - The server host (used to determine the relying party ID)
+ * @return string The authentication options to be sent to the client, serialized as a JSON string {@see https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson}
+ */
+ public function startAuthentication(string $uid, string $serverHost): string {
// List of registered PublicKeyCredentialDescriptor classes associated to the user
$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();
+ $credential = $this->repository->mapToCredentialRecord($entity);
return new PublicKeyCredentialDescriptor(
$credential->type,
$credential->publicKeyCredentialId,
@@ -160,41 +149,43 @@ public function startAuthentication(string $uid, string $serverHost): PublicKeyC
}, $this->credentialMapper->findAllForUid($uid));
// Public Key Credential Request Options
- return new PublicKeyCredentialRequestOptions(
- random_bytes(32), // Challenge
- $this->stripPort($serverHost), // Relying Party ID
- $registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
+ $options = new PublicKeyCredentialRequestOptions(
+ random_bytes(32),
+ $this->stripPort($serverHost),
+ $registeredPublicKeyCredentialDescriptors,
$userVerificationRequirement,
- 60000, // Timeout
+ self::TIMEOUT,
);
- }
-
- public function finishAuthentication(PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, string $data, string $uid): AuthenticatorData {
- $attestationStatementSupportManager = new AttestationStatementSupportManager();
- $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
- $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
- $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
-
- $tokenBindingHandler = new TokenBindingNotSupportedHandler();
- $extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
- $algorithmManager = new \Cose\Algorithm\Manager();
- $algorithmManager->add(new ES256());
- $algorithmManager->add(new RS256());
+ $serializer = $this->serializerFactory->create();
+ return $serializer->serialize(
+ $options,
+ 'json',
+ [
+ AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
+ JsonEncode::OPTIONS => JSON_THROW_ON_ERROR,
+ ]);
+ }
- $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
- $this->repository,
- $tokenBindingHandler,
- $extensionOutputCheckerHandler,
- $algorithmManager,
- );
- $authenticatorAssertionResponseValidator->setLogger($this->logger);
+ /**
+ * Finish authentication of a Webauthn request
+ * @param string $requestOptions - The authentication options that were sent to the client, serialized as a JSON string {@see https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson}
+ * @param string $data - The data returned from the client, serialized as a JSON string {@see https://w3c.github.io/webauthn/#typedefdef-publickeycredentialjson}
+ * @param string $uid - The user ID for which the authentication is being finished
+ */
+ public function finishAuthentication(string $requestOptions, string $data, string $uid): AuthenticatorData {
+ $csmFactory = new CeremonyStepManagerFactory();
+ $assertionCSM = $csmFactory->requestCeremony();
+ $authenticatorAttestationResponseValidator = AuthenticatorAssertionResponseValidator::create($assertionCSM);
+ $authenticatorAttestationResponseValidator->setLogger($this->logger);
try {
$this->logger->debug('Loading publickey credentials from: ' . $data);
// Load the data
- $publicKeyCredential = $publicKeyCredentialLoader->load($data);
+ $serializer = $this->serializerFactory->create();
+ $publicKeyCredentialRequestOptions = $serializer->deserialize($requestOptions, PublicKeyCredentialRequestOptions::class, 'json');
+ $publicKeyCredential = $serializer->deserialize($data, PublicKeyCredential::class, 'json');
$response = $publicKeyCredential->response;
// Check if the response is an Authenticator Attestation Response
@@ -202,17 +193,22 @@ public function finishAuthentication(PublicKeyCredentialRequestOptions $publicKe
throw new \RuntimeException('Not an authenticator attestation response');
}
+ $record = $this->repository->findOneByCredentialId($publicKeyCredential->rawId);
+ if ($record === null) {
+ throw new \RuntimeException('No credential found for the given ID');
+ }
+
// Check the response against the request
$request = ServerRequest::fromGlobals();
- $publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
- $publicKeyCredential->rawId,
+ $updatedRecord = $authenticatorAttestationResponseValidator->check(
+ $record,
$response,
$publicKeyCredentialRequestOptions,
- $request,
+ $request->getUri()->getHost(),
$uid,
- ['localhost'],
);
+ $this->repository->saveCredentialSource($updatedRecord);
} catch (\Throwable $e) {
throw $e;
}
@@ -220,6 +216,12 @@ public function finishAuthentication(PublicKeyCredentialRequestOptions $publicKe
return $response->authenticatorData;
}
+ /**
+ * Delete a WebAuthn registration
+ *
+ * @param IUser $user - The user for which the registration is being deleted
+ * @param int $id - The ID of the registration to be deleted
+ */
public function deleteRegistration(IUser $user, int $id): void {
try {
$entry = $this->credentialMapper->findById($user->getUID(), $id);
@@ -231,6 +233,9 @@ public function deleteRegistration(IUser $user, int $id): void {
$this->credentialMapper->delete($entry);
}
+ /**
+ * Check if WebAuthn is available
+ */
public function isWebAuthnAvailable(): bool {
if (!$this->config->getSystemValueBool('auth.webauthn.enabled', true)) {
return false;
@@ -238,4 +243,38 @@ public function isWebAuthnAvailable(): bool {
return true;
}
+
+ protected function getRegistrationOptions(IUser $user, string $serverHost, ?string $challenge = null): PublicKeyCredentialCreationOptions {
+ $rpEntity = new PublicKeyCredentialRpEntity('Nextcloud', $this->stripPort($serverHost));
+ $userEntity = new PublicKeyCredentialUserEntity(
+ $user->getUID(),
+ $user->getUID(),
+ $user->getDisplayName(),
+ );
+
+ $publicKeyCredentialParametersList = array_map(
+ fn (int $algorithm) => new PublicKeyCredentialParameters(PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $algorithm),
+ self::SUPPORTED_ALGORITHMS,
+ );
+
+ $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
+ AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
+ AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
+ null,
+ );
+
+ return new PublicKeyCredentialCreationOptions(
+ $rpEntity,
+ $userEntity,
+ $challenge,
+ $publicKeyCredentialParametersList,
+ $authenticatorSelectionCriteria,
+ PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
+ timeout: self::TIMEOUT,
+ );
+ }
+
+ private function stripPort(string $serverHost): string {
+ return preg_replace('/(:\d+$)/', '', $serverHost);
+ }
}