Skip to content

Commit 7eec3b5

Browse files
authored
Merge pull request #44761 from nextcloud/fix/deps-webauthn-lib
fix(deps): Bump web-auth/webauthn-lib from 3.3.9 to 4.8.5
2 parents 9028137 + a1a74cc commit 7eec3b5

30 files changed

+320
-355
lines changed

3rdparty

Submodule 3rdparty updated 1098 files

apps/settings/src/components/WebAuthn/AddDevice.vue

Lines changed: 69 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
{{ t('settings', 'Passwordless authentication requires a secure connection.') }}
2525
</div>
2626
<div v-else>
27-
<div v-if="step === RegistrationSteps.READY">
28-
<NcButton @click="start" type="primary">
29-
{{ t('settings', 'Add WebAuthn device') }}
30-
</NcButton>
31-
</div>
27+
<NcButton v-if="step === RegistrationSteps.READY"
28+
type="primary"
29+
@click="start">
30+
{{ t('settings', 'Add WebAuthn device') }}
31+
</NcButton>
3232

3333
<div v-else-if="step === RegistrationSteps.REGISTRATION"
3434
class="new-webauthn-device">
@@ -39,13 +39,14 @@
3939
<div v-else-if="step === RegistrationSteps.NAMING"
4040
class="new-webauthn-device">
4141
<span class="icon-loading-small webauthn-loading" />
42-
<input v-model="name"
43-
type="text"
44-
:placeholder="t('settings', 'Name your device')"
45-
@:keyup.enter="submit">
46-
<NcButton @click="submit" type="primary">
47-
{{ t('settings', 'Add') }}
48-
</NcButton>
42+
<NcTextField ref="nameInput"
43+
class="new-webauthn-device__name"
44+
:label="t('settings', 'Device name')"
45+
:value.sync="name"
46+
show-trailing-button
47+
:trailing-button-label="t('settings', 'Add')"
48+
trailing-button-icon="arrowRight"
49+
@trailing-button-click="submit" />
4950
</div>
5051

5152
<div v-else-if="step === RegistrationSteps.PERSIST"
@@ -61,15 +62,16 @@
6162
</template>
6263

