proxmox-backup/www/window/AddTotp.js
Thomas Lamprecht 3189d05134 ui: tfa: specify which confirmation password is required
Clarify that the password of the user one wants to add TFA too is
required, which is not necessarily the one of the current logged in
user. Use an empty text for that.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-01-18 10:12:23 +01:00

298 lines
6.7 KiB
JavaScript

/*global QRCode*/
Ext.define('PBS.window.AddTotp', {
extend: 'Proxmox.window.Edit',
alias: 'widget.pbsAddTotp',
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'user_mgmt',
modal: true,
resizable: false,
title: gettext('Add a TOTP login factor'),
width: 512,
layout: {
type: 'vbox',
align: 'stretch',
},
isAdd: true,
userid: undefined,
tfa_id: undefined,
fixedUser: false,
updateQrCode: function() {
let me = this;
let values = me.lookup('totp_form').getValues();
let algorithm = values.algorithm;
if (!algorithm) {
algorithm = 'SHA1';
}
let otpuri =
'otpauth://totp/' + encodeURIComponent(values.userid) +
'?secret=' + values.secret +
'&period=' + values.step +
'&digits=' + values.digits +
'&algorithm=' + algorithm +
'&issuer=' + encodeURIComponent(values.issuer);
me.getController().getViewModel().set('otpuri', otpuri);
me.qrcode.makeCode(otpuri);
me.lookup('challenge').setVisible(true);
me.down('#qrbox').setVisible(true);
},
viewModel: {
data: {
valid: false,
secret: '',
otpuri: '',
userid: Proxmox.UserName,
},
formulas: {
secretEmpty: function(get) {
return get('secret').length === 0;
},
passwordConfirmText: (get) => {
let id = get('userid');
return Ext.String.format(gettext("Confirm password of '{0}'"), id);
},
},
},
controller: {
xclass: 'Ext.app.ViewController',
control: {
'field[qrupdate=true]': {
change: function() {
this.getView().updateQrCode();
},
},
'field': {
validitychange: function(field, valid) {
let me = this;
let viewModel = me.getViewModel();
let form = me.lookup('totp_form');
let challenge = me.lookup('challenge');
let password = me.lookup('password');
viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
},
},
'#': {
show: function() {
let me = this;
let view = me.getView();
view.qrdiv = document.createElement('div');
view.qrcode = new QRCode(view.qrdiv, {
width: 256,
height: 256,
correctLevel: QRCode.CorrectLevel.M,
});
view.down('#qrbox').getEl().appendChild(view.qrdiv);
view.getController().randomizeSecret();
if (Proxmox.UserName === 'root@pam') {
view.lookup('password').setVisible(false);
view.lookup('password').setDisabled(true);
}
},
},
},
randomizeSecret: function() {
let me = this;
let rnd = new Uint8Array(32);
window.crypto.getRandomValues(rnd);
let data = '';
rnd.forEach(function(b) {
// secret must be base32, so just use the first 5 bits
b = b & 0x1f;
if (b < 26) {
// A..Z
data += String.fromCharCode(b + 0x41);
} else {
// 2..7
data += String.fromCharCode(b-26 + 0x32);
}
});
me.getViewModel().set('secret', data);
},
},
items: [
{
xtype: 'form',
layout: 'anchor',
border: false,
reference: 'totp_form',
fieldDefaults: {
anchor: '100%',
},
items: [
{
xtype: 'pmxDisplayEditField',
name: 'userid',
cbind: {
editable: (get) => get('isAdd') && !get('fixedUser'),
},
fieldLabel: gettext('User'),
editConfig: {
xtype: 'pbsUserSelector',
allowBlank: false,
},
renderer: Ext.String.htmlEncode,
value: Proxmox.UserName,
listeners: {
change: function(field, newValue, oldValue) {
let vm = this.up('window').getViewModel();
vm.set('userid', newValue);
},
},
qrupdate: true,
},
{
xtype: 'textfield',
fieldLabel: gettext('Description'),
emptyText: gettext('For example: TFA device ID, required to identify multiple factors.'),
allowBlank: false,
name: 'description',
maxLength: 256,
},
{
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,
bind: {
value: "{secret}",
},
flex: 4,
padding: '0 5 0 0',
},
{
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 Backup Server - ${Proxmox.NodeName}`,
qrupdate: true,
},
{
xtype: 'box',
itemId: 'qrbox',
visible: false, // will be enabled when generating a qr code
bind: {
visible: '{!secretEmpty}',
},
style: {
'background-color': 'white',
'margin-left': 'auto',
'margin-right': 'auto',
padding: '5px',
width: '266px',
height: '266px',
},
},
{
xtype: 'textfield',
fieldLabel: gettext('Verify Code'),
allowBlank: false,
reference: 'challenge',
name: 'challenge',
bind: {
disabled: '{!showTOTPVerifiction}',
visible: '{showTOTPVerifiction}',
},
emptyText: gettext('Scan QR code in a TOTP app and enter an auth. code here'),
},
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('Verify Password'),
minLength: 5,
reference: 'password',
name: 'password',
allowBlank: false,
validateBlank: true,
bind: {
emptyText: '{passwordConfirmText}',
},
},
],
},
],
initComponent: function() {
let me = this;
me.url = '/api2/extjs/access/tfa/';
me.method = 'POST';
me.callParent();
},
getValues: function(dirtyOnly) {
let me = this;
let viewmodel = me.getController().getViewModel();
let values = me.callParent(arguments);
let uid = encodeURIComponent(values.userid);
me.url = `/api2/extjs/access/tfa/${uid}`;
delete values.userid;
let data = {
description: values.description,
type: "totp",
totp: viewmodel.get('otpuri'),
value: values.challenge,
};
if (values.password) {
data.password = values.password;
}
return data;
},
});