Ext.define('PBS.window.AddWebauthn', { extend: 'Ext.window.Window', alias: 'widget.pbsAddWebauthn', mixins: ['Proxmox.Mixin.CBind'], onlineHelp: 'user_mgmt', modal: true, resizable: false, title: gettext('Add a Webauthn login token'), width: 512, user: undefined, fixedUser: false, initComponent: function() { let me = this; me.callParent(); Ext.GlobalEvents.fireEvent('proxmoxShowHelp', me.onlineHelp); }, viewModel: { data: { valid: false, userid: null, }, }, controller: { xclass: 'Ext.app.ViewController', control: { 'field': { validitychange: function(field, valid) { let me = this; let viewmodel = me.getViewModel(); let form = me.lookup('webauthn_form'); viewmodel.set('valid', form.isValid()); }, }, '#': { show: function() { let me = this; let view = me.getView(); if (Proxmox.UserName === 'root@pam') { view.lookup('password').setVisible(false); view.lookup('password').setDisabled(true); } }, }, }, registerWebauthn: async function() { let me = this; let values = me.lookup('webauthn_form').getValues(); values.type = "webauthn"; let userid = values.user; delete values.user; me.getView().mask(gettext('Please wait...'), 'x-mask-loading'); try { let register_response = await PBS.Async.api2({ url: `/api2/extjs/access/tfa/${userid}`, method: 'POST', params: values, }); let data = register_response.result.data; if (!data.challenge) { throw "server did not respond with a challenge"; } let challenge_obj = JSON.parse(data.challenge); // Fix this up before passing it to the browser, but keep a copy of the original // string to pass in the response: let challenge_str = challenge_obj.publicKey.challenge; challenge_obj.publicKey.challenge = PBS.Utils.base64url_to_bytes(challenge_str); challenge_obj.publicKey.user.id = PBS.Utils.base64url_to_bytes(challenge_obj.publicKey.user.id); // convert existing authenticators structure challenge_obj.publicKey.excludeCredentials = (challenge_obj.publicKey.excludeCredentials || []).map((cred) => ({ id: PBS.Utils.base64url_to_bytes(cred.id), type: cred.type, })); let msg = Ext.Msg.show({ title: `Webauthn: ${gettext('Setup')}`, message: gettext('Please press the button on your Webauthn Device'), buttons: [], }); let token_response; try { token_response = await navigator.credentials.create(challenge_obj); } catch (error) { let errmsg = ` ${error.name}: ${error.message}`; if (error.name === 'InvalidStateError') { // probably a duplicate token throw `${gettext('There was an error during authenticator registration.')}
${gettext('This probably means that this authenticator is already registered.')}

${errmsg}`; } else { throw `${gettext('There was an error during token registration.')}

${errmsg}`; } } // We cannot pass ArrayBuffers to the API, so extract & convert the data. let response = { id: token_response.id, type: token_response.type, rawId: PBS.Utils.bytes_to_base64url(token_response.rawId), response: { attestationObject: PBS.Utils.bytes_to_base64url( token_response.response.attestationObject, ), clientDataJSON: PBS.Utils.bytes_to_base64url( token_response.response.clientDataJSON, ), }, }; msg.close(); let params = { type: "webauthn", challenge: challenge_str, value: JSON.stringify(response), }; if (values.password) { params.password = values.password; } await PBS.Async.api2({ url: `/api2/extjs/access/tfa/${userid}`, method: 'POST', params, }); } catch (error) { console.error(error); // for debugging if it's not displayable... Ext.Msg.alert(gettext('Error'), error); } me.getView().close(); }, }, items: [ { xtype: 'form', reference: 'webauthn_form', layout: 'anchor', border: false, bodyPadding: 10, fieldDefaults: { anchor: '100%', }, items: [ { xtype: 'pmxDisplayEditField', name: 'user', cbind: { editable: (get) => !get('fixedUser'), value: () => Proxmox.UserName, }, fieldLabel: gettext('User'), editConfig: { xtype: 'pbsUserSelector', allowBlank: false, }, renderer: Ext.String.htmlEncode, listeners: { change: function(field, newValue, oldValue) { let vm = this.up('window').getViewModel(); vm.set('userid', newValue); }, }, }, { xtype: 'textfield', fieldLabel: gettext('Description'), allowBlank: false, name: 'description', maxLength: 256, emptyText: gettext('For example: TFA device ID, required to identify multiple factors.'), }, { xtype: 'textfield', name: 'password', reference: 'password', fieldLabel: gettext('Verify Password'), inputType: 'password', minLength: 5, allowBlank: false, validateBlank: true, cbind: { hidden: () => Proxmox.UserName === 'root@pam', disabled: () => Proxmox.UserName === 'root@pam', emptyText: () => Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName), }, }, ], }, ], buttons: [ { xtype: 'proxmoxHelpButton', }, '->', { xtype: 'button', text: gettext('Register Webauthn Device'), handler: 'registerWebauthn', bind: { disabled: '{!valid}', }, }, ], });