diff --git a/3rdparty b/3rdparty
index 179b231245bbae294d021b7158f99c3ffe7e2cb6..7375853f9f77a5c2a82a23bf7bbaf4217be92450 160000
--- a/3rdparty
+++ b/3rdparty
@@ -1 +1 @@
-Subproject commit 179b231245bbae294d021b7158f99c3ffe7e2cb6
+Subproject commit 7375853f9f77a5c2a82a23bf7bbaf4217be92450
diff --git a/apps/settings/appinfo/info.xml b/apps/settings/appinfo/info.xml
index f2d65c0f3e80d30344bead4355e58cace17e64a3..af28d19a9c95413a42ffffabec595e5684d0707a 100644
--- a/apps/settings/appinfo/info.xml
+++ b/apps/settings/appinfo/info.xml
@@ -35,6 +35,7 @@
 		<personal>OCA\Settings\Settings\Personal\Security\Authtokens</personal>
 		<personal>OCA\Settings\Settings\Personal\Security\Password</personal>
 		<personal>OCA\Settings\Settings\Personal\Security\TwoFactor</personal>
+		<personal>OCA\Settings\Settings\Personal\Security\WebAuthn</personal>
 		<personal-section>OCA\Settings\Sections\Personal\PersonalInfo</personal-section>
 		<personal-section>OCA\Settings\Sections\Personal\Security</personal-section>
 		<personal-section>OCA\Settings\Sections\Personal\SyncClients</personal-section>
diff --git a/apps/settings/appinfo/routes.php b/apps/settings/appinfo/routes.php
index 89b6c86993c2a1e47afb1b307103e67d172287d2..f0cc30084eb6c2b1068d14ecba4aba2d7270a8c1 100644
--- a/apps/settings/appinfo/routes.php
+++ b/apps/settings/appinfo/routes.php
@@ -90,5 +90,9 @@ $application->registerRoutes($this, [
 		['name' => 'TwoFactorSettings#update', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'PUT'],
 
 		['name' => 'Help#help', 'url' => '/settings/help/{mode}', 'verb' => 'GET', 'defaults' => ['mode' => '']],
+
+		['name' => 'WebAuthn#startRegistration', 'url' => '/settings/api/personal/webauthn/registration', 'verb' => 'GET'],
+		['name' => 'WebAuthn#finishRegistration', 'url' => '/settings/api/personal/webauthn/registration', 'verb' => 'POST'],
+		['name' => 'WebAuthn#deleteRegistration', 'url' => '/settings/api/personal/webauthn/registration/{id}', 'verb' => 'DELETE'],
 	]
 ]);
diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php
index 841a1aa21eb4378ae8c50b5e1d1471a0b7bf197d..9d89ec1a9995108da7909352a33671f3b8465b49 100644
--- a/apps/settings/composer/composer/autoload_classmap.php
+++ b/apps/settings/composer/composer/autoload_classmap.php
@@ -28,6 +28,7 @@ return array(
     'OCA\\Settings\\Controller\\PersonalSettingsController' => $baseDir . '/../lib/Controller/PersonalSettingsController.php',
     'OCA\\Settings\\Controller\\TwoFactorSettingsController' => $baseDir . '/../lib/Controller/TwoFactorSettingsController.php',
     'OCA\\Settings\\Controller\\UsersController' => $baseDir . '/../lib/Controller/UsersController.php',
+    'OCA\\Settings\\Controller\\WebAuthnController' => $baseDir . '/../lib/Controller/WebAuthnController.php',
     'OCA\\Settings\\Hooks' => $baseDir . '/../lib/Hooks.php',
     'OCA\\Settings\\Mailer\\NewUserMailHelper' => $baseDir . '/../lib/Mailer/NewUserMailHelper.php',
     'OCA\\Settings\\Middleware\\SubadminMiddleware' => $baseDir . '/../lib/Middleware/SubadminMiddleware.php',
@@ -50,5 +51,6 @@ return array(
     'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => $baseDir . '/../lib/Settings/Personal/Security/Authtokens.php',
     'OCA\\Settings\\Settings\\Personal\\Security\\Password' => $baseDir . '/../lib/Settings/Personal/Security/Password.php',
     'OCA\\Settings\\Settings\\Personal\\Security\\TwoFactor' => $baseDir . '/../lib/Settings/Personal/Security/TwoFactor.php',
+    'OCA\\Settings\\Settings\\Personal\\Security\\WebAuthn' => $baseDir . '/../lib/Settings/Personal/Security/WebAuthn.php',
     'OCA\\Settings\\Settings\\Personal\\ServerDevNotice' => $baseDir . '/../lib/Settings/Personal/ServerDevNotice.php',
 );
diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php
index bac850545f3d29bc2639f3412e160981d282b503..c1c1abb189f6a1221b734a8c12a0fdf9e98faa63 100644
--- a/apps/settings/composer/composer/autoload_static.php
+++ b/apps/settings/composer/composer/autoload_static.php
@@ -43,6 +43,7 @@ class ComposerStaticInitSettings
         'OCA\\Settings\\Controller\\PersonalSettingsController' => __DIR__ . '/..' . '/../lib/Controller/PersonalSettingsController.php',
         'OCA\\Settings\\Controller\\TwoFactorSettingsController' => __DIR__ . '/..' . '/../lib/Controller/TwoFactorSettingsController.php',
         'OCA\\Settings\\Controller\\UsersController' => __DIR__ . '/..' . '/../lib/Controller/UsersController.php',
+        'OCA\\Settings\\Controller\\WebAuthnController' => __DIR__ . '/..' . '/../lib/Controller/WebAuthnController.php',
         'OCA\\Settings\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php',
         'OCA\\Settings\\Mailer\\NewUserMailHelper' => __DIR__ . '/..' . '/../lib/Mailer/NewUserMailHelper.php',
         'OCA\\Settings\\Middleware\\SubadminMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/SubadminMiddleware.php',
@@ -65,6 +66,7 @@ class ComposerStaticInitSettings
         'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/Authtokens.php',
         'OCA\\Settings\\Settings\\Personal\\Security\\Password' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/Password.php',
         'OCA\\Settings\\Settings\\Personal\\Security\\TwoFactor' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/TwoFactor.php',
+        'OCA\\Settings\\Settings\\Personal\\Security\\WebAuthn' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/WebAuthn.php',
         'OCA\\Settings\\Settings\\Personal\\ServerDevNotice' => __DIR__ . '/..' . '/../lib/Settings/Personal/ServerDevNotice.php',
     );
 
