aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private
diff options
context:
space:
mode:
authorRoeland Jago Douma <rullzer@users.noreply.github.com>2020-03-31 22:55:13 +0200
committerGitHub <noreply@github.com>2020-03-31 22:55:13 +0200
commit590849e4d7a61760da499f63789e8d31ece81d24 (patch)
tree3a4b2f2484be7af1b9c3871ea29bd6d964386ead /lib/private
parentb39fb55ee0b4bc4c4bd0fcedca3f836a17586495 (diff)
parent53db05a1f67fc974dba904ec158b2d67fa72df95 (diff)
downloadnextcloud-server-590849e4d7a61760da499f63789e8d31ece81d24.tar.gz
nextcloud-server-590849e4d7a61760da499f63789e8d31ece81d24.zip
Merge pull request #19858 from nextcloud/feature/webauthn
Add WebAuthn support
Diffstat (limited to 'lib/private')
-rw-r--r--lib/private/Authentication/Login/CreateSessionTokenCommand.php36
-rw-r--r--lib/private/Authentication/Login/LoginData.php4
-rw-r--r--lib/private/Authentication/Login/WebAuthnChain.php96
-rw-r--r--lib/private/Authentication/Login/WebAuthnLoginCommand.php48
-rw-r--r--lib/private/Authentication/WebAuthn/CredentialRepository.php93
-rw-r--r--lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php92
-rw-r--r--lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialMapper.php86
-rw-r--r--lib/private/Authentication/WebAuthn/Manager.php269
8 files changed, 711 insertions, 13 deletions
diff --git a/lib/private/Authentication/Login/CreateSessionTokenCommand.php b/lib/private/Authentication/Login/CreateSessionTokenCommand.php
index fbc8215e67f..05b6c27f565 100644
--- a/lib/private/Authentication/Login/CreateSessionTokenCommand.php
+++ b/lib/private/Authentication/Login/CreateSessionTokenCommand.php
@@ -51,17 +51,31 @@ class CreateSessionTokenCommand extends ALoginCommand {
$tokenType = IToken::DO_NOT_REMEMBER;
}
- $this->userSession->createSessionToken(
- $loginData->getRequest(),
- $loginData->getUser()->getUID(),
- $loginData->getUsername(),
- $loginData->getPassword(),
- $tokenType
- );
- $this->userSession->updateTokens(
- $loginData->getUser()->getUID(),
- $loginData->getPassword()
- );
+ if ($loginData->getPassword() === '') {
+ $this->userSession->createSessionToken(
+ $loginData->getRequest(),
+ $loginData->getUser()->getUID(),
+ $loginData->getUsername(),
+ null,
+ $tokenType
+ );
+ $this->userSession->updateTokens(
+ $loginData->getUser()->getUID(),
+ ''
+ );
+ } else {
+ $this->userSession->createSessionToken(
+ $loginData->getRequest(),
+ $loginData->getUser()->getUID(),
+ $loginData->getUsername(),
+ $loginData->getPassword(),
+ $tokenType
+ );
+ $this->userSession->updateTokens(
+ $loginData->getUser()->getUID(),
+ $loginData->getPassword()
+ );
+ }
return $this->processNextOrFinishSuccessfully($loginData);
}
diff --git a/lib/private/Authentication/Login/LoginData.php b/lib/private/Authentication/Login/LoginData.php
index 3249c44a29a..ec8ebdbab46 100644
--- a/lib/private/Authentication/Login/LoginData.php
+++ b/lib/private/Authentication/Login/LoginData.php
@@ -56,7 +56,7 @@ class LoginData {
public function __construct(IRequest $request,
string $username,
- string $password,
+ ?string $password,
string $redirectUrl = null,
string $timeZone = '',
string $timeZoneOffset = '') {
@@ -80,7 +80,7 @@ class LoginData {
return $this->username;
}
- public function getPassword(): string {
+ public function getPassword(): ?string {
return $this->password;
}
diff --git a/lib/private/Authentication/Login/WebAuthnChain.php b/lib/private/Authentication/Login/WebAuthnChain.php
new file mode 100644
index 00000000000..dfc6943e853
--- /dev/null
+++ b/lib/private/Authentication/Login/WebAuthnChain.php
@@ -0,0 +1,96 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Authentication\Login;
+
+class WebAuthnChain {
+ /** @var UserDisabledCheckCommand */
+ private $userDisabledCheckCommand;
+
+ /** @var LoggedInCheckCommand */
+ private $loggedInCheckCommand;
+
+ /** @var CompleteLoginCommand */
+ private $completeLoginCommand;
+
+ /** @var CreateSessionTokenCommand */
+ private $createSessionTokenCommand;
+
+ /** @var ClearLostPasswordTokensCommand */
+ private $clearLostPasswordTokensCommand;
+
+ /** @var UpdateLastPasswordConfirmCommand */
+ private $updateLastPasswordConfirmCommand;
+
+ /** @var SetUserTimezoneCommand */
+ private $setUserTimezoneCommand;
+
+ /** @var TwoFactorCommand */
+ private $twoFactorCommand;
+
+ /** @var FinishRememberedLoginCommand */
+ private $finishRememberedLoginCommand;
+
+ /** @var WebAuthnLoginCommand */
+ private $webAuthnLoginCommand;
+
+ public function __construct(UserDisabledCheckCommand $userDisabledCheckCommand,
+ WebAuthnLoginCommand $webAuthnLoginCommand,
+ LoggedInCheckCommand $loggedInCheckCommand,
+ CompleteLoginCommand $completeLoginCommand,
+ CreateSessionTokenCommand $createSessionTokenCommand,
+ ClearLostPasswordTokensCommand $clearLostPasswordTokensCommand,
+ UpdateLastPasswordConfirmCommand $updateLastPasswordConfirmCommand,
+ SetUserTimezoneCommand $setUserTimezoneCommand,
+ TwoFactorCommand $twoFactorCommand,
+ FinishRememberedLoginCommand $finishRememberedLoginCommand
+ ) {
+ $this->userDisabledCheckCommand = $userDisabledCheckCommand;
+ $this->webAuthnLoginCommand = $webAuthnLoginCommand;
+ $this->loggedInCheckCommand = $loggedInCheckCommand;
+ $this->completeLoginCommand = $completeLoginCommand;
+ $this->createSessionTokenCommand = $createSessionTokenCommand;
+ $this->clearLostPasswordTokensCommand = $clearLostPasswordTokensCommand;
+ $this->updateLastPasswordConfirmCommand = $updateLastPasswordConfirmCommand;
+ $this->setUserTimezoneCommand = $setUserTimezoneCommand;
+ $this->twoFactorCommand = $twoFactorCommand;
+ $this->finishRememberedLoginCommand = $finishRememberedLoginCommand;
+ }
+
+ public function process(LoginData $loginData): LoginResult {
+ $chain = $this->userDisabledCheckCommand;
+ $chain
+ ->setNext($this->webAuthnLoginCommand)
+ ->setNext($this->loggedInCheckCommand)
+ ->setNext($this->completeLoginCommand)
+ ->setNext($this->createSessionTokenCommand)
+ ->setNext($this->clearLostPasswordTokensCommand)
+ ->setNext($this->updateLastPasswordConfirmCommand)
+ ->setNext($this->setUserTimezoneCommand)
+ ->setNext($this->twoFactorCommand)
+ ->setNext($this->finishRememberedLoginCommand);
+
+ return $chain->process($loginData);
+ }
+}
diff --git a/lib/private/Authentication/Login/WebAuthnLoginCommand.php b/lib/private/Authentication/Login/WebAuthnLoginCommand.php
new file mode 100644
index 00000000000..e477a243c56
--- /dev/null
+++ b/lib/private/Authentication/Login/WebAuthnLoginCommand.php
@@ -0,0 +1,48 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Authentication\Login;
+
+use OCP\IUserManager;
+
+class WebAuthnLoginCommand extends ALoginCommand {
+
+ /** @var IUserManager */
+ private $userManager;
+
+ public function __construct(IUserManager $userManager) {
+ $this->userManager = $userManager;
+ }
+
+ public function process(LoginData $loginData): LoginResult {
+ $user = $this->userManager->get($loginData->getUsername());
+ $loginData->setUser($user);
+ if ($user === null) {
+ $loginData->setUser(false);
+ }
+
+ return $this->processNextOrFinishSuccessfully($loginData);
+ }
+
+}
diff --git a/lib/private/Authentication/WebAuthn/CredentialRepository.php b/lib/private/Authentication/WebAuthn/CredentialRepository.php
new file mode 100644
index 00000000000..c6f8cdfd888
--- /dev/null
+++ b/lib/private/Authentication/WebAuthn/CredentialRepository.php
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Authentication\WebAuthn;
+
+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\PublicKeyCredentialUserEntity;
+
+class CredentialRepository implements PublicKeyCredentialSourceRepository {
+
+ /** @var PublicKeyCredentialMapper */
+ private $credentialMapper;
+
+ public function __construct(PublicKeyCredentialMapper $credentialMapper) {
+ $this->credentialMapper = $credentialMapper;
+ }
+
+ public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource {
+ try {
+ $entity = $this->credentialMapper->findOneByCredentialId($publicKeyCredentialId);
+ return $entity->toPublicKeyCredentialSource();
+ } catch (IMapperException $e) {
+ return null;
+ }
+ }
+
+ /**
+ * @return PublicKeyCredentialSource[]
+ */
+ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
+ $uid = $publicKeyCredentialUserEntity->getId();
+ $entities = $this->credentialMapper->findAllForUid($uid);
+
+ return array_map(function (PublicKeyCredentialEntity $entity) {
+ return $entity->toPublicKeyCredentialSource();
+ }, $entities);
+ }
+
+ public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, string $name = null): PublicKeyCredentialEntity {
+ $oldEntity = null;
+
+ try {
+ $oldEntity = $this->credentialMapper->findOneByCredentialId($publicKeyCredentialSource->getPublicKeyCredentialId());
+ } catch (IMapperException $e) {
+
+ }
+
+ if ($name === null) {
+ $name = 'default';
+ }
+
+ $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource);
+
+ if ($oldEntity) {
+ $entity->setId($oldEntity->getId());
+ if ($name === null) {
+ $entity->setName($oldEntity->getName());
+ }
+ }
+
+ return $this->credentialMapper->insertOrUpdate($entity);
+ }
+
+ public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, string $name = null): void {
+ $this->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
+ }
+
+}
diff --git a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php
new file mode 100644
index 00000000000..3b0413aef00
--- /dev/null
+++ b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php
@@ -0,0 +1,92 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Authentication\WebAuthn\Db;
+
+use JsonSerializable;
+use OCP\AppFramework\Db\Entity;
+use Webauthn\PublicKeyCredentialSource;
+use Webauthn\TrustPath\TrustPathLoader;
+
+/**
+ * @since 19.0.0
+ *
+ * @method string getUid();
+ * @method void setUid(string $uid)
+ * @method string getName();
+ * @method void setName(string $name);
+ * @method string getPublicKeyCredentialId();
+ * @method void setPublicKeyCredentialId(string $id);
+ * @method string getData();
+ * @method void setData(string $data);
+ */
+class PublicKeyCredentialEntity extends Entity implements JsonSerializable {
+
+ /** @var string */
+ protected $name;
+
+ /** @var string */
+ protected $uid;
+
+ /** @var string */
+ protected $publicKeyCredentialId;
+
+ /** @var string */
+ protected $data;
+
+ public function __construct() {
+ $this->addType('name', 'string');
+ $this->addType('uid', 'string');
+ $this->addType('publicKeyCredentialId', 'string');
+ $this->addType('data', 'string');
+ }
+
+ static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource): PublicKeyCredentialEntity {
+ $publicKeyCredentialEntity = new self();
+
+ $publicKeyCredentialEntity->setName($name);
+ $publicKeyCredentialEntity->setUid($publicKeyCredentialSource->getUserHandle());
+ $publicKeyCredentialEntity->setPublicKeyCredentialId(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()));
+ $publicKeyCredentialEntity->setData(json_encode($publicKeyCredentialSource));
+
+ return $publicKeyCredentialEntity;
+ }
+
+ function toPublicKeyCredentialSource(): PublicKeyCredentialSource {
+ return PublicKeyCredentialSource::createFromArray(
+ json_decode($this->getData(), true)
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->getId(),
+ 'name' => $this->getName(),
+ ];
+ }
+
+}
diff --git a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialMapper.php b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialMapper.php
new file mode 100644
index 00000000000..c931ccbb3f0
--- /dev/null
+++ b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialMapper.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Authentication\WebAuthn\Db;
+
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\IDBConnection;
+
+class PublicKeyCredentialMapper extends QBMapper {
+
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'webauthn', PublicKeyCredentialEntity::class);
+ }
+
+ public function findOneByCredentialId(string $publicKeyCredentialId): PublicKeyCredentialEntity {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where(
+ $qb->expr()->eq('public_key_credential_id', $qb->createNamedParameter(base64_encode($publicKeyCredentialId)))
+ );
+
+ return $this->findEntity($qb);
+ }
+
+ /**
+ * @return PublicKeyCredentialEntity[]
+ */
+ public function findAllForUid(string $uid): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where(
+ $qb->expr()->eq('uid', $qb->createNamedParameter($uid))
+ );
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @param string $uid
+ * @param int $id
+ *
+ * @return PublicKeyCredentialEntity
+ * @throws DoesNotExistException
+ */
+ public function findById(string $uid, int $id): PublicKeyCredentialEntity {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->andX(
+ $qb->expr()->eq('id', $qb->createNamedParameter($id)),
+ $qb->expr()->eq('uid', $qb->createNamedParameter($uid))
+ ));
+
+ return $this->findEntity($qb);
+ }
+
+}
diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php
new file mode 100644
index 00000000000..32a90345b5c
--- /dev/null
+++ b/lib/private/Authentication/WebAuthn/Manager.php
@@ -0,0 +1,269 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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\ILogger;
+use OCP\IUser;
+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\PublicKeyCredentialSource;
+use Webauthn\PublicKeyCredentialUserEntity;
+use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
+
+class Manager {
+
+ /** @var CredentialRepository */
+ private $repository;
+
+ /** @var PublicKeyCredentialMapper */
+ private $credentialMapper;
+
+ /** @var ILogger */
+ private $logger;
+
+ /** @var IConfig */
+ private $config;
+
+ public function __construct(
+ CredentialRepository $repository,
+ PublicKeyCredentialMapper $credentialMapper,
+ ILogger $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();
+
+ return new PublicKeyCredentialCreationOptions(
+ $rpEntity,
+ $userEntity,
+ $challenge,
+ $publicKeyCredentialParametersList,
+ $timeout,
+ $excludedPublicKeyDescriptors,
+ $authenticatorSelectionCriteria,
+ PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
+ null
+ );
+ }
+
+ 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
+ );
+
+ try {
+ // Load the data
+ $publicKeyCredential = $publicKeyCredentialLoader->load($data);
+ $response = $publicKeyCredential->getResponse();
+
+ // 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);
+ } catch (\Throwable $exception) {
+ throw $exception;
+ }
+
+ // Persist the data
+ return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
+ }
+
+ 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
+ $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) {
+ $credential = $entity->toPublicKeyCredentialSource();
+ return new PublicKeyCredentialDescriptor(
+ $credential->getType(),
+ $credential->getPublicKeyCredentialId()
+ );
+ }, $this->credentialMapper->findAllForUid($uid));
+
+ // Public Key Credential Request Options
+ return new PublicKeyCredentialRequestOptions(
+ random_bytes(32), // Challenge
+ 60000, // Timeout
+ $this->stripPort($serverHost), // Relying Party ID
+ $registeredPublicKeyCredentialDescriptors // Registered PublicKeyCredentialDescriptor classes
+ );
+ }
+
+ 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
+ );
+
+ try {
+ $this->logger->debug('Loading publickey credentials from: ' . $data);
+
+ // Load the data
+ $publicKeyCredential = $publicKeyCredentialLoader->load($data);
+ $response = $publicKeyCredential->getResponse();
+
+ // 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->getRawId(),
+ $response,
+ $publicKeyCredentialRequestOptions,
+ $request,
+ $uid
+ );
+
+ } 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 (!extension_loaded('bcmath')) {
+ return false;
+ }
+
+ if (!extension_loaded('gmp')) {
+ return false;
+ }
+
+ if (!$this->config->getSystemValueBool('auth.webauthn.enabled', true)) {
+ return false;
+ }
+
+ return true;
+ }
+}