6364
<script>
65+
import { showError } from '@nextcloud/dialogs'
6466
import { confirmPassword } from '@nextcloud/password-confirmation'
6567
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
66-
import '@nextcloud/password-confirmation/dist/style.css'
68+
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
6769
6870
import logger from '../../logger.ts'
6971
import {
7072
startRegistration,
7173
finishRegistration,
72-
} from '../../service/WebAuthnRegistrationSerice.js'
74+
} from '../../service/WebAuthnRegistrationSerice.ts'
7375
7476
const logAndPass = (text) => (data) => {
7577
logger.debug(text)
@@ -88,6 +90,7 @@ export default {
8890
8991
components: {
9092
NcButton,
93+
NcTextField,
9194
},
9295
9396
props: {
@@ -101,83 +104,55 @@ export default {
101104
default: false,
102105
},
103106
},
107+
108+
setup() {
109+
// non reactive props
110+
return {
111+
RegistrationSteps,
112+
}
113+
},
114+
104115
data() {
105116
return {
106117
name: '',
107118
credential: {},
108-
RegistrationSteps,
109119
step: RegistrationSteps.READY,
110120
}
111121
},
112-
methods: {
113-
arrayToBase64String(a) {
114-
return btoa(String.fromCharCode(...a))
122+
123+
watch: {
124+
/**
125+
* Auto focus the name input when naming a device
126+
*/
127+
step() {
128+
if (this.step === RegistrationSteps.NAMING) {
129+
this.$nextTick(() => this.$refs.nameInput?.focus())
130+
}
115131
},
116-
start() {
132+
},
133+
134+
methods: {
135+
/**
136+
* Start the registration process by loading the authenticator parameters
137+
* The next step is the naming of the device
138+
*/
139+
async start() {
117140
this.step = RegistrationSteps.REGISTRATION
118141
console.debug('Starting WebAuthn registration')
119142
120-
return confirmPassword()
121-
.then(this.getRegistrationData)
122-
.then(this.register.bind(this))
123-
.then(() => { this.step = RegistrationSteps.NAMING })
124-
.catch(err => {
125-
console.error(err.name, err.message)
126-
this.step = RegistrationSteps.READY
127-
})
128-
},
129-
130-
getRegistrationData() {
131-
console.debug('Fetching webauthn registration data')
132-
133-
const base64urlDecode = function(input) {
134-
// Replace non-url compatible chars with base64 standard chars
135-
input = input
136-
.replace(/-/g, '+')
137-
.replace(/_/g, '/')
138-
139-
// Pad out with standard base64 required padding characters
140-
const pad = input.length % 4
141-
if (pad) {
142-
if (pad === 1) {
143-
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding')
144-
}
145-
input += new Array(5 - pad).join('=')
146-
}
147-
148-
return window.atob(input)
143+
try {
144+
await confirmPassword()
145+
this.credential = await startRegistration()
146+
this.step = RegistrationSteps.NAMING
147+
} catch (err) {
148+
showError(err)
149+
this.step = RegistrationSteps.READY
149150
}
150-
151-
return startRegistration()
152-
.then(publicKey => {
153-
console.debug(publicKey)
154-
publicKey.challenge = Uint8Array.from(base64urlDecode(publicKey.challenge), c => c.charCodeAt(0))
155-
publicKey.user.id = Uint8Array.from(publicKey.user.id, c => c.charCodeAt(0))
156-
return publicKey
157-
})
158-
.catch(err => {
159-
console.error('Error getting webauthn registration data from server', err)
160-
throw new Error(t('settings', 'Server error while trying to add WebAuthn device'))
161-
})
162-
},
163-
164-
register(publicKey) {
165-
console.debug('starting webauthn registration')
166-
167-
return navigator.credentials.create({ publicKey })
168-
.then(data => {
169-
this.credential = {
170-
id: data.id,
171-
type: data.type,
172-
rawId: this.arrayToBase64String(new Uint8Array(data.rawId)),
173-
response: {
174-
clientDataJSON: this.arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
175-
attestationObject: this.arrayToBase64String(new Uint8Array(data.response.attestationObject)),
176-
},
177-
}
178-
})
179151
},
180152
153+
/**
154+
* Save the new device with the given name on the server
155+
*/
181156
submit() {
182157
this.step = RegistrationSteps.PERSIST
183158
@@ -187,12 +162,12 @@ export default {
187162
.then(logAndPass('registration data saved'))
188163
.then(() => this.reset())
189164
.then(logAndPass('app reset'))
190-
.catch(console.error.bind(this))
165+
.catch(console.error)
191166
},
192167
193168
async saveRegistrationData() {
194169
try {
195-
const device = await finishRegistration(this.name, JSON.stringify(this.credential))
170+
const device = await finishRegistration(this.name, this.credential)
196171
197172
logger.info('new device added', { device })
198173
@@ -212,15 +187,21 @@ export default {
212187
}
213188
</script>
214189
215-
<style scoped>
216-
.webauthn-loading {
217-
display: inline-block;
218-
vertical-align: sub;
219-
margin-left: 2px;
220-
margin-right: 2px;
221-
}
190+
<style scoped lang="scss">
191+
.webauthn-loading {
192+
display: inline-block;
193+
vertical-align: sub;
194+
margin-left: 2px;
195+
margin-right: 2px;
196+
}
197+
198+
.new-webauthn-device {
199+
display: flex;
200+
gap: 22px;
201+
align-items: center;
222202
223-
.new-webauthn-device {
224-
line-height: 300%;
203+
&__name {
204+
max-width: min(100vw, 400px);
225205
}
206+
}
226207
</style>

apps/settings/src/components/WebAuthn/Device.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
-->
2121

2222
<template>
23-
<div class="webauthn-device">
23+
<li class="webauthn-device">
2424
<span class="icon-webauthn-device" />
2525
{{ name || t('settings', 'Unnamed device') }}
2626
<NcActions :force-menu="true">
2727
<NcActionButton icon="icon-delete" @click="$emit('delete')">
2828
{{ t('settings', 'Delete') }}
2929
</NcActionButton>
3030
</NcActions>
31-
</div>
31+
</li>
3232
</template>
3333

3434
<script>

apps/settings/src/components/WebAuthn/Section.vue

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,30 @@
2828
<NcNoteCard v-if="devices.length === 0" type="info">
2929
{{ t('settings', 'No devices configured.') }}
3030
</NcNoteCard>
31-
<h3 v-else>
31+
32+
<h3 v-else id="security-webauthn__active-devices">
3233
{{ t('settings', 'The following devices are configured for your account:') }}
3334
</h3>
34-
<Device v-for="device in sortedDevices"
35-
:key="device.id"
36-
:name="device.name"
37-
@delete="deleteDevice(device.id)" />
35+
<ul aria-labelledby="security-webauthn__active-devices" class="security-webauthn__device-list">
36+
<Device v-for="device in sortedDevices"
37+
:key="device.id"
38+
:name="device.name"
39+
@delete="deleteDevice(device.id)" />
40+
</ul>
3841

39-
<NcNoteCard v-if="!hasPublicKeyCredential" type="warning">
42+
<NcNoteCard v-if="!supportsWebauthn" type="warning">
4043
{{ t('settings', 'Your browser does not support WebAuthn.') }}
4144
</NcNoteCard>
4245

43-
<AddDevice v-if="hasPublicKeyCredential"
46+
<AddDevice v-if="supportsWebauthn"
4447
:is-https="isHttps"
4548
:is-localhost="isLocalhost"
4649
@added="deviceAdded" />
4750
</div>
4851
</template>
4952

5053
<script>
54+
import { browserSupportsWebAuthn } from '@simplewebauthn/browser'
5155
import { confirmPassword } from '@nextcloud/password-confirmation'
5256
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
5357
import '@nextcloud/password-confirmation/dist/style.css'
@@ -79,11 +83,15 @@ export default {
7983
type: Boolean,
8084
default: false,
8185
},
82-
hasPublicKeyCredential: {
83-
type: Boolean,
84-
default: false,
85-
},
8686
},
87+
88+
setup() {
89+
// Non reactive properties
90+
return {
91+
supportsWebauthn: browserSupportsWebAuthn(),
92+
}
93+
},
94+
8795
data() {
8896
return {
8997
devices: this.initialDevices,
@@ -115,5 +123,7 @@ export default {
115123
</script>
116124

117125
<style scoped>
118-
126+
.security-webauthn__device-list {
127+
margin-block: 12px 18px;
128+
}
119129
</style>

apps/settings/src/main-personal-webauth.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,5 @@ new View({
3737
initialDevices: devices,
3838
isHttps: window.location.protocol === 'https:',
3939
isLocalhost: window.location.hostname === 'localhost',
40-
hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined',
4140
},
4241
}).$mount('#security-webauthn')

apps/settings/src/service/WebAuthnRegistrationSerice.js renamed to apps/settings/src/service/WebAuthnRegistrationSerice.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,55 @@
2020
*
2121
*/
2222

23-
import axios from '@nextcloud/axios'
23+
import type { RegistrationResponseJSON } from '@simplewebauthn/types'
24+
25+
import { translate as t } from '@nextcloud/l10n'
2426
import { generateUrl } from '@nextcloud/router'
27+
import { startRegistration as registerWebAuthn } from '@simplewebauthn/browser'
28+
29+
import Axios from 'axios'
30+
import axios from '@nextcloud/axios'
31+
import logger from '../logger'
2532

2633
/**
27-
*
34+
* Start registering a new device
35+
* @return The device attributes
2836
*/
2937
export async function startRegistration() {
3038
const url = generateUrl('/settings/api/personal/webauthn/registration')
3139

32-
const resp = await axios.get(url)
33-
return resp.data
40+
try {
41+
logger.debug('Fetching webauthn registration data')
42+
const { data } = await axios.get(url)
43+
logger.debug('Start webauthn registration')
44+
const attrs = await registerWebAuthn(data)
45+
return attrs
46+
} catch (e) {
47+
logger.error(e as Error)
48+
if (Axios.isAxiosError(e)) {
49+
throw new Error(t('settings', 'Could not register device: Network error'))
50+
} else if ((e as Error).name === 'InvalidStateError') {
51+
throw new Error(t('settings', 'Could not register device: Probably already registered'))
52+
}
53+
throw new Error(t('settings', 'Could not register device'))
54+
}
3455
}
3556

3657
/**
37-
* @param {any} name -
38-
* @param {any} data -
58+
* @param name Name of the device
59+
* @param data Device attributes
3960
*/
40-
export async function finishRegistration(name, data) {
61+
export async function finishRegistration(name: string, data: RegistrationResponseJSON) {
4162
const url = generateUrl('/settings/api/personal/webauthn/registration')
4263

43-
const resp = await axios.post(url, { name, data })
64+
const resp = await axios.post(url, { name, data: JSON.stringify(data) })
4465
return resp.data
4566
}
4667

4768
/**
48-
* @param {any} id -
69+
* @param id Remove registered device with that id
4970
*/
50-
export async function removeRegistration(id) {
71+
export async function removeRegistration(id: string | number) {
5172
const url = generateUrl(`/settings/api/personal/webauthn/registration/${id}`)
5273

5374
await axios.delete(url)

0 commit comments

Comments
 (0)