aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Authentication/WebAuthn/Manager.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Authentication/WebAuthn/Manager.php')
-rw-r--r--lib/private/Authentication/WebAuthn/Manager.php255
1 files changed, 255 insertions, 0 deletions
diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php
new file mode 100644
index 00000000000..96dc0719b54
--- /dev/null
+++ b/lib/private/Authentication/WebAuthn/Manager.php
@@ -0,0 +1,255 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+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;
+use OC\Authentication\WebAuthn\Db\PublicKeyCredentialMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\IConfig;
+use OCP\IUser;
+use Psr\Log\LoggerInterface;
+use Webauthn\AttestationStatement\AttestationObjectLoader;
+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\AuthenticatorSelectionCriteria;
+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 {
+ /** @var CredentialRepository */
+ private $repository;
+
+ /** @var PublicKeyCredentialMapper */
+ private $credentialMapper;
+
+ /** @var LoggerInterface */
+ private $logger;
+
+ /** @var IConfig */
+ private $config;
+
+ public function __construct(
+ CredentialRepository $repository,
+ PublicKeyCredentialMapper $credentialMapper,
+ LoggerInterface $logger,
+ IConfig $config,
+ ) {
+ $this->repository = $repository;
+ $this->credentialMapper = $credentialMapper;
+ $this->logger = $logger;
+ $this->config = $config;
+ }
+
+ 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,
+ );
+ }
+
+ 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
+ );
+ $authenticatorAttestationResponseValidator->setLogger($this->logger);
+
+ try {
+ // Load the data
+ $publicKeyCredential = $publicKeyCredentialLoader->load($data);
+ $response = $publicKeyCredential->response;
+
+ // Check if the response is an Authenticator Attestation Response
+ if (!$response instanceof AuthenticatorAttestationResponse) {
+ throw new \RuntimeException('Not an authenticator attestation response');
+ }
+
+ // Check the response against the request
+ $request = ServerRequest::fromGlobals();
+
+ $publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
+ $response,
+ $publicKeyCredentialCreationOptions,
+ $request,
+ ['localhost'],
+ );
+ } catch (\Throwable $exception) {
+ throw $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);
+ }
+
+ public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions {
+ // 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();
+ return new PublicKeyCredentialDescriptor(
+ $credential->type,
+ $credential->publicKeyCredentialId,
+ );
+ }, $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
+ $userVerificationRequirement,
+ 60000, // Timeout
+ );
+ }
+
+ public function finishAuthentication(PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, string $data, string $uid) {
+ $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());
+
+ $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
+ $this->repository,
+ $tokenBindingHandler,
+ $extensionOutputCheckerHandler,
+ $algorithmManager,
+ );
+ $authenticatorAssertionResponseValidator->setLogger($this->logger);
+
+ try {
+ $this->logger->debug('Loading publickey credentials from: ' . $data);
+
+ // Load the data
+ $publicKeyCredential = $publicKeyCredentialLoader->load($data);
+ $response = $publicKeyCredential->response;
+
+ // Check if the response is an Authenticator Attestation Response
+ if (!$response instanceof AuthenticatorAssertionResponse) {
+ throw new \RuntimeException('Not an authenticator attestation response');
+ }
+
+ // Check the response against the request
+ $request = ServerRequest::fromGlobals();
+
+ $publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
+ $publicKeyCredential->rawId,
+ $response,
+ $publicKeyCredentialRequestOptions,
+ $request,
+ $uid,
+ ['localhost'],
+ );
+ } catch (\Throwable $e) {
+ throw $e;
+ }
+
+ return true;
+ }
+
+ public function deleteRegistration(IUser $user, int $id): void {
+ try {
+ $entry = $this->credentialMapper->findById($user->getUID(), $id);
+ } catch (DoesNotExistException $e) {
+ $this->logger->warning("WebAuthn device $id does not exist, can't delete it");
+ return;
+ }
+
+ $this->credentialMapper->delete($entry);
+ }
+
+ public function isWebAuthnAvailable(): bool {
+ if (!$this->config->getSystemValueBool('auth.webauthn.enabled', true)) {
+ return false;
+ }
+
+ return true;
+ }
+}