Add WebAuthn supporttags/v19.0.0beta1
Subproject commit 179b231245bbae294d021b7158f99c3ffe7e2cb6 | |||||
Subproject commit 7375853f9f77a5c2a82a23bf7bbaf4217be92450 |
<personal>OCA\Settings\Settings\Personal\Security\Authtokens</personal> | <personal>OCA\Settings\Settings\Personal\Security\Authtokens</personal> | ||||
<personal>OCA\Settings\Settings\Personal\Security\Password</personal> | <personal>OCA\Settings\Settings\Personal\Security\Password</personal> | ||||
<personal>OCA\Settings\Settings\Personal\Security\TwoFactor</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\PersonalInfo</personal-section> | ||||
<personal-section>OCA\Settings\Sections\Personal\Security</personal-section> | <personal-section>OCA\Settings\Sections\Personal\Security</personal-section> | ||||
<personal-section>OCA\Settings\Sections\Personal\SyncClients</personal-section> | <personal-section>OCA\Settings\Sections\Personal\SyncClients</personal-section> |
['name' => 'TwoFactorSettings#update', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'PUT'], | ['name' => 'TwoFactorSettings#update', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'PUT'], | ||||
['name' => 'Help#help', 'url' => '/settings/help/{mode}', 'verb' => 'GET', 'defaults' => ['mode' => '']], | ['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'], | |||||
] | ] | ||||
]); | ]); |
'OCA\\Settings\\Controller\\PersonalSettingsController' => $baseDir . '/../lib/Controller/PersonalSettingsController.php', | 'OCA\\Settings\\Controller\\PersonalSettingsController' => $baseDir . '/../lib/Controller/PersonalSettingsController.php', | ||||
'OCA\\Settings\\Controller\\TwoFactorSettingsController' => $baseDir . '/../lib/Controller/TwoFactorSettingsController.php', | 'OCA\\Settings\\Controller\\TwoFactorSettingsController' => $baseDir . '/../lib/Controller/TwoFactorSettingsController.php', | ||||
'OCA\\Settings\\Controller\\UsersController' => $baseDir . '/../lib/Controller/UsersController.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\\Hooks' => $baseDir . '/../lib/Hooks.php', | ||||
'OCA\\Settings\\Mailer\\NewUserMailHelper' => $baseDir . '/../lib/Mailer/NewUserMailHelper.php', | 'OCA\\Settings\\Mailer\\NewUserMailHelper' => $baseDir . '/../lib/Mailer/NewUserMailHelper.php', | ||||
'OCA\\Settings\\Middleware\\SubadminMiddleware' => $baseDir . '/../lib/Middleware/SubadminMiddleware.php', | 'OCA\\Settings\\Middleware\\SubadminMiddleware' => $baseDir . '/../lib/Middleware/SubadminMiddleware.php', | ||||
'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => $baseDir . '/../lib/Settings/Personal/Security/Authtokens.php', | '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\\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\\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', | 'OCA\\Settings\\Settings\\Personal\\ServerDevNotice' => $baseDir . '/../lib/Settings/Personal/ServerDevNotice.php', | ||||
); | ); |
'OCA\\Settings\\Controller\\PersonalSettingsController' => __DIR__ . '/..' . '/../lib/Controller/PersonalSettingsController.php', | 'OCA\\Settings\\Controller\\PersonalSettingsController' => __DIR__ . '/..' . '/../lib/Controller/PersonalSettingsController.php', | ||||
'OCA\\Settings\\Controller\\TwoFactorSettingsController' => __DIR__ . '/..' . '/../lib/Controller/TwoFactorSettingsController.php', | 'OCA\\Settings\\Controller\\TwoFactorSettingsController' => __DIR__ . '/..' . '/../lib/Controller/TwoFactorSettingsController.php', | ||||
'OCA\\Settings\\Controller\\UsersController' => __DIR__ . '/..' . '/../lib/Controller/UsersController.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\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php', | ||||
'OCA\\Settings\\Mailer\\NewUserMailHelper' => __DIR__ . '/..' . '/../lib/Mailer/NewUserMailHelper.php', | 'OCA\\Settings\\Mailer\\NewUserMailHelper' => __DIR__ . '/..' . '/../lib/Mailer/NewUserMailHelper.php', | ||||
'OCA\\Settings\\Middleware\\SubadminMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/SubadminMiddleware.php', | 'OCA\\Settings\\Middleware\\SubadminMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/SubadminMiddleware.php', | ||||
'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/Authtokens.php', | '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\\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\\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', | 'OCA\\Settings\\Settings\\Personal\\ServerDevNotice' => __DIR__ . '/..' . '/../lib/Settings/Personal/ServerDevNotice.php', | ||||
); | ); | ||||
class Application extends App { | class Application extends App { | ||||
const APP_ID = 'settings'; | |||||
/** | /** | ||||
* @param array $urlParams | * @param array $urlParams | ||||
*/ | */ | ||||
public function __construct(array $urlParams=[]){ | public function __construct(array $urlParams=[]){ | ||||
parent::__construct('settings', $urlParams); | |||||
parent::__construct(self::APP_ID, $urlParams); | |||||
$container = $this->getContainer(); | $container = $this->getContainer(); | ||||
<?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([]); | |||||
} | |||||
} |
<?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; | |||||
} | |||||
} |
<!-- | |||||
- @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> |
<!-- | |||||
- @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> |
<!-- | |||||
- @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> |
/* | |||||
* @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() |
/** | |||||
* @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') |
/** | |||||
* @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) | |||||
} |
<?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> |
entry: { | entry: { | ||||
'settings-apps-users-management': path.join(__dirname, 'src', 'main-apps-users-management'), | 'settings-apps-users-management': path.join(__dirname, 'src', 'main-apps-users-management'), | ||||
'settings-admin-security': path.join(__dirname, 'src', 'main-admin-security'), | '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: { | output: { | ||||
path: path.resolve(__dirname, './js'), | path: path.resolve(__dirname, './js'), |
*/ | */ | ||||
'auth.bruteforce.protection.enabled' => true, | '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 | * 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 | * copied to the data directory of new users. Leave empty to not copy any |
use OC\AppFramework\Http\Request; | use OC\AppFramework\Http\Request; | ||||
use OC\Authentication\Login\Chain; | use OC\Authentication\Login\Chain; | ||||
use OC\Authentication\Login\LoginData; | use OC\Authentication\Login\LoginData; | ||||
use OC\Authentication\WebAuthn\Manager as WebAuthnManager; | |||||
use OC\Security\Bruteforce\Throttler; | use OC\Security\Bruteforce\Throttler; | ||||
use OC\User\Session; | use OC\User\Session; | ||||
use OC_App; | use OC_App; | ||||
private $loginChain; | private $loginChain; | ||||
/** @var IInitialStateService */ | /** @var IInitialStateService */ | ||||
private $initialStateService; | private $initialStateService; | ||||
/** @var WebAuthnManager */ | |||||
private $webAuthnManager; | |||||
public function __construct(?string $appName, | public function __construct(?string $appName, | ||||
IRequest $request, | IRequest $request, | ||||
Defaults $defaults, | Defaults $defaults, | ||||
Throttler $throttler, | Throttler $throttler, | ||||
Chain $loginChain, | Chain $loginChain, | ||||
IInitialStateService $initialStateService) { | |||||
IInitialStateService $initialStateService, | |||||
WebAuthnManager $webAuthnManager) { | |||||
parent::__construct($appName, $request); | parent::__construct($appName, $request); | ||||
$this->userManager = $userManager; | $this->userManager = $userManager; | ||||
$this->config = $config; | $this->config = $config; | ||||
$this->throttler = $throttler; | $this->throttler = $throttler; | ||||
$this->loginChain = $loginChain; | $this->loginChain = $loginChain; | ||||
$this->initialStateService = $initialStateService; | $this->initialStateService = $initialStateService; | ||||
$this->webAuthnManager = $webAuthnManager; | |||||
} | } | ||||
/** | /** | ||||
$this->setPasswordResetInitialState($user); | $this->setPasswordResetInitialState($user); | ||||
$this->initialStateService->provideInitialState('core', 'webauthn-available', $this->webAuthnManager->isWebAuthnAvailable()); | |||||
// OpenGraph Support: http://ogp.me/ | // OpenGraph Support: http://ogp.me/ | ||||
Util::addHeader('meta', ['property' => 'og:title', 'content' => Util::sanitizeHTML($this->defaults->getName())]); | Util::addHeader('meta', ['property' => 'og:title', 'content' => Util::sanitizeHTML($this->defaults->getName())]); | ||||
Util::addHeader('meta', ['property' => 'og:description', 'content' => Util::sanitizeHTML($this->defaults->getSlogan())]); | Util::addHeader('meta', ['property' => 'og:description', 'content' => Util::sanitizeHTML($this->defaults->getSlogan())]); |
<?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([]); | |||||
} | |||||
} |
<?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; | |||||
} | |||||
} |
['name' => 'Wipe#checkWipe', 'url' => '/core/wipe/check', 'verb' => 'POST'], | ['name' => 'Wipe#checkWipe', 'url' => '/core/wipe/check', 'verb' => 'POST'], | ||||
['name' => 'Wipe#wipeDone', 'url' => '/core/wipe/success', '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 | // 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' => 'viewcontroller#showFile', 'url' => '/f/{fileid}', 'verb' => 'GET', 'app' => 'files'], | ||||
['name' => 'sharecontroller#showShare', 'url' => '/s/{token}', 'verb' => 'GET', 'app' => 'files_sharing'], | ['name' => 'sharecontroller#showShare', 'url' => '/s/{token}', 'verb' => 'GET', 'app' => 'files_sharing'], |
<!-- | |||||
- @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> |
--> | --> | ||||
<template> | <template> | ||||
<form method="post" | |||||
<form ref="loginForm" | |||||
method="post" | |||||
name="login" | name="login" | ||||
:action="OC.generateUrl('login')" | :action="OC.generateUrl('login')" | ||||
@submit="submit"> | @submit="submit"> | ||||
</a> | </a> | ||||
</p> | </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" | <p v-if="invalidPassword" | ||||
class="warning wrongPasswordMsg"> | class="warning wrongPasswordMsg"> | ||||
<script> | <script> | ||||
import jstz from 'jstimezonedetect' | import jstz from 'jstimezonedetect' | ||||
import LoginButton from './LoginButton' | |||||
export default { | export default { | ||||
name: 'LoginForm', | name: 'LoginForm', | ||||
components: { LoginButton }, | |||||
props: { | props: { | ||||
username: { | username: { | ||||
type: String, | type: String, |
<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> |
resetPasswordTarget: fromStateOr('resetPasswordTarget', ''), | resetPasswordTarget: fromStateOr('resetPasswordTarget', ''), | ||||
resetPasswordUser: fromStateOr('resetPasswordUser', ''), | resetPasswordUser: fromStateOr('resetPasswordUser', ''), | ||||
directLogin: query.direct === '1', | directLogin: query.direct === '1', | ||||
hasPasswordless: fromStateOr('webauthn-available', false), | |||||
isHttps: window.location.protocol === 'https:', | |||||
hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined', | |||||
}, | }, | ||||
}).$mount('#login') | }).$mount('#login') |
/** | |||||
* @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) | |||||
} |
<template> | <template> | ||||
<div> | <div> | ||||
<transition name="fade" mode="out-in"> | <transition name="fade" mode="out-in"> | ||||
<div v-if="!resetPassword && resetPasswordTarget === ''" | |||||
<div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''" | |||||
key="login"> | key="login"> | ||||
<LoginForm | <LoginForm | ||||
:username.sync="user" | :username.sync="user" | ||||
@click.prevent="resetPassword = true"> | @click.prevent="resetPassword = true"> | ||||
{{ t('core', 'Forgot password?') }} | {{ t('core', 'Forgot password?') }} | ||||
</a> | </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> | ||||
<div v-else-if="!loading && canResetPassword" | <div v-else-if="!loading && canResetPassword" | ||||
key="reset" | key="reset" | ||||
<script> | <script> | ||||
import LoginForm from '../components/login/LoginForm.vue' | import LoginForm from '../components/login/LoginForm.vue' | ||||
import PasswordLessLoginForm from '../components/login/PasswordLessLoginForm.vue' | |||||
import ResetPassword from '../components/login/ResetPassword.vue' | import ResetPassword from '../components/login/ResetPassword.vue' | ||||
import UpdatePassword from '../components/login/UpdatePassword.vue' | import UpdatePassword from '../components/login/UpdatePassword.vue' | ||||
name: 'Login', | name: 'Login', | ||||
components: { | components: { | ||||
LoginForm, | LoginForm, | ||||
PasswordLessLoginForm, | |||||
ResetPassword, | ResetPassword, | ||||
UpdatePassword, | UpdatePassword, | ||||
}, | }, | ||||
type: Boolean, | type: Boolean, | ||||
default: false, | default: false, | ||||
}, | }, | ||||
hasPasswordless: { | |||||
type: Boolean, | |||||
default: false, | |||||
}, | |||||
isHttps: { | |||||
type: Boolean, | |||||
default: false, | |||||
}, | |||||
hasPublicKeyCredential: { | |||||
type: Boolean, | |||||
default: false, | |||||
}, | |||||
}, | }, | ||||
data() { | data() { | ||||
return { | return { | ||||
loading: false, | loading: false, | ||||
user: this.username, | user: this.username, | ||||
passwordlessLogin: false, | |||||
resetPassword: false, | resetPassword: false, | ||||
} | } | ||||
}, | }, |
'OC\\Authentication\\Login\\UidLoginCommand' => $baseDir . '/lib/private/Authentication/Login/UidLoginCommand.php', | '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\\UpdateLastPasswordConfirmCommand' => $baseDir . '/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php', | ||||
'OC\\Authentication\\Login\\UserDisabledCheckCommand' => $baseDir . '/lib/private/Authentication/Login/UserDisabledCheckCommand.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\\Notifications\\Notifier' => $baseDir . '/lib/private/Authentication/Notifications/Notifier.php', | ||||
'OC\\Authentication\\Token\\DefaultToken' => $baseDir . '/lib/private/Authentication/Token/DefaultToken.php', | 'OC\\Authentication\\Token\\DefaultToken' => $baseDir . '/lib/private/Authentication/Token/DefaultToken.php', | ||||
'OC\\Authentication\\Token\\DefaultTokenCleanupJob' => $baseDir . '/lib/private/Authentication/Token/DefaultTokenCleanupJob.php', | 'OC\\Authentication\\Token\\DefaultTokenCleanupJob' => $baseDir . '/lib/private/Authentication/Token/DefaultTokenCleanupJob.php', | ||||
'OC\\Authentication\\TwoFactorAuth\\ProviderManager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderManager.php', | '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\\ProviderSet' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderSet.php', | ||||
'OC\\Authentication\\TwoFactorAuth\\Registry' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Registry.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\\Avatar' => $baseDir . '/lib/private/Avatar/Avatar.php', | ||||
'OC\\Avatar\\AvatarManager' => $baseDir . '/lib/private/Avatar/AvatarManager.php', | 'OC\\Avatar\\AvatarManager' => $baseDir . '/lib/private/Avatar/AvatarManager.php', | ||||
'OC\\Avatar\\GuestAvatar' => $baseDir . '/lib/private/Avatar/GuestAvatar.php', | 'OC\\Avatar\\GuestAvatar' => $baseDir . '/lib/private/Avatar/GuestAvatar.php', | ||||
'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', | 'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', | ||||
'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', | 'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', | ||||
'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.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\\WhatsNewController' => $baseDir . '/core/Controller/WhatsNewController.php', | ||||
'OC\\Core\\Controller\\WipeController' => $baseDir . '/core/Controller/WipeController.php', | 'OC\\Core\\Controller\\WipeController' => $baseDir . '/core/Controller/WipeController.php', | ||||
'OC\\Core\\Data\\LoginFlowV2Credentials' => $baseDir . '/core/Data/LoginFlowV2Credentials.php', | 'OC\\Core\\Data\\LoginFlowV2Credentials' => $baseDir . '/core/Data/LoginFlowV2Credentials.php', | ||||
'OC\\Core\\Migrations\\Version18000Date20190920085628' => $baseDir . '/core/Migrations/Version18000Date20190920085628.php', | 'OC\\Core\\Migrations\\Version18000Date20190920085628' => $baseDir . '/core/Migrations/Version18000Date20190920085628.php', | ||||
'OC\\Core\\Migrations\\Version18000Date20191014105105' => $baseDir . '/core/Migrations/Version18000Date20191014105105.php', | 'OC\\Core\\Migrations\\Version18000Date20191014105105' => $baseDir . '/core/Migrations/Version18000Date20191014105105.php', | ||||
'OC\\Core\\Migrations\\Version18000Date20191204114856' => $baseDir . '/core/Migrations/Version18000Date20191204114856.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\\Notification\\RemoveLinkSharesNotifier' => $baseDir . '/core/Notification/RemoveLinkSharesNotifier.php', | ||||
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', | 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', | ||||
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', | 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', |
'OC\\Authentication\\Login\\UidLoginCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UidLoginCommand.php', | '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\\UpdateLastPasswordConfirmCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php', | ||||
'OC\\Authentication\\Login\\UserDisabledCheckCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UserDisabledCheckCommand.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\\Notifications\\Notifier' => __DIR__ . '/../../..' . '/lib/private/Authentication/Notifications/Notifier.php', | ||||
'OC\\Authentication\\Token\\DefaultToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultToken.php', | 'OC\\Authentication\\Token\\DefaultToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultToken.php', | ||||
'OC\\Authentication\\Token\\DefaultTokenCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultTokenCleanupJob.php', | 'OC\\Authentication\\Token\\DefaultTokenCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultTokenCleanupJob.php', | ||||
'OC\\Authentication\\TwoFactorAuth\\ProviderManager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderManager.php', | '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\\ProviderSet' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderSet.php', | ||||
'OC\\Authentication\\TwoFactorAuth\\Registry' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Registry.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\\Avatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/Avatar.php', | ||||
'OC\\Avatar\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/Avatar/AvatarManager.php', | 'OC\\Avatar\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/Avatar/AvatarManager.php', | ||||
'OC\\Avatar\\GuestAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/GuestAvatar.php', | 'OC\\Avatar\\GuestAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/GuestAvatar.php', | ||||
'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', | 'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', | ||||
'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', | 'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', | ||||
'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.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\\WhatsNewController' => __DIR__ . '/../../..' . '/core/Controller/WhatsNewController.php', | ||||
'OC\\Core\\Controller\\WipeController' => __DIR__ . '/../../..' . '/core/Controller/WipeController.php', | 'OC\\Core\\Controller\\WipeController' => __DIR__ . '/../../..' . '/core/Controller/WipeController.php', | ||||
'OC\\Core\\Data\\LoginFlowV2Credentials' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Credentials.php', | 'OC\\Core\\Data\\LoginFlowV2Credentials' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Credentials.php', | ||||
'OC\\Core\\Migrations\\Version18000Date20190920085628' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20190920085628.php', | 'OC\\Core\\Migrations\\Version18000Date20190920085628' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20190920085628.php', | ||||
'OC\\Core\\Migrations\\Version18000Date20191014105105' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191014105105.php', | 'OC\\Core\\Migrations\\Version18000Date20191014105105' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191014105105.php', | ||||
'OC\\Core\\Migrations\\Version18000Date20191204114856' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191204114856.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\\Notification\\RemoveLinkSharesNotifier' => __DIR__ . '/../../..' . '/core/Notification/RemoveLinkSharesNotifier.php', | ||||
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', | 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', | ||||
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', | 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', |
$tokenType = IToken::DO_NOT_REMEMBER; | $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); | return $this->processNextOrFinishSuccessfully($loginData); | ||||
} | } |
public function __construct(IRequest $request, | public function __construct(IRequest $request, | ||||
string $username, | string $username, | ||||
string $password, | |||||
?string $password, | |||||
string $redirectUrl = null, | string $redirectUrl = null, | ||||
string $timeZone = '', | string $timeZone = '', | ||||
string $timeZoneOffset = '') { | string $timeZoneOffset = '') { | ||||
return $this->username; | return $this->username; | ||||
} | } | ||||
public function getPassword(): string { | |||||
public function getPassword(): ?string { | |||||
return $this->password; | return $this->password; | ||||
} | } | ||||
<?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); | |||||
} | |||||
} |
<?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); | |||||
} | |||||
} |
<?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); | |||||
} | |||||
} |
<?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(), | |||||
]; | |||||
} | |||||
} |
<?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); | |||||
} | |||||
} |
<?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; | |||||
} | |||||
} |
/** @var IInitialStateService|MockObject */ | /** @var IInitialStateService|MockObject */ | ||||
private $initialStateService; | private $initialStateService; | ||||
/** @var \OC\Authentication\WebAuthn\Manager|MockObject */ | |||||
private $webAuthnManager; | |||||
protected function setUp(): void { | protected function setUp(): void { | ||||
parent::setUp(); | parent::setUp(); | ||||
$this->request = $this->createMock(IRequest::class); | $this->request = $this->createMock(IRequest::class); | ||||
$this->throttler = $this->createMock(Throttler::class); | $this->throttler = $this->createMock(Throttler::class); | ||||
$this->chain = $this->createMock(LoginChain::class); | $this->chain = $this->createMock(LoginChain::class); | ||||
$this->initialStateService = $this->createMock(IInitialStateService::class); | $this->initialStateService = $this->createMock(IInitialStateService::class); | ||||
$this->webAuthnManager = $this->createMock(\OC\Authentication\WebAuthn\Manager::class); | |||||
$this->request->method('getRemoteAddress') | $this->request->method('getRemoteAddress') | ||||
->willReturn('1.2.3.4'); | ->willReturn('1.2.3.4'); | ||||
$this->defaults, | $this->defaults, | ||||
$this->throttler, | $this->throttler, | ||||
$this->chain, | $this->chain, | ||||
$this->initialStateService | |||||
$this->initialStateService, | |||||
$this->webAuthnManager | |||||
); | ); | ||||
} | } | ||||
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel | // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel | ||||
// when updating major/minor version number. | // when updating major/minor version number. | ||||
$OC_Version = [19, 0, 0, 0]; | |||||
$OC_Version = [19, 0, 0, 1]; | |||||
// The human readable string | // The human readable string | ||||
$OC_VersionString = '19.0.0 alpha'; | $OC_VersionString = '19.0.0 alpha'; |