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" >
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"
6162</template >
6263
6364<script >
65+ import { showError } from ' @nextcloud/dialogs'
6466import { confirmPassword } from ' @nextcloud/password-confirmation'
6567import 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
6870import logger from ' ../../logger.ts'
6971import {
7072 startRegistration ,
7173 finishRegistration ,
72- } from ' ../../service/WebAuthnRegistrationSerice.js '
74+ } from ' ../../service/WebAuthnRegistrationSerice.ts '
7375
7476const 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>
0 commit comments