diff --git a/apps/settings/js/vue-0.js b/apps/settings/js/vue-0.js
index bfe8db11c0e2b3e6811fa7dbe0892fec03359f0f..5e9dfb62598618551e0e100620a4647153ae350f 100644
Binary files a/apps/settings/js/vue-0.js and b/apps/settings/js/vue-0.js differ
diff --git a/apps/settings/js/vue-0.js.map b/apps/settings/js/vue-0.js.map
index 72d812c5e2a313595b589b986db3e96e5526ed92..22bcfa3d672820fcb4bd6c26c49de5128a52bf8c 100644
Binary files a/apps/settings/js/vue-0.js.map and b/apps/settings/js/vue-0.js.map differ
diff --git a/apps/settings/js/vue-5.js b/apps/settings/js/vue-5.js
index c68c470e02dbe4fa0128fa93c4b90751411d7db3..58c856d1e9c34f5cadc7ef3667e1ff603f84a276 100644
Binary files a/apps/settings/js/vue-5.js and b/apps/settings/js/vue-5.js differ
diff --git a/apps/settings/js/vue-5.js.map b/apps/settings/js/vue-5.js.map
index 9ed7d6d8a40b6257724e35319fbf7d24a0d55f8d..fefe5e728ee8c5808a1e3efbd2a75a8292119464 100644
Binary files a/apps/settings/js/vue-5.js.map and b/apps/settings/js/vue-5.js.map differ
diff --git a/apps/settings/js/vue-6.js b/apps/settings/js/vue-6.js
index 0ade2c5e8a988d9ceb1e482cc0489138fc2199ef..8da3a9731554ecead700adf0df9b5725a289000d 100644
Binary files a/apps/settings/js/vue-6.js and b/apps/settings/js/vue-6.js differ
diff --git a/apps/settings/js/vue-6.js.map b/apps/settings/js/vue-6.js.map
index bca2a1556cf684bb0c0f395900f77943b472c3b0..6c51679baad2923fc77e9e0a480588028aef07bd 100644
Binary files a/apps/settings/js/vue-6.js.map and b/apps/settings/js/vue-6.js.map differ
diff --git a/apps/settings/js/vue-7.js b/apps/settings/js/vue-7.js
index 67e9105391bff67007f39ebfa34b0a400e2d2baa..53716b14ca4e501c833c7fc7d5fc07f73611e0c0 100644
Binary files a/apps/settings/js/vue-7.js and b/apps/settings/js/vue-7.js differ
diff --git a/apps/settings/js/vue-7.js.map b/apps/settings/js/vue-7.js.map
index 3ef0002e5c18ebda3a903e1cf686fe7c3612f55b..2ab3b17365e7079bf27b93728cfbce558e556d1c 100644
Binary files a/apps/settings/js/vue-7.js.map and b/apps/settings/js/vue-7.js.map differ
diff --git a/apps/settings/js/vue-8.js b/apps/settings/js/vue-8.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0463c5496512bb534240f55c9b250b51fd0e9dc
Binary files /dev/null and b/apps/settings/js/vue-8.js differ
diff --git a/apps/settings/js/vue-8.js.map b/apps/settings/js/vue-8.js.map
new file mode 100644
index 0000000000000000000000000000000000000000..2cf4c138a33d6941dac5222522048eae1350efd2
Binary files /dev/null and b/apps/settings/js/vue-8.js.map differ
diff --git a/apps/settings/js/vue-settings-admin-security.js b/apps/settings/js/vue-settings-admin-security.js
index 688da0ec399d071294cefc60112ac6419ed0c352..6d0ac833051bb85143375c0090174e5222cf1b64 100644
Binary files a/apps/settings/js/vue-settings-admin-security.js and b/apps/settings/js/vue-settings-admin-security.js differ
diff --git a/apps/settings/js/vue-settings-admin-security.js.map b/apps/settings/js/vue-settings-admin-security.js.map
index f36016b155246d4d8e2c2f8ad748642e645937ad..10cf2ad7b949297457c0bb51dd9a76c990c9d3bf 100644
Binary files a/apps/settings/js/vue-settings-admin-security.js.map and b/apps/settings/js/vue-settings-admin-security.js.map differ
diff --git a/apps/settings/js/vue-settings-apps-users-management.js b/apps/settings/js/vue-settings-apps-users-management.js
index abffc29851a9d43422e312125ff3ac987df215d4..6b1e543e7d20e76d1d48354a31b75370554b6c80 100644
Binary files a/apps/settings/js/vue-settings-apps-users-management.js and b/apps/settings/js/vue-settings-apps-users-management.js differ
diff --git a/apps/settings/js/vue-settings-apps-users-management.js.map b/apps/settings/js/vue-settings-apps-users-management.js.map
index 8a1a5c6342448f98962e192b0686923408dccee2..baa312b4b715ae1a6f8f194362ab2d5c29f79091 100644
Binary files a/apps/settings/js/vue-settings-apps-users-management.js.map and b/apps/settings/js/vue-settings-apps-users-management.js.map differ
diff --git a/apps/settings/js/vue-settings-personal-security.js b/apps/settings/js/vue-settings-personal-security.js
index 4b4ac275885b38b9b4075740818d6a20bf9503dc..b78390048a1d372a691404b033f23cc20f23980d 100644
Binary files a/apps/settings/js/vue-settings-personal-security.js and b/apps/settings/js/vue-settings-personal-security.js differ
diff --git a/apps/settings/js/vue-settings-personal-security.js.map b/apps/settings/js/vue-settings-personal-security.js.map
index 302561a771c3bd75483d1c779fb095dc8bc3e988..6085e92adc3516a95c58a9390625efb6ed9f4f36 100644
Binary files a/apps/settings/js/vue-settings-personal-security.js.map and b/apps/settings/js/vue-settings-personal-security.js.map differ
diff --git a/apps/settings/js/vue-settings-personal-webauthn.js b/apps/settings/js/vue-settings-personal-webauthn.js
new file mode 100644
index 0000000000000000000000000000000000000000..584006ee78b6b385956adeb114afdb7b15fe77cd
Binary files /dev/null and b/apps/settings/js/vue-settings-personal-webauthn.js differ
diff --git a/apps/settings/js/vue-settings-personal-webauthn.js.map b/apps/settings/js/vue-settings-personal-webauthn.js.map
new file mode 100644
index 0000000000000000000000000000000000000000..b4d05aa10457f02d885ee1799d790af7b987e621
Binary files /dev/null and b/apps/settings/js/vue-settings-personal-webauthn.js.map differ
diff --git a/apps/settings/lib/AppInfo/Application.php b/apps/settings/lib/AppInfo/Application.php
index 96c8734129017d239f37c32b2fcf863192bd720c..230b2377cdac61e65b751696a53d92239a9acde2 100644
--- a/apps/settings/lib/AppInfo/Application.php
+++ b/apps/settings/lib/AppInfo/Application.php
@@ -55,12 +55,13 @@ use Symfony\Component\EventDispatcher\GenericEvent;
 
 class Application extends App {
 
+	const APP_ID = 'settings';
 
 	/**
 	 * @param array $urlParams
 	 */
 	public function __construct(array $urlParams=[]){
-		parent::__construct('settings', $urlParams);
+		parent::__construct(self::APP_ID, $urlParams);
 
 		$container = $this->getContainer();
 
diff --git a/apps/settings/lib/Controller/WebAuthnController.php b/apps/settings/lib/Controller/WebAuthnController.php
new file mode 100644
index 0000000000000000000000000000000000000000..e9b45105a81b61ae6ab8c1376838bb5e87ff233a
--- /dev/null
+++ b/apps/settings/lib/Controller/WebAuthnController.php
@@ -0,0 +1,114 @@
+<?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 OCA\Settings\Controller;
+
+use OC\Authentication\WebAuthn\Manager;
+use OCA\Settings\AppInfo\Application;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\ILogger;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUserSession;
+use Webauthn\PublicKeyCredentialCreationOptions;
+
+class WebAuthnController extends Controller {
+
+	private const WEBAUTHN_REGISTRATION = 'webauthn_registration';
+
+	/** @var Manager */
+	private $manager;
+
+	/** @var IUserSession */
+	private $userSession;
+	/**
+	 * @var ISession
+	 */
+	private $session;
+	/**
+	 * @var ILogger
+	 */
+	private $logger;
+
+	public function __construct(IRequest $request, ILogger $logger, Manager $webAuthnManager, IUserSession $userSession, ISession $session) {
+		parent::__construct(Application::APP_ID, $request);
+
+		$this->manager = $webAuthnManager;
+		$this->userSession = $userSession;
+		$this->session = $session;
+		$this->logger = $logger;
+	}
+
+	/**
+	 * @NoAdminRequired
+	 * @PasswordConfirmationRequired
+	 * @UseSession
+	 * @NoCSRFRequired
+	 */
+	public function startRegistration(): JSONResponse {
+		$this->logger->debug('Starting WebAuthn registration');
+
+		$credentialOptions = $this->manager->startRegistration($this->userSession->getUser(), $this->request->getServerHost());
+
+		// Set this in the session since we need it on finish
+		$this->session->set(self::WEBAUTHN_REGISTRATION, $credentialOptions);
+
+		return new JSONResponse($credentialOptions);
+	}
+
+	/**
+	 * @NoAdminRequired
+	 * @PasswordConfirmationRequired
+	 * @UseSession
+	 */
+	public function finishRegistration(string $name, string $data): JSONResponse {
+		$this->logger->debug('Finishing WebAuthn registration');
+
+		if (!$this->session->exists(self::WEBAUTHN_REGISTRATION)) {
+			$this->logger->debug('Trying to finish WebAuthn registration without session data');
+			return new JSONResponse([], Http::STATUS_BAD_REQUEST);
+		}
+
+		// Obtain the publicKeyCredentialOptions from when we started the registration
+		$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::createFromArray($this->session->get(self::WEBAUTHN_REGISTRATION));
+
+		$this->session->remove(self::WEBAUTHN_REGISTRATION);
+
+		return new JSONResponse($this->manager->finishRegister($publicKeyCredentialCreationOptions, $name, $data));
+	}
+
+	/**
+	 * @NoAdminRequired
+	 * @PasswordConfirmationRequired
+	 */
+	public function deleteRegistration(int $id): JSONResponse {
+		$this->logger->debug('Finishing WebAuthn registration');
+
+		$this->manager->deleteRegistration($this->userSession->getUser(), $id);
+
+		return new JSONResponse([]);
+	}
+}
diff --git a/apps/settings/lib/Settings/Personal/Security/WebAuthn.php b/apps/settings/lib/Settings/Personal/Security/WebAuthn.php
new file mode 100644
index 0000000000000000000000000000000000000000..e4e2b3fe9359d92c7d92c22f8acfc2142425a97d
--- /dev/null
+++ b/apps/settings/lib/Settings/Personal/Security/WebAuthn.php
@@ -0,0 +1,80 @@
+<?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 OCA\Settings\Settings\Personal\Security;
+
+use OC\Authentication\WebAuthn\Db\PublicKeyCredentialMapper;
+use OC\Authentication\WebAuthn\Manager;
+use OCA\Settings\AppInfo\Application;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\IInitialStateService;
+use OCP\Settings\ISettings;
+
+class WebAuthn implements ISettings {
+
+	/** @var PublicKeyCredentialMapper */
+	private $mapper;
+
+	/** @var string */
+	private $uid;
+
+	/** @var IInitialStateService */
+	private $initialStateService;
+
+	/** @var Manager */
+	private $manager;
+
+	public function __construct(PublicKeyCredentialMapper $mapper,
+								string $UserId,
+								IInitialStateService $initialStateService,
+								Manager $manager) {
+		$this->mapper = $mapper;
+		$this->uid = $UserId;
+		$this->initialStateService = $initialStateService;
+		$this->manager = $manager;
+	}
+
+	public function getForm() {
+		$this->initialStateService->provideInitialState(
+			Application::APP_ID,
+			'webauthn-devices',
+			$this->mapper->findAllForUid($this->uid)
+		);
+
+		return new TemplateResponse('settings', 'settings/personal/security/webauthn', [
+		]);
+	}
+
+	public function getSection(): ?string {
+		if (!$this->manager->isWebAuthnAvailable()) {
+			return null;
+		}
+
+		return 'security';
+	}
+
+	public function getPriority(): int {
+		return 20;
+	}
+}
diff --git a/apps/settings/src/components/WebAuthn/AddDevice.vue b/apps/settings/src/components/WebAuthn/AddDevice.vue
new file mode 100644
index 0000000000000000000000000000000000000000..05b649ec313427b31cfe273da7d456dedce1dfca
--- /dev/null
+++ b/apps/settings/src/components/WebAuthn/AddDevice.vue
@@ -0,0 +1,215 @@
+<!--
+  - @copyright 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/>.
+  -->
+
+<template>
+	<div v-if="!isHttps">
+		{{ t('settings', 'Passwordless authentication requires a secure connection.') }}
+	</div>
+	<div v-else>
+		<div v-if="step === RegistrationSteps.READY">
+			<button @click="start">
+				{{ t('settings', 'Add Webauthn device') }}
+			</button>
+		</div>
+
+		<div v-else-if="step === RegistrationSteps.REGISTRATION"
+			class="new-webauthn-device">
+			<span class="icon-loading-small webauthn-loading" />
+			{{ t('settings', 'Please authorize your WebAuthn device.') }}
+		</div>
+
+		<div v-else-if="step === RegistrationSteps.NAMING"
+			class="new-webauthn-device">
+			<span class="icon-loading-small webauthn-loading" />
+			<input v-model="name"
+				type="text"
+				:placeholder="t('settings', 'Name your device')"
+				@:keyup.enter="submit">
+			<button @click="submit">
+				{{ t('settings', 'Add') }}
+			</button>
+		</div>
+
+		<div v-else-if="step === RegistrationSteps.PERSIST"
+			class="new-webauthn-device">
+			<span class="icon-loading-small webauthn-loading" />
+			{{ t('settings', 'Adding your device …') }}
+		</div>
+
+		<div v-else>
+			Invalid registration step. This should not have happened.
+		</div>
+	</div>
+</template>
+
+<script>
+import confirmPassword from '@nextcloud/password-confirmation'
+
+import logger from '../../logger'
+import {
+	startRegistration,
+	finishRegistration,
+} from '../../service/WebAuthnRegistrationSerice'
+
+const logAndPass = (text) => (data) => {
+	logger.debug(text)
+	return data
+}
+
+const RegistrationSteps = Object.freeze({
+	READY: 1,
+	REGISTRATION: 2,
+	NAMING: 3,
+	PERSIST: 4,
+})
+
+export default {
+	name: 'AddDevice',
+	props: {
+		httpWarning: Boolean,
+		isHttps: {
+			type: Boolean,
+			default: false
+		}
+	},
+	data() {
+		return {
+			name: '',
+			credential: {},
+			RegistrationSteps,
+			step: RegistrationSteps.READY,
+		}
+	},
+	methods: {
+		arrayToBase64String(a) {
+			return btoa(String.fromCharCode(...a))
+		},
+		start() {
+			this.step = RegistrationSteps.REGISTRATION
+			console.debug('Starting WebAuthn registration')
+
+			return confirmPassword()
+				.then(this.getRegistrationData)
+				.then(this.register.bind(this))
+				.then(() => { this.step = RegistrationSteps.NAMING })
+				.catch(err => {
+					console.error(err.name, err.message)
+					this.step = RegistrationSteps.READY
+				})
+		},
+
+		getRegistrationData() {
+			console.debug('Fetching webauthn registration data')
+
+			const base64urlDecode = function(input) {
+				// Replace non-url compatible chars with base64 standard chars
+				input = input
+					.replace(/-/g, '+')
+					.replace(/_/g, '/')
+
+				// Pad out with standard base64 required padding characters
+				const pad = input.length % 4
+				if (pad) {
+					if (pad === 1) {
+						throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding')
+					}
+					input += new Array(5 - pad).join('=')
+				}
+
+				return window.atob(input)
+			}
+
+			return startRegistration()
+				.then(publicKey => {
+					console.debug(publicKey)
+					publicKey.challenge = Uint8Array.from(base64urlDecode(publicKey.challenge), c => c.charCodeAt(0))
+					publicKey.user.id = Uint8Array.from(publicKey.user.id, c => c.charCodeAt(0))
+					return publicKey
+				})
+				.catch(err => {
+					console.error('Error getting webauthn registration data from server', err)
+					throw new Error(t('settings', 'Server error while trying to add webauthn device'))
+				})
+		},
+
+		register(publicKey) {
+			console.debug('starting webauthn registration')
+
+			return navigator.credentials.create({ publicKey })
+				.then(data => {
+					this.credential = {
+						id: data.id,
+						type: data.type,
+						rawId: this.arrayToBase64String(new Uint8Array(data.rawId)),
+						response: {
+							clientDataJSON: this.arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
+							attestationObject: this.arrayToBase64String(new Uint8Array(data.response.attestationObject)),
+						},
+					}
+				})
+		},
+
+		submit() {
+			this.step = RegistrationSteps.PERSIST
+
+			return confirmPassword()
+				.then(logAndPass('confirmed password'))
+				.then(this.saveRegistrationData)
+				.then(logAndPass('registration data saved'))
+				.then(() => this.reset())
+				.then(logAndPass('app reset'))
+				.catch(console.error.bind(this))
+		},
+
+		async saveRegistrationData() {
+			try {
+				const device = await finishRegistration(this.name, JSON.stringify(this.credential))
+
+				logger.info('new device added', { device })
+
+				this.$emit('added', device)
+			} catch (err) {
+				logger.error('Error persisting webauthn registration', { error: err })
+				throw new Error(t('settings', 'Server error while trying to complete webauthn device registration'))
+			}
+		},
+
+		reset() {
+			this.name = ''
+			this.registrationData = {}
+			this.step = RegistrationSteps.READY
+		},
+	},
+}
+</script>
+
+<style scoped>
+	.webauthn-loading {
+		display: inline-block;
+		vertical-align: sub;
+		margin-left: 2px;
+		margin-right: 2px;
+	}
+
+	.new-webauthn-device {
+		line-height: 300%;
+	}
+</style>
diff --git a/apps/settings/src/components/WebAuthn/Device.vue b/apps/settings/src/components/WebAuthn/Device.vue
new file mode 100644
index 0000000000000000000000000000000000000000..fc1bab3c8b0c2ce248b6277b32a3aabf7f894360
--- /dev/null
+++ b/apps/settings/src/components/WebAuthn/Device.vue
@@ -0,0 +1,65 @@
+<!--
+  - @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+  -
+  - @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+  -
+  - @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/>.
+  -->
+
+<template>
+	<div class="webauthn-device">
+		<span class="icon-webauthn-device" />
+		{{ name || t('settings', 'Unnamed device') }}
+		<Actions :force-menu="true">
+			<ActionButton icon="icon-delete" @click="$emit('delete')">
+				{{ t('settings', 'Delete') }}
+			</ActionButton>
+		</Actions>
+	</div>
+</template>
+
+<script>
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+
+export default {
+	name: 'Device',
+	components: {
+		ActionButton,
+		Actions,
+	},
+	props: {
+		name: {
+			type: String,
+			required: true,
+		},
+	},
+}
+</script>
+
+<style scoped>
+	.webauthn-device {
+		line-height: 300%;
+		display: flex;
+	}
+
+	.icon-webauthn-device {
+		display: inline-block;
+		background-size: 100%;
+		padding: 3px;
+		margin: 3px;
+	}
+</style>
diff --git a/apps/settings/src/components/WebAuthn/Section.vue b/apps/settings/src/components/WebAuthn/Section.vue
new file mode 100644
index 0000000000000000000000000000000000000000..cd09ec43c1abd269f3754c95b7257e22760cf78c
--- /dev/null
+++ b/apps/settings/src/components/WebAuthn/Section.vue
@@ -0,0 +1,109 @@
+<!--
+  - @copyright 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/>.
+  -->
+
+<template>
+	<div id="security-webauthn" class="section">
+		<h2>{{ t('settings', 'Passwordless Authentication') }}</h2>
+		<p class="settings-hint hidden-when-empty">
+			{{ t('settings', 'Set up your account for passwordless authentication following the FIDO2 standard.') }}
+		</p>
+		<p v-if="devices.length === 0">
+			{{ t('twofactor_u2f', 'No devices configured.') }}
+		</p>
+		<p v-else>
+			{{ t('twofactor_u2f', 'The following devices are configured for your account:') }}
+		</p>
+		<Device v-for="device in sortedDevices"
+			:key="device.id"
+			:name="device.name"
+			@delete="deleteDevice(device.id)" />
+
+		<p v-if="!hasPublicKeyCredential" class="warning">
+			{{ t('settings', 'Your browser does not support Webauthn.') }}
+		</p>
+
+		<AddDevice v-if="hasPublicKeyCredential" :isHttps="isHttps" @added="deviceAdded" />
+	</div>
+</template>
+
+<script>
+import confirmPassword from '@nextcloud/password-confirmation'
+import sortBy from 'lodash/fp/sortBy'
+
+import AddDevice from './AddDevice'
+import Device from './Device'
+import logger from '../../logger'
+import { removeRegistration } from '../../service/WebAuthnRegistrationSerice'
+
+const sortByName = sortBy('name')
+
+export default {
+	components: {
+		AddDevice,
+		Device,
+	},
+	props: {
+		initialDevices: {
+			type: Array,
+			required: true,
+		},
+		isHttps: {
+			type: Boolean,
+			default: false,
+		},
+		hasPublicKeyCredential: {
+			type: Boolean,
+			default: false,
+		},
+	},
+	data() {
+		return {
+			devices: this.initialDevices,
+		}
+	},
+	computed: {
+		sortedDevices() {
+			return sortByName(this.devices)
+		},
+	},
+	methods: {
+		deviceAdded(device) {
+			logger.debug(`adding new device to the list ${device.id}`)
+
+			this.devices.push(device)
+		},
+		async deleteDevice(id) {
+			logger.info(`deleting webauthn device ${id}`)
+
+			await confirmPassword()
+			await removeRegistration(id)
+
+			this.devices = this.devices.filter(d => d.id !== id)
+
+			logger.info(`webauthn device ${id} removed successfully`)
+		},
+	},
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/apps/settings/src/logger.js b/apps/settings/src/logger.js
new file mode 100644
index 0000000000000000000000000000000000000000..275771ce4c5cea8c004abe1d0d8ad000e4066b91
--- /dev/null
+++ b/apps/settings/src/logger.js
@@ -0,0 +1,27 @@
+/*
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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/>.
+ */
+
+import { getLoggerBuilder } from '@nextcloud/logger'
+
+export default getLoggerBuilder()
+	.setApp('settings')
+	.detectUser()
+	.build()
diff --git a/apps/settings/src/main-personal-webauth.js b/apps/settings/src/main-personal-webauth.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6e302df5f8df65ffca672dc9846e5a170073732
--- /dev/null
+++ b/apps/settings/src/main-personal-webauth.js
@@ -0,0 +1,40 @@
+/**
+ * @copyright 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/>.
+ */
+
+import Vue from 'vue'
+import { loadState } from '@nextcloud/initial-state'
+
+import WebAuthnSection from './components/WebAuthn/Section'
+
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = btoa(OC.requestToken)
+
+Vue.prototype.t = t
+
+const View = Vue.extend(WebAuthnSection)
+const devices = loadState('settings', 'webauthn-devices')
+new View({
+	propsData: {
+		initialDevices: devices,
+		isHttps: window.location.protocol === 'https:',
+		hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined',
+	},
+}).$mount('#security-webauthn')
diff --git a/apps/settings/src/service/WebAuthnRegistrationSerice.js b/apps/settings/src/service/WebAuthnRegistrationSerice.js
new file mode 100644
index 0000000000000000000000000000000000000000..4c82c5b9fa75dc4dba3d319d69a09abda3ab32ae
--- /dev/null
+++ b/apps/settings/src/service/WebAuthnRegistrationSerice.js
@@ -0,0 +1,43 @@
+/**
+ * @copyright 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/>.
+ */
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+export async function startRegistration() {
+	const url = generateUrl('/settings/api/personal/webauthn/registration')
+
+	const resp = await axios.get(url)
+	return resp.data
+}
+
+export async function finishRegistration(name, data) {
+	const url = generateUrl('/settings/api/personal/webauthn/registration')
+
+	const resp = await axios.post(url, { name, data })
+	return resp.data
+}
+
+export async function removeRegistration(id) {
+	const url = generateUrl(`/settings/api/personal/webauthn/registration/${id}`)
+
+	await axios.delete(url)
+}
diff --git a/apps/settings/templates/settings/personal/security/webauthn.php b/apps/settings/templates/settings/personal/security/webauthn.php
new file mode 100644
index 0000000000000000000000000000000000000000..268e06aef7dd6913736346c45d4c3f905c4dc489
--- /dev/null
+++ b/apps/settings/templates/settings/personal/security/webauthn.php
@@ -0,0 +1,31 @@
+<?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/>.
+ *
+ */
+
+script('settings', [
+	'vue-settings-personal-webauthn',
+]);
+
+?>
+
+<div id="security-webauthn" class="section"></div>
diff --git a/apps/settings/webpack.js b/apps/settings/webpack.js
index 7c887861f1e05834061915f7fabe46d0347cac89..d8dbb722235fcd3c838541022f1d9490edb7f7f7 100644
--- a/apps/settings/webpack.js
+++ b/apps/settings/webpack.js
@@ -4,7 +4,8 @@ module.exports = {
 	entry: {
 		'settings-apps-users-management': path.join(__dirname, 'src', 'main-apps-users-management'),
 		'settings-admin-security': path.join(__dirname, 'src', 'main-admin-security'),
-		'settings-personal-security': path.join(__dirname, 'src', 'main-personal-security')
+		'settings-personal-security': path.join(__dirname, 'src', 'main-personal-security'),
+		'settings-personal-webauthn': path.join(__dirname, 'src', 'main-personal-webauth')
 	},
 	output: {
 		path: path.resolve(__dirname, './js'),
diff --git a/config/config.sample.php b/config/config.sample.php
index 0e19f69826149812dbeb13248bcd21b42c8649db..71d22fbe2b4007c71d2e8c490f649159f84b8451 100644
--- a/config/config.sample.php
+++ b/config/config.sample.php
@@ -269,6 +269,11 @@ $CONFIG = [
  */
 'auth.bruteforce.protection.enabled' => true,
 
+/**
+ * By default WebAuthn is available but it can be explicitly disabled by admins
+ */
+'auth.webauthn.enabled' => true,
+
 /**
  * The directory where the skeleton files are located. These files will be
  * copied to the data directory of new users. Leave empty to not copy any
diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php
index 13aef8f67ab0d8e3537d7dfe5769d1a3d5775aff..b3f7bb310ba05caf46b9a5b031412be4e30482a2 100644
--- a/core/Controller/LoginController.php
+++ b/core/Controller/LoginController.php
@@ -34,6 +34,7 @@ namespace OC\Core\Controller;
 use OC\AppFramework\Http\Request;
 use OC\Authentication\Login\Chain;
 use OC\Authentication\Login\LoginData;
+use OC\Authentication\WebAuthn\Manager as WebAuthnManager;
 use OC\Security\Bruteforce\Throttler;
 use OC\User\Session;
 use OC_App;
@@ -80,6 +81,8 @@ class LoginController extends Controller {
 	private $loginChain;
 	/** @var IInitialStateService */
 	private $initialStateService;
+	/** @var WebAuthnManager */
+	private $webAuthnManager;
 
 	public function __construct(?string $appName,
 								IRequest $request,
@@ -92,7 +95,8 @@ class LoginController extends Controller {
 								Defaults $defaults,
 								Throttler $throttler,
 								Chain $loginChain,
-								IInitialStateService $initialStateService) {
+								IInitialStateService $initialStateService,
+								WebAuthnManager $webAuthnManager) {
 		parent::__construct($appName, $request);
 		$this->userManager = $userManager;
 		$this->config = $config;
@@ -104,6 +108,7 @@ class LoginController extends Controller {
 		$this->throttler = $throttler;
 		$this->loginChain = $loginChain;
 		$this->initialStateService = $initialStateService;
+		$this->webAuthnManager = $webAuthnManager;
 	}
 
 	/**
@@ -181,6 +186,8 @@ class LoginController extends Controller {
 
 		$this->setPasswordResetInitialState($user);
 
+		$this->initialStateService->provideInitialState('core', 'webauthn-available', $this->webAuthnManager->isWebAuthnAvailable());
+
 		// OpenGraph Support: http://ogp.me/
 		Util::addHeader('meta', ['property' => 'og:title', 'content' => Util::sanitizeHTML($this->defaults->getName())]);
 		Util::addHeader('meta', ['property' => 'og:description', 'content' => Util::sanitizeHTML($this->defaults->getSlogan())]);
diff --git a/core/Controller/WebAuthnController.php b/core/Controller/WebAuthnController.php
new file mode 100644
index 0000000000000000000000000000000000000000..0b98a58c1eb8b40c56d2bdcc7ea2953ac1d61134
--- /dev/null
+++ b/core/Controller/WebAuthnController.php
@@ -0,0 +1,117 @@
+<?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\Core\Controller;
+
+use OC\Authentication\Login\LoginData;
+use OC\Authentication\Login\WebAuthnChain;
+use OC\Authentication\WebAuthn\Manager;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\ILogger;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\Util;
+use Webauthn\PublicKeyCredentialRequestOptions;
+
+class WebAuthnController extends Controller {
+
+	private const WEBAUTHN_LOGIN = 'webauthn_login';
+	private const WEBAUTHN_LOGIN_UID = 'webauthn_login_uid';
+
+	/** @var Manager */
+	private $webAuthnManger;
+
+	/** @var ISession */
+	private $session;
+
+	/** @var ILogger */
+	private $logger;
+
+	/** @var WebAuthnChain */
+	private $webAuthnChain;
+
+	public function __construct($appName, IRequest $request, Manager $webAuthnManger, ISession $session, ILogger $logger, WebAuthnChain $webAuthnChain) {
+		parent::__construct($appName, $request);
+
+		$this->webAuthnManger = $webAuthnManger;
+		$this->session = $session;
+		$this->logger = $logger;
+		$this->webAuthnChain = $webAuthnChain;
+	}
+
+	/**
+	 * @NoAdminRequired
+	 * @PublicPage
+	 * @UseSession
+	 */
+	public function startAuthentication(string $loginName): JSONResponse {
+		$this->logger->debug('Starting WebAuthn login');
+
+		$this->logger->debug('Converting login name to UID');
+		$uid = $loginName;
+		Util::emitHook(
+			'\OCA\Files_Sharing\API\Server2Server',
+			'preLoginNameUsedAsUserName',
+			array('uid' => &$uid)
+		);
+		$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_UID, $uid);
+
+		return new JSONResponse($publicKeyCredentialRequestOptions);
+	}
+
+	/**
+	 * @NoAdminRequired
+	 * @PublicPage
+	 * @UseSession
+	 */
+	public function finishAuthentication(string $data): JSONResponse {
+		$this->logger->debug('Validating WebAuthn login');
+
+		if (!$this->session->exists(self::WEBAUTHN_LOGIN) || !$this->session->exists(self::WEBAUTHN_LOGIN_UID)) {
+			$this->logger->debug('Trying to finish WebAuthn login without session data');
+			return new JSONResponse([], Http::STATUS_BAD_REQUEST);
+		}
+
+		// Obtain the publicKeyCredentialOptions from when we started the registration
+		$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($this->session->get(self::WEBAUTHN_LOGIN));
+		$uid = $this->session->get(self::WEBAUTHN_LOGIN_UID);
+		$this->webAuthnManger->finishAuthentication($publicKeyCredentialRequestOptions, $data, $uid);
+
+		//TODO: add other parameters
+		$loginData = new LoginData(
+			$this->request,
+			$uid,
+			''
+		);
+		$this->webAuthnChain->process($loginData);
+
+		return new JSONResponse([]);
+	}
+}
diff --git a/core/Migrations/Version19000Date20200211083441.php b/core/Migrations/Version19000Date20200211083441.php
new file mode 100644
index 0000000000000000000000000000000000000000..ef4d4fde54f12ab8ab1f02ff081d3da4a6eadc02
--- /dev/null
+++ b/core/Migrations/Version19000Date20200211083441.php
@@ -0,0 +1,45 @@
+<?php
+declare(strict_types=1);
+
+namespace OC\Core\Migrations;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version19000Date20200211083441 extends SimpleMigrationStep {
+
+	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
+		/** @var ISchemaWrapper $schema */
+		$schema = $schemaClosure();
+
+		if (!$schema->hasTable('webauthn')) {
+			$table = $schema->createTable('webauthn');
+			$table->addColumn('id', 'integer', [
+				'autoincrement' => true,
+				'notnull' => true,
+				'length' => 64,
+			]);
+			$table->addColumn('uid', 'string', [
+				'notnull' => true,
+				'length' => 64,
+			]);
+			$table->addColumn('name', 'string', [
+				'notnull' => true,
+				'length' => 64,
+			]);
+			$table->addColumn('public_key_credential_id', 'string', [
+				'notnull' => true,
+				'length' => 255
+			]);
+			$table->addColumn('data', 'text', [
+				'notnull' => true,
+			]);
+			$table->setPrimaryKey(['id']);
+			$table->addIndex(['uid'], 'webauthn_uid');
+			$table->addIndex(['public_key_credential_id'], 'webauthn_publicKeyCredentialId');
+		}
+		return $schema;
+	}
+}
diff --git a/core/js/dist/install.js b/core/js/dist/install.js
index 09c56a913deccaf8128bd177996586f7762abf05..4133714ff75926ba10fc4bf1b9798f3cb95c6478 100644
Binary files a/core/js/dist/install.js and b/core/js/dist/install.js differ
diff --git a/core/js/dist/install.js.map b/core/js/dist/install.js.map
index 1662a495081607287b11732f2d37cc576030d1bb..14afc78c21e9b66b6ad3785365c65c48578e1bf5 100644
Binary files a/core/js/dist/install.js.map and b/core/js/dist/install.js.map differ
diff --git a/core/js/dist/login.js b/core/js/dist/login.js
index d2786d13f1d111581abb77ac6a3435bd432aaa46..216d1c59a385dfb6e4fc588fd4e532ec3acfc740 100644
Binary files a/core/js/dist/login.js and b/core/js/dist/login.js differ
diff --git a/core/js/dist/login.js.map b/core/js/dist/login.js.map
index 16dfe2ea984b0ea34b41dfb64f33c6a64eed78a8..3e558a8d81647e00a0d3e653ad6e2e7fb5573c37 100644
Binary files a/core/js/dist/login.js.map and b/core/js/dist/login.js.map differ
diff --git a/core/js/dist/main.js b/core/js/dist/main.js
index ac565391a66e57afaf5c3d9c85e0d9689cf17607..521e9eec116ecdc0dbfebe0609b8f91c31821d80 100644
Binary files a/core/js/dist/main.js and b/core/js/dist/main.js differ
diff --git a/core/js/dist/main.js.map b/core/js/dist/main.js.map
index a5ffba9f3b22b8b4e3d69c718886f832247b8aba..078bc851b4602361264a1dee081a9e5a41586d6c 100644
Binary files a/core/js/dist/main.js.map and b/core/js/dist/main.js.map differ
diff --git a/core/js/dist/maintenance.js b/core/js/dist/maintenance.js
index 5e579b3b3f8b06c2a7dcccc1fb0f704a7a58f158..3801d52c83792ee4f055f1723d2776f3478778cd 100644
Binary files a/core/js/dist/maintenance.js and b/core/js/dist/maintenance.js differ
diff --git a/core/js/dist/maintenance.js.map b/core/js/dist/maintenance.js.map
index 59cc4f65c41b8940d6ada2e6bdc22860ba90a102..e28537ba2e61052fc674f94847abef8375ba2465 100644
Binary files a/core/js/dist/maintenance.js.map and b/core/js/dist/maintenance.js.map differ
diff --git a/core/js/dist/recommendedapps.js b/core/js/dist/recommendedapps.js
index 65677ffab725ec981def930e0b4ac0eadb517f47..c682c09617550d3e76e280e82d03af4c75bb9a58 100644
Binary files a/core/js/dist/recommendedapps.js and b/core/js/dist/recommendedapps.js differ
diff --git a/core/js/dist/recommendedapps.js.map b/core/js/dist/recommendedapps.js.map
index de0903cf92d41cdc2e66cac8fda262cd0d70ab98..7d51add261f0e8ade6323ec9012043d58bea678a 100644
Binary files a/core/js/dist/recommendedapps.js.map and b/core/js/dist/recommendedapps.js.map differ
diff --git a/core/routes.php b/core/routes.php
index 5fb13bc298afccf1fc9addf73ec6a0f2a7bf5721..8d03be05bb3a219757e3e6a542c2ba3fe3c53489 100644
--- a/core/routes.php
+++ b/core/routes.php
@@ -86,6 +86,10 @@ $application->registerRoutes($this, [
 		['name' => 'Wipe#checkWipe', 'url' => '/core/wipe/check', 'verb' => 'POST'],
 		['name' => 'Wipe#wipeDone', 'url' => '/core/wipe/success', 'verb' => 'POST'],
 
+		// Logins for passwordless auth
+		['name' => 'WebAuthn#startAuthentication', 'url' => 'login/webauthn/start', 'verb' => 'POST'],
+		['name' => 'WebAuthn#finishAuthentication', 'url' => 'login/webauthn/finish', 'verb' => 'POST'],
+
 		// Legacy routes that need to be globally available while they are handled by an app
 		['name' => 'viewcontroller#showFile', 'url' => '/f/{fileid}', 'verb' => 'GET', 'app' => 'files'],
 		['name' => 'sharecontroller#showShare', 'url' => '/s/{token}', 'verb' => 'GET', 'app' => 'files_sharing'],
diff --git a/core/src/components/login/LoginButton.vue b/core/src/components/login/LoginButton.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f7d426e6c63abd4dfb9ef5259924d5e9eb20e197
--- /dev/null
+++ b/core/src/components/login/LoginButton.vue
@@ -0,0 +1,56 @@
+<!--
+  - @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+  -
+  - @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+  -
+  - @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/>.
+  -->
+
+<template>
+	<div id="submit-wrapper" @click="$emit('click')">
+		<input id="submit-form"
+			type="submit"
+			class="login primary"
+			title=""
+			:value="!loading ? t('core', 'Log in') : t('core', 'Logging in …')">
+		<div class="submit-icon"
+			:class="{
+				'icon-confirm-white': !loading,
+				'icon-loading-small': loading && invertedColors,
+				'icon-loading-small-dark': loading && !invertedColors,
+			}" />
+	</div>
+</template>
+
+<script>
+export default {
+	name: 'LoginButton',
+	props: {
+		loading: {
+			type: Boolean,
+			required: true,
+		},
+		invertedColors: {
+			type: Boolean,
+			default: false,
+		},
+	},
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue
index 687896ceb5475ab8de2f2ee1cec2854d6d492496..a20ce6dc4c2302326e443f4745cc6c83753aeba8 100644
--- a/core/src/components/login/LoginForm.vue
+++ b/core/src/components/login/LoginForm.vue
@@ -20,7 +20,8 @@
   -->
 
 <template>
-	<form method="post"
+	<form ref="loginForm"
+		method="post"
 		name="login"
 		:action="OC.generateUrl('login')"
 		@submit="submit">
@@ -84,19 +85,7 @@
 				</a>
 			</p>
 
-			<div id="submit-wrapper">
-				<input id="submit-form"
-					type="submit"
-					class="login primary"
-					title=""
-					:value="!loading ? t('core', 'Log in') : t('core', 'Logging in …')">
-				<div class="submit-icon"
-					:class="{
-						'icon-confirm-white': !loading,
-						'icon-loading-small': loading && invertedColors,
-						'icon-loading-small-dark': loading && !invertedColors,
-					}" />
-			</div>
+			<LoginButton :loading="loading" :inverted-colors="invertedColors" />
 
 			<p v-if="invalidPassword"
 				class="warning wrongPasswordMsg">
@@ -135,9 +124,11 @@
 
 <script>
 import jstz from 'jstimezonedetect'
+import LoginButton from './LoginButton'
 
 export default {
 	name: 'LoginForm',
+	components: { LoginButton },
 	props: {
 		username: {
 			type: String,
diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..028f7d547da72c4c931e7bd2b9b4de106ac505df
--- /dev/null
+++ b/core/src/components/login/PasswordLessLoginForm.vue
@@ -0,0 +1,208 @@
+<template>
+	<form v-if="isHttps && hasPublicKeyCredential"
+		ref="loginForm"
+		method="post"
+		name="login"
+		@submit.prevent="submit">
+		<fieldset>
+			<p class="grouptop groupbottom">
+				<input id="user"
+					ref="user"
+					v-model="user"
+					type="text"
+					name="user"
+					:autocomplete="autoCompleteAllowed ? 'on' : 'off'"
+					:placeholder="t('core', 'Username or email')"
+					:aria-label="t('core', 'Username or email')"
+					required
+					@change="$emit('update:username', user)">
+				<label for="user" class="infield">{{ t('core', 'Username or	email') }}</label>
+			</p>
+
+			<div v-if="!validCredentials">
+				{{ t('core', 'Your account is not setup for passwordless login.') }}
+			</div>
+
+			<LoginButton v-if="validCredentials"
+				:loading="loading"
+				:inverted-colors="invertedColors"
+				@click="authenticate" />
+		</fieldset>
+	</form>
+	<div v-else-if="!hasPublicKeyCredential">
+		{{ t('core', 'Passwordless authentication is not supported in your browser.')}}
+	</div>
+	<div v-else-if="!isHttps">
+		{{ t('core', 'Passwordless authentication is only available over a secure connection.')}}
+	</div>
+</template>
+
+<script>
+import {
+	startAuthentication,
+	finishAuthentication,
+} from '../../service/WebAuthnAuthenticationService'
+import LoginButton from './LoginButton'
+
+class NoValidCredentials extends Error {
+
+}
+
+export default {
+	name: 'PasswordLessLoginForm',
+	components: {
+		LoginButton,
+	},
+	props: {
+		username: {
+			type: String,
+			default: '',
+		},
+		redirectUrl: {
+			type: String,
+		},
+		invertedColors: {
+			type: Boolean,
+			default: false,
+		},
+		autoCompleteAllowed: {
+			type: Boolean,
+			default: true,
+		},
+		isHttps: {
+			type: Boolean,
+			default: false,
+		},
+		hasPublicKeyCredential: {
+			type: Boolean,
+			default: false,
+		}
+	},
+	data() {
+		return {
+			user: this.username,
+			loading: false,
+			validCredentials: true,
+		}
+	},
+	methods: {
+		authenticate() {
+			console.debug('passwordless login initiated')
+
+			this.getAuthenticationData(this.user)
+				.then(publicKey => {
+					console.debug(publicKey)
+					return publicKey
+				})
+				.then(this.sign)
+				.then(this.completeAuthentication)
+				.catch(error => {
+					if (error instanceof NoValidCredentials) {
+						this.validCredentials = false
+						return
+					}
+					console.debug(error)
+				})
+		},
+		getAuthenticationData(uid) {
+			const base64urlDecode = function(input) {
+				// Replace non-url compatible chars with base64 standard chars
+				input = input
+					.replace(/-/g, '+')
+					.replace(/_/g, '/')
+
+				// Pad out with standard base64 required padding characters
+				const pad = input.length % 4
+				if (pad) {
+					if (pad === 1) {
+						throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding')
+					}
+					input += new Array(5 - pad).join('=')
+				}
+
+				return window.atob(input)
+			}
+
+			return startAuthentication(uid)
+				.then(publicKey => {
+					console.debug('Obtained PublicKeyCredentialRequestOptions')
+					console.debug(publicKey)
+
+					if (!Object.prototype.hasOwnProperty.call(publicKey, 'allowCredentials')) {
+						console.debug('No credentials found.')
+						throw new NoValidCredentials()
+					}
+
+					publicKey.challenge = Uint8Array.from(base64urlDecode(publicKey.challenge), c => c.charCodeAt(0))
+					publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) {
+						return {
+							...data,
+							'id': Uint8Array.from(base64urlDecode(data.id), c => c.charCodeAt(0)),
+						}
+					})
+
+					console.debug('Converted PublicKeyCredentialRequestOptions')
+					console.debug(publicKey)
+					return publicKey
+				})
+				.catch(error => {
+					console.debug('Error while obtaining data')
+					throw error
+				})
+		},
+		sign(publicKey) {
+			const arrayToBase64String = function(a) {
+				return window.btoa(String.fromCharCode(...a))
+			}
+
+			return navigator.credentials.get({ publicKey })
+				.then(data => {
+					console.debug(data)
+					console.debug(new Uint8Array(data.rawId))
+					console.debug(arrayToBase64String(new Uint8Array(data.rawId)))
+					return {
+						id: data.id,
+						type: data.type,
+						rawId: arrayToBase64String(new Uint8Array(data.rawId)),
+						response: {
+							authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
+							clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
+							signature: arrayToBase64String(new Uint8Array(data.response.signature)),
+							userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null,
+						},
+					}
+				})
+				.then(challenge => {
+					console.debug(challenge)
+					return challenge
+				})
+				.catch(error => {
+					console.debug('GOT AN ERROR!')
+					console.debug(error) // Example: timeout, interaction refused...
+				})
+		},
+		completeAuthentication(challenge) {
+			console.debug('TIME TO COMPLETE')
+
+			const location = this.redirectUrl
+
+			return finishAuthentication(JSON.stringify(challenge))
+				.then(data => {
+					console.debug('Logged in redirecting')
+					window.location.href = location
+				})
+				.catch(error => {
+					console.debug('GOT AN ERROR WHILE SUBMITTING CHALLENGE!')
+					console.debug(error) // Example: timeout, interaction refused...
+				})
+		},
+		submit() {
+			// noop
+		},
+	},
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/core/src/login.js b/core/src/login.js
index 7270442c83e1f9ff7dda9c9b6bacfa889506e48e..bfcfabf169f3f098ab6d39aed11fb715dba06144 100644
--- a/core/src/login.js
+++ b/core/src/login.js
@@ -64,5 +64,8 @@ new View({
 		resetPasswordTarget: fromStateOr('resetPasswordTarget', ''),
 		resetPasswordUser: fromStateOr('resetPasswordUser', ''),
 		directLogin: query.direct === '1',
+		hasPasswordless: fromStateOr('webauthn-available', false),
+		isHttps: window.location.protocol === 'https:',
+		hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined',
 	},
 }).$mount('#login')
diff --git a/core/src/service/WebAuthnAuthenticationService.js b/core/src/service/WebAuthnAuthenticationService.js
new file mode 100644
index 0000000000000000000000000000000000000000..91f191770660fa69f6ecf47f513c0fa522197c09
--- /dev/null
+++ b/core/src/service/WebAuthnAuthenticationService.js
@@ -0,0 +1,37 @@
+/**
+ * @copyright 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/>.
+ */
+
+import Axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+export function startAuthentication(loginName) {
+	const url = generateUrl('/login/webauthn/start')
+
+	return Axios.post(url, { loginName })
+		.then(resp => resp.data)
+}
+
+export function finishAuthentication(data) {
+	const url = generateUrl('/login/webauthn/finish')
+
+	return Axios.post(url, { data })
+		.then(resp => resp.data)
+}
diff --git a/core/src/views/Login.vue b/core/src/views/Login.vue
index baea18cbe3c3a376fa6bb0ce7e31cc64ab4896c1..a50e6c5c72cc3a56d2c16391ffd723cef892d192 100644
--- a/core/src/views/Login.vue
+++ b/core/src/views/Login.vue
@@ -22,7 +22,7 @@
 <template>
 	<div>
 		<transition name="fade" mode="out-in">
-			<div v-if="!resetPassword && resetPasswordTarget === ''"
+			<div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''"
 				key="login">
 				<LoginForm
 					:username.sync="user"
@@ -45,6 +45,25 @@
 					@click.prevent="resetPassword = true">
 					{{ t('core', 'Forgot password?') }}
 				</a>
+				<br>
+				<a v-if="hasPasswordless" @click.prevent="passwordlessLogin = true">
+					{{ t('core', 'Log in with a device') }}
+				</a>
+			</div>
+			<div v-else-if="!loading && passwordlessLogin"
+				key="reset"
+				class="login-additional">
+				<PasswordLessLoginForm
+					:username.sync="user"
+					:redirect-url="redirectUrl"
+					:inverted-colors="invertedColors"
+					:auto-complete-allowed="autoCompleteAllowed"
+					:isHttps="isHttps"
+					:hasPublicKeyCredential="hasPublicKeyCredential"
+					@submit="loading = true" />
+				<a @click.prevent="passwordlessLogin = false">
+					{{ t('core', 'Back') }}
+				</a>
 			</div>
 			<div v-else-if="!loading && canResetPassword"
 				key="reset"
@@ -69,6 +88,7 @@
 
 <script>
 import LoginForm from '../components/login/LoginForm.vue'
+import PasswordLessLoginForm from '../components/login/PasswordLessLoginForm.vue'
 import ResetPassword from '../components/login/ResetPassword.vue'
 import UpdatePassword from '../components/login/UpdatePassword.vue'
 
@@ -76,6 +96,7 @@ export default {
 	name: 'Login',
 	components: {
 		LoginForm,
+		PasswordLessLoginForm,
 		ResetPassword,
 		UpdatePassword,
 	},
@@ -120,11 +141,24 @@ export default {
 			type: Boolean,
 			default: false,
 		},
+		hasPasswordless: {
+			type: Boolean,
+			default: false,
+		},
+		isHttps: {
+			type: Boolean,
+			default: false,
+		},
+		hasPublicKeyCredential: {
+			type: Boolean,
+			default: false,
+		},
 	},
 	data() {
 		return {
 			loading: false,
 			user: this.username,
+			passwordlessLogin: false,
 			resetPassword: false,
 		}
 	},
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 388c7906eb81c7ae2311d76ec6e7aa0b3b93d4d5..3672146205f1cb9b3230ccd3e42727146ab2587e 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 cfc6d9842dfa765524222b1cc4f785a648ffd098..99fe8d8b4c7eb4f2945ab718ef4fcac0b67249c1 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 fbc8215e67f0c75033d8a1312ec3fc9dcfd69a47..05b6c27f5652cf7118a13cb4408ee76d3036e1fd 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 0000000000000000000000000000000000000000..dfc6943e853fc169df01123aec47658f60327174
--- /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 0000000000000000000000000000000000000000..e477a243c56397912b052f2924e850c5caa72ee0
--- /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 0000000000000000000000000000000000000000..c6f8cdfd8886d8c8e3f1ad506bf66420393b3d4f
--- /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 0000000000000000000000000000000000000000..3b0413aef00644f20cd8385aa3875cd0f0dc260c
--- /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 0000000000000000000000000000000000000000..c931ccbb3f0f0a5f27e6afd37702d8febd49ec66
--- /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 0000000000000000000000000000000000000000..32a90345b5c55bc9b19865da00fb20762119dddd
--- /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;
+	}
+}
diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php
index cddf89527db6b2999eaa8b888644d5cc1e8eb1fe..c1177d84e1ee645b1479f45af2165f8141823670 100644
--- a/tests/Core/Controller/LoginControllerTest.php
+++ b/tests/Core/Controller/LoginControllerTest.php
@@ -83,6 +83,9 @@ class LoginControllerTest extends TestCase {
 	/** @var IInitialStateService|MockObject */
 	private $initialStateService;
 
+	/** @var \OC\Authentication\WebAuthn\Manager|MockObject */
+	private $webAuthnManager;
+
 	protected function setUp(): void {
 		parent::setUp();
 		$this->request = $this->createMock(IRequest::class);
@@ -97,6 +100,8 @@ class LoginControllerTest extends TestCase {
 		$this->throttler = $this->createMock(Throttler::class);
 		$this->chain = $this->createMock(LoginChain::class);
 		$this->initialStateService = $this->createMock(IInitialStateService::class);
+		$this->webAuthnManager = $this->createMock(\OC\Authentication\WebAuthn\Manager::class);
+
 
 		$this->request->method('getRemoteAddress')
 			->willReturn('1.2.3.4');
@@ -118,7 +123,8 @@ class LoginControllerTest extends TestCase {
 			$this->defaults,
 			$this->throttler,
 			$this->chain,
-			$this->initialStateService
+			$this->initialStateService,
+			$this->webAuthnManager
 		);
 	}
 
diff --git a/version.php b/version.php
index f39ef9497e9f0a9b328450f7bc2a44b2ec637617..4db60d087187b6d6f347655d0ef103e02299cbd8 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
 // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
 // when updating major/minor version number.
 
-$OC_Version = [19, 0, 0, 0];
+$OC_Version = [19, 0, 0, 1];
 
 // The human readable string
 $OC_VersionString = '19.0.0 alpha';