mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-05-28 16:11:25 +00:00
493 lines
11 KiB
JavaScript
493 lines
11 KiB
JavaScript
/*global u2f,QRCode,Uint8Array*/
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.window.TFAEdit', {
|
|
extend: 'Ext.window.Window',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
modal: true,
|
|
resizable: false,
|
|
title: gettext('Two Factor Authentication'),
|
|
subject: 'TFA',
|
|
url: '/api2/extjs/access/tfa',
|
|
width: 512,
|
|
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
|
|
updateQrCode: function() {
|
|
var me = this;
|
|
var values = me.lookup('totp_form').getValues();
|
|
var algorithm = values.algorithm;
|
|
if (!algorithm) {
|
|
algorithm = 'SHA1';
|
|
}
|
|
|
|
me.qrcode.makeCode(
|
|
'otpauth://totp/' + encodeURIComponent(me.userid) +
|
|
'?secret=' + values.secret +
|
|
'&period=' + values.step +
|
|
'&digits=' + values.digits +
|
|
'&algorithm=' + algorithm +
|
|
'&issuer=' + encodeURIComponent(values.issuer)
|
|
);
|
|
|
|
me.lookup('challenge').setVisible(true);
|
|
me.down('#qrbox').setVisible(true);
|
|
},
|
|
|
|
showError: function(error) {
|
|
var ErrorNames = {
|
|
'1': gettext('Other Error'),
|
|
'2': gettext('Bad Request'),
|
|
'3': gettext('Configuration Unsupported'),
|
|
'4': gettext('Device Ineligible'),
|
|
'5': gettext('Timeout')
|
|
};
|
|
Ext.Msg.alert(
|
|
gettext('Error'),
|
|
"U2F Error: " + (ErrorNames[error] || Proxmox.Utils.unknownText)
|
|
);
|
|
},
|
|
|
|
doU2FChallenge: function(response) {
|
|
var me = this;
|
|
|
|
var data = response.result.data;
|
|
me.lookup('password').setDisabled(true);
|
|
var msg = Ext.Msg.show({
|
|
title: 'U2F: '+gettext('Setup'),
|
|
message: gettext('Please press the button on your U2F Device'),
|
|
buttons: []
|
|
});
|
|
Ext.Function.defer(function() {
|
|
u2f.register(data.appId, [data], [], function(data) {
|
|
msg.close();
|
|
if (data.errorCode) {
|
|
me.showError(data.errorCode);
|
|
} else {
|
|
me.respondToU2FChallenge(data);
|
|
}
|
|
});
|
|
}, 500, me);
|
|
},
|
|
|
|
respondToU2FChallenge: function(data) {
|
|
var me = this;
|
|
var params = {
|
|
userid: me.userid,
|
|
action: 'confirm',
|
|
response: JSON.stringify(data)
|
|
};
|
|
if (Proxmox.UserName !== 'root@pam') {
|
|
params.password = me.lookup('password').value;
|
|
}
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'PUT',
|
|
success: function() {
|
|
me.close();
|
|
Ext.Msg.show({
|
|
title: gettext('Success'),
|
|
message: gettext('U2F Device successfully connected.'),
|
|
buttons: Ext.Msg.OK
|
|
});
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
in_totp_tab: true,
|
|
tfa_required: false,
|
|
has_tfa: false,
|
|
valid: false,
|
|
u2f_available: true
|
|
},
|
|
formulas: {
|
|
canDeleteTFA: function(get) {
|
|
return (get('has_tfa') && !get('tfa_required'));
|
|
}
|
|
}
|
|
},
|
|
|
|
afterLoadingRealm: function(realm_tfa_type) {
|
|
var me = this;
|
|
var viewmodel = me.getViewModel();
|
|
if (!realm_tfa_type) {
|
|
// There's no TFA enforced by the realm, everything works.
|
|
viewmodel.set('u2f_available', true);
|
|
viewmodel.set('tfa_required', false);
|
|
} else if (realm_tfa_type === 'oath') {
|
|
// The realm explicitly requires TOTP
|
|
viewmodel.set('tfa_required', true);
|
|
viewmodel.set('u2f_available', false);
|
|
} else {
|
|
// The realm enforces some other TFA type (yubico)
|
|
me.close();
|
|
Ext.Msg.alert(
|
|
gettext('Error'),
|
|
Ext.String.format(
|
|
gettext("Custom 2nd factor configuration is not supported on realms with '{0}' TFA."),
|
|
realm_tfa_type
|
|
)
|
|
);
|
|
}
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
control: {
|
|
'field[qrupdate=true]': {
|
|
change: function() {
|
|
var me = this.getView();
|
|
me.updateQrCode();
|
|
}
|
|
},
|
|
'field': {
|
|
validitychange: function(field, valid) {
|
|
var me = this;
|
|
var viewModel = me.getViewModel();
|
|
var form = me.lookup('totp_form');
|
|
var challenge = me.lookup('challenge');
|
|
var password = me.lookup('password');
|
|
viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
|
|
}
|
|
},
|
|
'#': {
|
|
show: function() {
|
|
var me = this.getView();
|
|
var viewmodel = this.getViewModel();
|
|
|
|
me.qrdiv = document.createElement('center');
|
|
me.qrcode = new QRCode(me.qrdiv, {
|
|
width: 256,
|
|
height: 256,
|
|
correctLevel: QRCode.CorrectLevel.M
|
|
});
|
|
me.down('#qrbox').getEl().appendChild(me.qrdiv);
|
|
|
|
viewmodel.set('has_tfa', me.hasTFA);
|
|
if (!me.hasTFA) {
|
|
this.randomizeSecret();
|
|
} else {
|
|
me.down('#qrbox').setVisible(false);
|
|
me.lookup('challenge').setVisible(false);
|
|
}
|
|
|
|
if (Proxmox.UserName === 'root@pam') {
|
|
me.lookup('password').setVisible(false);
|
|
me.lookup('password').setDisabled(true);
|
|
}
|
|
}
|
|
},
|
|
'#tfatabs': {
|
|
tabchange: function(panel, newcard) {
|
|
var viewmodel = this.getViewModel();
|
|
viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel');
|
|
}
|
|
}
|
|
},
|
|
|
|
applySettings: function() {
|
|
var me = this;
|
|
var values = me.lookup('totp_form').getValues();
|
|
var params = {
|
|
userid: me.getView().userid,
|
|
action: 'new',
|
|
key: values.secret,
|
|
config: PVE.Parser.printPropertyString({
|
|
type: 'oath',
|
|
digits: values.digits,
|
|
step: values.step
|
|
}),
|
|
// this is used to verify that the client generates the correct codes:
|
|
response: me.lookup('challenge').value
|
|
};
|
|
|
|
if (Proxmox.UserName !== 'root@pam') {
|
|
params.password = me.lookup('password').value;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'PUT',
|
|
waitMsgTarget: me.getView(),
|
|
success: function(response, opts) {
|
|
me.getView().close();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
|
|
deleteTFA: function() {
|
|
var me = this;
|
|
var values = me.lookup('totp_form').getValues();
|
|
var params = {
|
|
userid: me.getView().userid,
|
|
action: 'delete'
|
|
};
|
|
|
|
if (Proxmox.UserName !== 'root@pam') {
|
|
params.password = me.lookup('password').value;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'PUT',
|
|
waitMsgTarget: me.getView(),
|
|
success: function(response, opts) {
|
|
me.getView().close();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
|
|
randomizeSecret: function() {
|
|
var me = this;
|
|
var rnd = new Uint8Array(16);
|
|
window.crypto.getRandomValues(rnd);
|
|
var data = '';
|
|
rnd.forEach(function(b) {
|
|
// just use the first 5 bit
|
|
b = b & 0x1f;
|
|
if (b < 26) {
|
|
// A..Z
|
|
data += String.fromCharCode(b + 0x41);
|
|
} else {
|
|
// 2..7
|
|
data += String.fromCharCode(b-26 + 0x32);
|
|
}
|
|
});
|
|
me.lookup('tfa_secret').setValue(data);
|
|
},
|
|
|
|
startU2FRegistration: function() {
|
|
var me = this;
|
|
|
|
var params = {
|
|
userid: me.getView().userid,
|
|
action: 'new'
|
|
};
|
|
|
|
if (Proxmox.UserName !== 'root@pam') {
|
|
params.password = me.lookup('password').value;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'PUT',
|
|
waitMsgTarget: me.getView(),
|
|
success: function(response) {
|
|
me.getView().doU2FChallenge(response);
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'tabpanel',
|
|
itemId: 'tfatabs',
|
|
border: false,
|
|
items: [
|
|
{
|
|
xtype: 'panel',
|
|
title: 'TOTP',
|
|
itemId: 'totp-panel',
|
|
border: false,
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'form',
|
|
layout: 'anchor',
|
|
border: false,
|
|
reference: 'totp_form',
|
|
fieldDefaults: {
|
|
anchor: '100%',
|
|
padding: '0 5'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('User name'),
|
|
cbind: {
|
|
value: '{userid}'
|
|
}
|
|
},
|
|
{
|
|
layout: 'hbox',
|
|
border: false,
|
|
padding: '0 0 5 0',
|
|
items: [{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Secret'),
|
|
emptyText: gettext('Unchanged'),
|
|
name: 'secret',
|
|
reference: 'tfa_secret',
|
|
regex: /^[A-Z2-7=]+$/,
|
|
regexText: 'Must be base32 [A-Z2-7=]',
|
|
maskRe: /[A-Z2-7=]/,
|
|
qrupdate: true,
|
|
flex: 4
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
text: gettext('Randomize'),
|
|
reference: 'randomize_button',
|
|
handler: 'randomizeSecret',
|
|
flex: 1
|
|
}]
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
fieldLabel: gettext('Time period'),
|
|
name: 'step',
|
|
// Google Authenticator ignores this and generates bogus data
|
|
hidden: true,
|
|
value: 30,
|
|
minValue: 10,
|
|
qrupdate: true
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
fieldLabel: gettext('Digits'),
|
|
name: 'digits',
|
|
value: 6,
|
|
// Google Authenticator ignores this and generates bogus data
|
|
hidden: true,
|
|
minValue: 6,
|
|
maxValue: 8,
|
|
qrupdate: true
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Issuer Name'),
|
|
name: 'issuer',
|
|
value: 'Proxmox Web UI',
|
|
qrupdate: true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
itemId: 'qrbox',
|
|
visible: false, // will be enabled when generating a qr code
|
|
style: {
|
|
'background-color': 'white',
|
|
padding: '5px',
|
|
width: '266px',
|
|
height: '266px'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Verification Code'),
|
|
allowBlank: false,
|
|
reference: 'challenge',
|
|
padding: '0 5',
|
|
emptyText: gettext('Scan QR code and enter TOTP auth. code to verify')
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'U2F',
|
|
itemId: 'u2f-panel',
|
|
reference: 'u2f_panel',
|
|
border: false,
|
|
padding: '5 5',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'middle'
|
|
},
|
|
bind: {
|
|
disabled: '{!u2f_available}'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'label',
|
|
width: 500,
|
|
text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.')
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
inputType: 'password',
|
|
fieldLabel: gettext('Password'),
|
|
minLength: 5,
|
|
reference: 'password',
|
|
allowBlank: false,
|
|
validateBlank: true,
|
|
padding: '0 0 5 5',
|
|
emptyText: gettext('verify current password')
|
|
}
|
|
],
|
|
|
|
buttons: [
|
|
{
|
|
text: gettext('Apply'),
|
|
handler: 'applySettings',
|
|
bind: {
|
|
hidden: '{!in_totp_tab}',
|
|
disabled: '{!valid}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
text: gettext('Register U2F Device'),
|
|
handler: 'startU2FRegistration',
|
|
bind: {
|
|
hidden: '{in_totp_tab}'
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Delete'),
|
|
reference: 'delete_button',
|
|
handler: 'deleteTFA',
|
|
bind: {
|
|
disabled: '{!canDeleteTFA}'
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-domains',
|
|
autoLoad: true
|
|
});
|
|
|
|
store.on('load', function() {
|
|
var user_realm = me.userid.split('@')[1];
|
|
var realm = me.store.findRecord('realm', user_realm);
|
|
me.afterLoadingRealm(realm && realm.data && realm.data.tfa);
|
|
}, me);
|
|
|
|
Ext.apply(me, { store: store });
|
|
|
|
me.callParent();
|
|
}
|
|
});
|