diff options
Diffstat (limited to 'apps/settings/src/components/WebAuthn/AddDevice.vue')
-rw-r--r-- | apps/settings/src/components/WebAuthn/AddDevice.vue | 186 |
1 files changed, 77 insertions, 109 deletions
diff --git a/apps/settings/src/components/WebAuthn/AddDevice.vue b/apps/settings/src/components/WebAuthn/AddDevice.vue index f9b3223d8cb..db00bae451a 100644 --- a/apps/settings/src/components/WebAuthn/AddDevice.vue +++ b/apps/settings/src/components/WebAuthn/AddDevice.vue @@ -1,34 +1,18 @@ <!-- - - @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/>. - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <div v-if="!isHttps && !isLocalhost"> {{ t('settings', 'Passwordless authentication requires a secure connection.') }} </div> <div v-else> - <div v-if="step === RegistrationSteps.READY"> - <NcButton @click="start" type="primary"> - {{ t('settings', 'Add WebAuthn device') }} - </NcButton> - </div> + <NcButton v-if="step === RegistrationSteps.READY" + type="primary" + @click="start"> + {{ t('settings', 'Add WebAuthn device') }} + </NcButton> <div v-else-if="step === RegistrationSteps.REGISTRATION" class="new-webauthn-device"> @@ -39,13 +23,16 @@ <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"> - <NcButton @click="submit" type="primary"> - {{ t('settings', 'Add') }} - </NcButton> + <form @submit.prevent="submit"> + <NcTextField ref="nameInput" + class="new-webauthn-device__name" + :label="t('settings', 'Device name')" + :value.sync="name" + show-trailing-button + :trailing-button-label="t('settings', 'Add')" + trailing-button-icon="arrowRight" + @trailing-button-click="submit" /> + </form> </div> <div v-else-if="step === RegistrationSteps.PERSIST" @@ -61,15 +48,18 @@ </template> <script> +import { showError } from '@nextcloud/dialogs' import { confirmPassword } from '@nextcloud/password-confirmation' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import '@nextcloud/password-confirmation/dist/style.css' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcTextField from '@nextcloud/vue/components/NcTextField' -import logger from '../../logger.js' +import logger from '../../logger.ts' import { startRegistration, finishRegistration, -} from '../../service/WebAuthnRegistrationSerice.js' +} from '../../service/WebAuthnRegistrationSerice.ts' + +import '@nextcloud/password-confirmation/dist/style.css' const logAndPass = (text) => (data) => { logger.debug(text) @@ -88,6 +78,7 @@ export default { components: { NcButton, + NcTextField, }, props: { @@ -101,83 +92,55 @@ export default { default: false, }, }, + + setup() { + // non reactive props + return { + RegistrationSteps, + } + }, + data() { return { name: '', credential: {}, - RegistrationSteps, step: RegistrationSteps.READY, } }, - methods: { - arrayToBase64String(a) { - return btoa(String.fromCharCode(...a)) + + watch: { + /** + * Auto focus the name input when naming a device + */ + step() { + if (this.step === RegistrationSteps.NAMING) { + this.$nextTick(() => this.$refs.nameInput?.focus()) + } }, - start() { + }, + + methods: { + /** + * Start the registration process by loading the authenticator parameters + * The next step is the naming of the device + */ + async 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) + try { + await confirmPassword() + this.credential = await startRegistration() + this.step = RegistrationSteps.NAMING + } catch (err) { + showError(err) + this.step = RegistrationSteps.READY } - - 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)), - }, - } - }) }, + /** + * Save the new device with the given name on the server + */ submit() { this.step = RegistrationSteps.PERSIST @@ -187,12 +150,12 @@ export default { .then(logAndPass('registration data saved')) .then(() => this.reset()) .then(logAndPass('app reset')) - .catch(console.error.bind(this)) + .catch(console.error) }, async saveRegistrationData() { try { - const device = await finishRegistration(this.name, JSON.stringify(this.credential)) + const device = await finishRegistration(this.name, this.credential) logger.info('new device added', { device }) @@ -212,15 +175,20 @@ export default { } </script> -<style scoped> - .webauthn-loading { - display: inline-block; - vertical-align: sub; - margin-left: 2px; - margin-right: 2px; - } +<style scoped lang="scss"> +.webauthn-loading { + display: inline-block; + vertical-align: sub; + margin-inline: 2px; +} + +.new-webauthn-device { + display: flex; + gap: 22px; + align-items: center; - .new-webauthn-device { - line-height: 300%; + &__name { + max-width: min(100vw, 400px); } +} </style> |