aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorRoeland Jago Douma <roeland@famdouma.nl>2020-02-09 20:06:08 +0100
committerRoeland Jago Douma <roeland@famdouma.nl>2020-03-31 22:17:07 +0200
commit53db05a1f67fc974dba904ec158b2d67fa72df95 (patch)
treecc306fb0b96ccb8ee057af4a86be161aa1b76e2a /lib
parentf04f34b94b7e61f9d11fc07608d7eb2ae2163de8 (diff)
downloadnextcloud-server-53db05a1f67fc974dba904ec158b2d67fa72df95.tar.gz
nextcloud-server-53db05a1f67fc974dba904ec158b2d67fa72df95.zip
Start with webauthn
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl> Signed-off-by: npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/composer/composer/autoload_classmap.php8
-rw-r--r--lib/composer/composer/autoload_static.php8
-rw-r--r--lib/private/Authentication/Login/CreateSessionTokenCommand.php36
-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
9 files changed, 725 insertions, 11 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 388c7906eb8..3672146205f 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -626,6 +626,8 @@ return array(
'OC\\Authentication\\Login\\UidLoginCommand' => $baseDir . '/lib/private/Authentication/Login/UidLoginCommand.php',
'OC\\Authentication\\Login\\UpdateLastPasswordConfirmCommand' => $baseDir . '/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php',
'OC\\Authentication\\Login\\UserDisabledCheckCommand' => $baseDir . '/lib/private/Authentication/Login/UserDisabledCheckCommand.php',
+ 'OC\\Authentication\\Login\\WebAuthnChain' => $baseDir . '/lib/private/Authentication/Login/WebAuthnChain.php',
+ 'OC\\Authentication\\Login\\WebAuthnLoginCommand' => $baseDir . '/lib/private/Authentication/Login/WebAuthnLoginCommand.php',
'OC\\Authentication\\Notifications\\Notifier' => $baseDir . '/lib/private/Authentication/Notifications/Notifier.php',
'OC\\Authentication\\Token\\DefaultToken' => $baseDir . '/lib/private/Authentication/Token/DefaultToken.php',
'OC\\Authentication\\Token\\DefaultTokenCleanupJob' => $baseDir . '/lib/private/Authentication/Token/DefaultTokenCleanupJob.php',
@@ -648,6 +650,10 @@ return array(
'OC\\Authentication\\TwoFactorAuth\\ProviderManager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderManager.php',
'OC\\Authentication\\TwoFactorAuth\\ProviderSet' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderSet.php',
'OC\\Authentication\\TwoFactorAuth\\Registry' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Registry.php',
+ 'OC\\Authentication\\WebAuthn\\CredentialRepository' => $baseDir . '/lib/private/Authentication/WebAuthn/CredentialRepository.php',
+ 'OC\\Authentication\\WebAuthn\\Db\\PublicKeyCredentialEntity' => $baseDir . '/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php',
+ 'OC\\Authentication\\WebAuthn\\Db\\PublicKeyCredentialMapper' => $baseDir . '/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialMapper.php',
+ 'OC\\Authentication\\WebAuthn\\Manager' => $baseDir . '/lib/private/Authentication/WebAuthn/Manager.php',
'OC\\Avatar\\Avatar' => $baseDir . '/lib/private/Avatar/Avatar.php',
'OC\\Avatar\\AvatarManager' => $baseDir . '/lib/private/Avatar/AvatarManager.php',
'OC\\Avatar\\GuestAvatar' => $baseDir . '/lib/private/Avatar/GuestAvatar.php',
@@ -814,6 +820,7 @@ return array(
'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php',
'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php',
'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php',
+ 'OC\\Core\\Controller\\WebAuthnController' => $baseDir . '/core/Controller/WebAuthnController.php',
'OC\\Core\\Controller\\WhatsNewController' => $baseDir . '/core/Controller/WhatsNewController.php',
'OC\\Core\\Controller\\WipeController' => $baseDir . '/core/Controller/WipeController.php',
'OC\\Core\\Data\\LoginFlowV2Credentials' => $baseDir . '/core/Data/LoginFlowV2Credentials.php',
@@ -847,6 +854,7 @@ return array(
'OC\\Core\\Migrations\\Version18000Date20190920085628' => $baseDir . '/core/Migrations/Version18000Date20190920085628.php',
'OC\\Core\\Migrations\\Version18000Date20191014105105' => $baseDir . '/core/Migrations/Version18000Date20191014105105.php',
'OC\\Core\\Migrations\\Version18000Date20191204114856' => $baseDir . '/core/Migrations/Version18000Date20191204114856.php',
+ 'OC\\Core\\Migrations\\Version19000Date20200211083441' => $baseDir . '/core/Migrations/Version19000Date20200211083441.php',
'OC\\Core\\Notification\\RemoveLinkSharesNotifier' => $baseDir . '/core/Notification/RemoveLinkSharesNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index cfc6d9842df..99fe8d8b4c7 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -655,6 +655,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Authentication\\Login\\UidLoginCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UidLoginCommand.php',
'OC\\Authentication\\Login\\UpdateLastPasswordConfirmCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php',
'OC\\Authentication\\Login\\UserDisabledCheckCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UserDisabledCheckCommand.php',
+ 'OC\\Authentication\\Login\\WebAuthnChain' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/WebAuthnChain.php',
+ 'OC\\Authentication\\Login\\WebAuthnLoginCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/WebAuthnLoginCommand.php',
'OC\\Authentication\\Notifications\\Notifier' => __DIR__ . '/../../..' . '/lib/private/Authentication/Notifications/Notifier.php',
'OC\\Authentication\\Token\\DefaultToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultToken.php',
'OC\\Authentication\\Token\\DefaultTokenCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultTokenCleanupJob.php',
@@ -677,6 +679,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Authentication\\TwoFactorAuth\\ProviderManager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderManager.php',
'OC\\Authentication\\TwoFactorAuth\\ProviderSet' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderSet.php',
'OC\\Authentication\\TwoFactorAuth\\Registry' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Registry.php',
+ 'OC\\Authentication\\WebAuthn\\CredentialRepository' => __DIR__ . '/../../..' . '/lib/private/Authentication/WebAuthn/CredentialRepository.php',
+ 'OC\\Authentication\\WebAuthn\\Db\\PublicKeyCredentialEntity' => __DIR__ . '/../../..' . '/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php',
+ 'OC\\Authentication\\WebAuthn\\Db\\PublicKeyCredentialMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialMapper.php',
+ 'OC\\Authentication\\WebAuthn\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/WebAuthn/Manager.php',
'OC\\Avatar\\Avatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/Avatar.php',
'OC\\Avatar\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/Avatar/AvatarManager.php',
'OC\\Avatar\\GuestAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/GuestAvatar.php',
@@ -843,6 +849,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php',
'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php',
'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php',
+ 'OC\\Core\\Controller\\WebAuthnController' => __DIR__ . '/../../..' . '/core/Controller/WebAuthnController.php',
'OC\\Core\\Controller\\WhatsNewController' => __DIR__ . '/../../..' . '/core/Controller/WhatsNewController.php',
'OC\\Core\\Controller\\WipeController' => __DIR__ . '/../../..' . '/core/Controller/WipeController.php',
'OC\\Core\\Data\\LoginFlowV2Credentials' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Credentials.php',
@@ -876,6 +883,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Migrations\\Version18000Date20190920085628' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20190920085628.php',
'OC\\Core\\Migrations\\Version18000Date20191014105105' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191014105105.php',
'OC\\Core\\Migrations\\Version18000Date20191204114856' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191204114856.php',
+ 'OC\\Core\\Migrations\\Version19000Date20200211083441' => __DIR__ . '/../../..' . '/core/Migrations/Version19000Date20200211083441.php',
'OC\\Core\\Notification\\RemoveLinkSharesNotifier' => __DIR__ . '/../../..' . '/core/Notification/RemoveLinkSharesNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
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/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;
+ }
+}