diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-04-10 05:30:10 +0200 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-04-16 11:51:03 +0200 |
commit | 3880e4c8d71feaf9d53d368058ef40ffaf616194 (patch) | |
tree | 27f0936ec890b67fad7b581bbefad689d3c18ecc | |
parent | e8452d9ef1c590c4a228d246e37178b687761d71 (diff) | |
download | nextcloud-server-3880e4c8d71feaf9d53d368058ef40ffaf616194.tar.gz nextcloud-server-3880e4c8d71feaf9d53d368058ef40ffaf616194.zip |
fix: Use `@simplewebauthn` for frontend logic
This simplifies the code a lot and fixes errors with the exisiting custom code,
where slightly different base64 values were emitted which are not valid according to the standard.
ref: https://github.com/web-auth/webauthn-framework/issues/510
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r-- | apps/settings/src/components/WebAuthn/AddDevice.vue | 157 | ||||
-rw-r--r-- | apps/settings/src/components/WebAuthn/Device.vue | 4 | ||||
-rw-r--r-- | apps/settings/src/components/WebAuthn/Section.vue | 34 | ||||
-rw-r--r-- | apps/settings/src/main-personal-webauth.js | 1 | ||||
-rw-r--r-- | apps/settings/src/service/WebAuthnRegistrationSerice.ts (renamed from apps/settings/src/service/WebAuthnRegistrationSerice.js) | 41 | ||||
-rw-r--r-- | core/src/components/login/PasswordLessLoginForm.vue | 134 | ||||
-rw-r--r-- | core/src/services/WebAuthnAuthenticationService.js | 44 | ||||
-rw-r--r-- | core/src/services/WebAuthnAuthenticationService.ts | 59 | ||||
-rw-r--r-- | core/src/views/Login.vue | 2 | ||||
-rw-r--r-- | package-lock.json | 15 | ||||
-rw-r--r-- | package.json | 2 |
11 files changed, 224 insertions, 269 deletions
diff --git a/apps/settings/src/components/WebAuthn/AddDevice.vue b/apps/settings/src/components/WebAuthn/AddDevice.vue index b9e9d087a0b..72077003cdd 100644 --- a/apps/settings/src/components/WebAuthn/AddDevice.vue +++ b/apps/settings/src/components/WebAuthn/AddDevice.vue @@ -24,11 +24,11 @@ {{ 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 +39,14 @@ <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> + <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" /> </div> <div v-else-if="step === RegistrationSteps.PERSIST" @@ -61,15 +62,16 @@ </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 NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import logger from '../../logger.ts' import { startRegistration, finishRegistration, -} from '../../service/WebAuthnRegistrationSerice.js' +} from '../../service/WebAuthnRegistrationSerice.ts' const logAndPass = (text) => (data) => { logger.debug(text) @@ -88,6 +90,7 @@ export default { components: { NcButton, + NcTextField, }, props: { @@ -101,83 +104,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 +162,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 +187,21 @@ 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-left: 2px; + margin-right: 2px; +} + +.new-webauthn-device { + display: flex; + gap: 22px; + align-items: center; - .new-webauthn-device { - line-height: 300%; + &__name { + max-width: min(100vw, 400px); } +} </style> diff --git a/apps/settings/src/components/WebAuthn/Device.vue b/apps/settings/src/components/WebAuthn/Device.vue index 1de2661b8dc..319c99c3184 100644 --- a/apps/settings/src/components/WebAuthn/Device.vue +++ b/apps/settings/src/components/WebAuthn/Device.vue @@ -20,7 +20,7 @@ --> <template> - <div class="webauthn-device"> + <li class="webauthn-device"> <span class="icon-webauthn-device" /> {{ name || t('settings', 'Unnamed device') }} <NcActions :force-menu="true"> @@ -28,7 +28,7 @@ {{ t('settings', 'Delete') }} </NcActionButton> </NcActions> - </div> + </li> </template> <script> diff --git a/apps/settings/src/components/WebAuthn/Section.vue b/apps/settings/src/components/WebAuthn/Section.vue index 2f5c840bdcc..7e9c75b9264 100644 --- a/apps/settings/src/components/WebAuthn/Section.vue +++ b/apps/settings/src/components/WebAuthn/Section.vue @@ -28,19 +28,22 @@ <NcNoteCard v-if="devices.length === 0" type="info"> {{ t('settings', 'No devices configured.') }} </NcNoteCard> - <h3 v-else> + + <h3 v-else id="security-webauthn__active-devices"> {{ t('settings', 'The following devices are configured for your account:') }} </h3> - <Device v-for="device in sortedDevices" - :key="device.id" - :name="device.name" - @delete="deleteDevice(device.id)" /> + <ul aria-labelledby="security-webauthn__active-devices" class="security-webauthn__device-list"> + <Device v-for="device in sortedDevices" + :key="device.id" + :name="device.name" + @delete="deleteDevice(device.id)" /> + </ul> - <NcNoteCard v-if="!hasPublicKeyCredential" type="warning"> + <NcNoteCard v-if="!supportsWebauthn" type="warning"> {{ t('settings', 'Your browser does not support WebAuthn.') }} </NcNoteCard> - <AddDevice v-if="hasPublicKeyCredential" + <AddDevice v-if="supportsWebauthn" :is-https="isHttps" :is-localhost="isLocalhost" @added="deviceAdded" /> @@ -48,6 +51,7 @@ </template> <script> +import { browserSupportsWebAuthn } from '@simplewebauthn/browser' import { confirmPassword } from '@nextcloud/password-confirmation' import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' import '@nextcloud/password-confirmation/dist/style.css' @@ -79,11 +83,15 @@ export default { type: Boolean, default: false, }, - hasPublicKeyCredential: { - type: Boolean, - default: false, - }, }, + + setup() { + // Non reactive properties + return { + supportsWebauthn: browserSupportsWebAuthn(), + } + }, + data() { return { devices: this.initialDevices, @@ -115,5 +123,7 @@ export default { </script> <style scoped> - +.security-webauthn__device-list { + margin-block: 12px 18px; +} </style> diff --git a/apps/settings/src/main-personal-webauth.js b/apps/settings/src/main-personal-webauth.js index dc11ecdbba2..edbdde6ea27 100644 --- a/apps/settings/src/main-personal-webauth.js +++ b/apps/settings/src/main-personal-webauth.js @@ -37,6 +37,5 @@ new View({ initialDevices: devices, isHttps: window.location.protocol === 'https:', isLocalhost: window.location.hostname === 'localhost', - hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined', }, }).$mount('#security-webauthn') diff --git a/apps/settings/src/service/WebAuthnRegistrationSerice.js b/apps/settings/src/service/WebAuthnRegistrationSerice.ts index 185dbd8cf28..f95395e865a 100644 --- a/apps/settings/src/service/WebAuthnRegistrationSerice.js +++ b/apps/settings/src/service/WebAuthnRegistrationSerice.ts @@ -20,34 +20,55 @@ * */ -import axios from '@nextcloud/axios' +import type { RegistrationResponseJSON } from '@simplewebauthn/types' + +import { translate as t } from '@nextcloud/l10n' import { generateUrl } from '@nextcloud/router' +import { startRegistration as registerWebAuthn } from '@simplewebauthn/browser' + +import Axios from 'axios' +import axios from '@nextcloud/axios' +import logger from '../logger' /** - * + * Start registering a new device + * @return The device attributes */ export async function startRegistration() { const url = generateUrl('/settings/api/personal/webauthn/registration') - const resp = await axios.get(url) - return resp.data + try { + logger.debug('Fetching webauthn registration data') + const { data } = await axios.get(url) + logger.debug('Start webauthn registration') + const attrs = await registerWebAuthn(data) + return attrs + } catch (e) { + logger.error(e as Error) + if (Axios.isAxiosError(e)) { + throw new Error(t('settings', 'Could not register device: Network error')) + } else if ((e as Error).name === 'InvalidStateError') { + throw new Error(t('settings', 'Could not register device: Probably already registered')) + } + throw new Error(t('settings', 'Could not register device')) + } } /** - * @param {any} name - - * @param {any} data - + * @param name Name of the device + * @param data Device attributes */ -export async function finishRegistration(name, data) { +export async function finishRegistration(name: string, data: RegistrationResponseJSON) { const url = generateUrl('/settings/api/personal/webauthn/registration') - const resp = await axios.post(url, { name, data }) + const resp = await axios.post(url, { name, data: JSON.stringify(data) }) return resp.data } /** - * @param {any} id - + * @param id Remove registered device with that id */ -export async function removeRegistration(id) { +export async function removeRegistration(id: string | number) { const url = generateUrl(`/settings/api/personal/webauthn/registration/${id}`) await axios.delete(url) diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue index 8a3886e52d0..128adddc303 100644 --- a/core/src/components/login/PasswordLessLoginForm.vue +++ b/core/src/components/login/PasswordLessLoginForm.vue @@ -1,5 +1,5 @@ <template> - <form v-if="(isHttps || isLocalhost) && hasPublicKeyCredential" + <form v-if="(isHttps || isLocalhost) && supportsWebauthn" ref="loginForm" method="post" name="login" @@ -20,7 +20,7 @@ @click="authenticate" /> </fieldset> </form> - <div v-else-if="!hasPublicKeyCredential" class="update"> + <div v-else-if="!supportsWebauthn" class="update"> <InformationIcon size="70" /> <h2>{{ t('core', 'Browser not supported') }}</h2> <p class="infogroup"> @@ -37,18 +37,16 @@ </template> <script> +import { browserSupportsWebAuthn } from '@simplewebauthn/browser' import { startAuthentication, finishAuthentication, -} from '../../services/WebAuthnAuthenticationService.js' +} from '../../services/WebAuthnAuthenticationService.ts' import LoginButton from './LoginButton.vue' import InformationIcon from 'vue-material-design-icons/Information.vue' import LockOpenIcon from 'vue-material-design-icons/LockOpen.vue' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' - -class NoValidCredentials extends Error { - -} +import logger from '../../logger' export default { name: 'PasswordLessLoginForm', @@ -79,11 +77,14 @@ export default { type: Boolean, default: false, }, - hasPublicKeyCredential: { - type: Boolean, - default: false, - }, }, + + setup() { + return { + supportsWebauthn: browserSupportsWebAuthn(), + } + }, + data() { return { user: this.username, @@ -92,7 +93,7 @@ export default { } }, methods: { - authenticate() { + async authenticate() { // check required fields if (!this.$refs.loginForm.checkValidity()) { return @@ -100,112 +101,25 @@ export default { 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) - }) + try { + const params = await startAuthentication(this.user) + await this.completeAuthentication(params) + } catch (error) { + if (error instanceof NoValidCredentials) { + this.validCredentials = false + return + } + logger.debug(error) + } }, changeUsername(username) { this.user = username this.$emit('update:username', this.user) }, - 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)) - } - - const arrayToString = function(a) { - return 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 ? arrayToString(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 redirectUrl = this.redirectUrl - return finishAuthentication(JSON.stringify(challenge)) + return finishAuthentication(challenge) .then(({ defaultRedirectUrl }) => { console.debug('Logged in redirecting') // Redirect url might be false so || should be used instead of ??. diff --git a/core/src/services/WebAuthnAuthenticationService.js b/core/src/services/WebAuthnAuthenticationService.js deleted file mode 100644 index 3eabceef5e4..00000000000 --- a/core/src/services/WebAuthnAuthenticationService.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0-or-later - * - * 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' - -/** - * @param {any} loginName - - */ -export function startAuthentication(loginName) { - const url = generateUrl('/login/webauthn/start') - - return Axios.post(url, { loginName }) - .then(resp => resp.data) -} - -/** - * @param {any} data - - */ -export function finishAuthentication(data) { - const url = generateUrl('/login/webauthn/finish') - - return Axios.post(url, { data }) - .then(resp => resp.data) -} diff --git a/core/src/services/WebAuthnAuthenticationService.ts b/core/src/services/WebAuthnAuthenticationService.ts new file mode 100644 index 00000000000..69b18551f62 --- /dev/null +++ b/core/src/services/WebAuthnAuthenticationService.ts @@ -0,0 +1,59 @@ +/** + * @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license AGPL-3.0-or-later + * + * 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 type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types' + +import { startAuthentication as startWebauthnAuthentication } from '@simplewebauthn/browser' +import { generateUrl } from '@nextcloud/router' + +import Axios from '@nextcloud/axios' +import logger from '../logger' + +export class NoValidCredentials extends Error {} + +/** + * Start webautn authentication + * This loads the challenge, connects to the authenticator and returns the repose that needs to be sent to the server. + * + * @param loginName Name to login + */ +export async function startAuthentication(loginName: string) { + const url = generateUrl('/login/webauthn/start') + + const { data } = await Axios.post<PublicKeyCredentialRequestOptionsJSON>(url, { loginName }) + if (!data.allowCredentials || data.allowCredentials.length === 0) { + logger.error('No valid credentials returned for webauthn') + throw new NoValidCredentials() + } + return await startWebauthnAuthentication(data) +} + +/** + * Verify webauthn authentication + * @param authData The authentication data to sent to the server + */ +export async function finishAuthentication(authData: AuthenticationResponseJSON) { + const url = generateUrl('/login/webauthn/finish') + + const { data } = await Axios.post(url, { data: JSON.stringify(authData) }) + return data +} diff --git a/core/src/views/Login.vue b/core/src/views/Login.vue index 57634bcb8f8..f6b0d2ec64c 100644 --- a/core/src/views/Login.vue +++ b/core/src/views/Login.vue @@ -73,7 +73,6 @@ :auto-complete-allowed="autoCompleteAllowed" :is-https="isHttps" :is-localhost="isLocalhost" - :has-public-key-credential="hasPublicKeyCredential" @submit="loading = true" /> <NcButton type="tertiary" :aria-label="t('core', 'Back to login form')" @@ -178,7 +177,6 @@ export default { alternativeLogins: loadState('core', 'alternativeLogins', []), isHttps: window.location.protocol === 'https:', isLocalhost: window.location.hostname === 'localhost', - hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined', hideLoginForm: loadState('core', 'hideLoginForm', false), emailStates: loadState('core', 'emailStates', []), } diff --git a/package-lock.json b/package-lock.json index 3819527f56e..b377598b8f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@nextcloud/sharing": "^0.1.0", "@nextcloud/upload": "^1.1.1", "@nextcloud/vue": "^8.11.2", + "@simplewebauthn/browser": "^9.0.1", "@skjnldsv/sanitize-svg": "^1.0.2", "@vueuse/components": "^10.7.2", "@vueuse/core": "^10.7.2", @@ -103,6 +104,7 @@ "@nextcloud/typings": "^1.8.0", "@nextcloud/webpack-vue-config": "^6.0.1", "@pinia/testing": "^0.1.2", + "@simplewebauthn/types": "^9.0.1", "@testing-library/jest-dom": "^6.1.5", "@testing-library/user-event": "^14.4.3", "@testing-library/vue": "^5.8.3", @@ -5061,6 +5063,19 @@ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "dev": true }, + "node_modules/@simplewebauthn/browser": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz", + "integrity": "sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==", + "dependencies": { + "@simplewebauthn/types": "^9.0.1" + } + }, + "node_modules/@simplewebauthn/types": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz", + "integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", diff --git a/package.json b/package.json index afe19dda465..d01b3f5a5ee 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@nextcloud/sharing": "^0.1.0", "@nextcloud/upload": "^1.1.1", "@nextcloud/vue": "^8.11.2", + "@simplewebauthn/browser": "^9.0.1", "@skjnldsv/sanitize-svg": "^1.0.2", "@vueuse/components": "^10.7.2", "@vueuse/core": "^10.7.2", @@ -130,6 +131,7 @@ "@nextcloud/typings": "^1.8.0", "@nextcloud/webpack-vue-config": "^6.0.1", "@pinia/testing": "^0.1.2", + "@simplewebauthn/types": "^9.0.1", "@testing-library/jest-dom": "^6.1.5", "@testing-library/user-event": "^14.4.3", "@testing-library/vue": "^5.8.3", |