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); + } }