aca4c2b5a9
some fido2/webauthn keys can have a pin, and the client can request a mode for the user verification. 'default' (no value set), lets the browser/device decide if the user has to enter the pin of the device 'discouraged' requests that the user should not need to enter the pin 'preferred' requests that the user should need to enter the pin (if possible) since we use webauthn only as a 2nd factor, having the user enter the device pin on login may seem too much hassle for some users, so give them the option since this is a client option anyway, do not save it in the backend, but in the browser local storage Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
215 lines
5.0 KiB
JavaScript
215 lines
5.0 KiB
JavaScript
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);
|
|
let userVerification = Ext.state.Manager.getProvider().get('webauthn-user-verification');
|
|
if (userVerification !== undefined) {
|
|
challenge_obj.publicKey.authenticatorSelection = {
|
|
userVerification,
|
|
};
|
|
}
|
|
|
|
challenge_obj.publicKey.user.id =
|
|
PBS.Utils.base64url_to_bytes(challenge_obj.publicKey.user.id);
|
|
|
|
let msg = Ext.Msg.show({
|
|
title: `Webauthn: ${gettext('Setup')}`,
|
|
message: gettext('Please press the button on your Webauthn Device'),
|
|
buttons: [],
|
|
});
|
|
|
|
let token_response = await navigator.credentials.create(challenge_obj);
|
|
|
|
// 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}',
|
|
},
|
|
},
|
|
],
|
|
});
|