Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(deps): Bump web-auth/webauthn-lib from 3.3.9 to 4.8.5 #44761

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3rdparty
Submodule 3rdparty updated 1098 files
157 changes: 69 additions & 88 deletions apps/settings/src/components/WebAuthn/AddDevice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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"
Expand All @@ -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)
Expand All @@ -88,6 +90,7 @@ export default {

components: {
NcButton,
NcTextField,
},

props: {
Expand All @@ -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

Expand All @@ -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 })

Expand All @@ -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>
4 changes: 2 additions & 2 deletions apps/settings/src/components/WebAuthn/Device.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
-->

<template>
<div class="webauthn-device">
<li class="webauthn-device">
<span class="icon-webauthn-device" />
{{ name || t('settings', 'Unnamed device') }}
<NcActions :force-menu="true">
<NcActionButton icon="icon-delete" @click="$emit('delete')">
{{ t('settings', 'Delete') }}
</NcActionButton>
</NcActions>
</div>
</li>
</template>

<script>
Expand Down
34 changes: 22 additions & 12 deletions apps/settings/src/components/WebAuthn/Section.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,30 @@
<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" />
</div>
</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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -115,5 +123,7 @@ export default {
</script>

<style scoped>

.security-webauthn__device-list {
margin-block: 12px 18px;
}
</style>
1 change: 0 additions & 1 deletion apps/settings/src/main-personal-webauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading