Signed-off-by: Wolfgang Bumiller <w.bumil...@proxmox.com>
---
 PVE/HTTPServer.pm                  |   2 +-
 www/index.html.tpl                 |   2 +
 www/manager6/Makefile              |   1 +
 www/manager6/Workspace.js          |   6 +-
 www/manager6/dc/TFAEdit.js         | 463 +++++++++++++++++++++++++++++++++++++
 www/manager6/dc/UserView.js        |  15 +-
 www/manager6/window/LoginWindow.js | 121 +++++++---
 7 files changed, 577 insertions(+), 33 deletions(-)
 create mode 100644 www/manager6/dc/TFAEdit.js

diff --git a/PVE/HTTPServer.pm b/PVE/HTTPServer.pm
index ec970010..ec57cd09 100755
--- a/PVE/HTTPServer.pm
+++ b/PVE/HTTPServer.pm
@@ -85,7 +85,7 @@ sub auth_handler {
        if (defined($challenge)) {
            $rpcenv->set_u2f_challenge($challenge);
            die "No ticket\n"
-               if ($rel_uri ne '/access/u2f' || $method ne 'POST');
+               if ($rel_uri ne '/access/tfa' || $method ne 'POST');
        }
 
        $rpcenv->set_user($username);
diff --git a/www/index.html.tpl b/www/index.html.tpl
index ae7f610f..2cb6d0c7 100644
--- a/www/index.html.tpl
+++ b/www/index.html.tpl
@@ -22,6 +22,8 @@
     [%- ELSE %]
     <script type="text/javascript" src="/pve2/ext6/ext-all.js"></script>
     <script type="text/javascript" src="/pve2/ext6/charts.js"></script>
+    <script type="text/javascript" src="/pve2/js/u2f-api.js"></script>
+    <script type="text/javascript" src="/pve2/js/qrcode.min.js"></script>
     [% END %]
     <script type="text/javascript">
     Proxmox = {
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 5ad70933..853fcb4f 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -200,6 +200,7 @@ JSSRC=                                                      
\
        dc/Guests.js                                    \
        dc/OptionView.js                                \
        dc/StorageView.js                               \
+       dc/TFAEdit.js                                   \
        dc/UserEdit.js                                  \
        dc/UserView.js                                  \
        dc/PoolView.js                                  \
diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js
index e88300f2..1d343525 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -19,8 +19,7 @@ Ext.define('PVE.Workspace', {
     updateLoginData: function(loginData) {
        var me = this;
        me.loginData = loginData;
-       Proxmox.CSRFPreventionToken = loginData.CSRFPreventionToken;
-       Proxmox.UserName = loginData.username;
+       Proxmox.Utils.setAuthData(loginData);
 
        var rt = me.down('pveResourceTree');
        rt.setDatacenterText(loginData.clustername);
@@ -29,9 +28,6 @@ Ext.define('PVE.Workspace', {
            Ext.state.Manager.set('GuiCap', loginData.cap);
        }
 
-       // creates a session cookie (expire = null) 
-       // that way the cookie gets deleted after browser window close
-       Ext.util.Cookies.set('PVEAuthCookie', loginData.ticket, null, '/', 
null, true);
        me.onLogin(loginData);
     },
 
diff --git a/www/manager6/dc/TFAEdit.js b/www/manager6/dc/TFAEdit.js
new file mode 100644
index 00000000..c92afa6b
--- /dev/null
+++ b/www/manager6/dc/TFAEdit.js
@@ -0,0 +1,463 @@
+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(values.name) +
+           '?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,
+           u2f_available: true,
+       }
+    },
+
+    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
+               )
+           );
+       }
+       //me.lookup('delete-button').setDisabled(has_tfa_configured);
+       //me.lookup('u2f-panel').setDisabled(has_tfa_configured);
+    },
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+       control: {
+           'field[qrupdate=true]': {
+               change: function() {
+                   var me = this.getView();
+                   me.updateQrCode();
+               }
+           },
+           '#': {
+               show: function() {
+                   var me = this.getView();
+                   me.down('#qrbox').getEl().appendChild(me.qrdiv);
+                   me.down('#qrbox').setVisible(false);
+
+                   if (Proxmox.UserName === 'root@pam') {
+                       me.lookup('password').setVisible(false);
+                       me.lookup('password').setDisabled(true);
+                   }
+                   me.lookup('challenge').setVisible(false);
+               }
+           },
+           '#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: {
+                               labelWidth: 120,
+                               anchor: '100%',
+                               padding: '0 5',
+                           },
+                           items: [
+                               {
+                                   xtype: 'textfield',
+                                   fieldLabel: gettext('Secret'),
+                                   name: 'secret',
+                                   reference: 'tfa-secret',
+                                   validateValue: function(value) {
+                                       return value.match(/^[A-Z2-7=]$/);
+                                   },
+                                   qrupdate: true,
+                                   padding: '5 5',
+                               },
+                               {
+                                   xtype: 'numberfield',
+                                   fieldLabel: gettext('Time period'),
+                                   name: 'step',
+                                   value: 30,
+                                   minValue: 10,
+                                   qrupdate: true,
+                               },
+                               {
+                                   xtype: 'numberfield',
+                                   fieldLabel: gettext('Digits'),
+                                   name: 'digits',
+                                   value: 6,
+                                   minValue: 6,
+                                   maxValue: 8,
+                                   qrupdate: true,
+                               },
+                               {
+                                   xtype: 'textfield',
+                                   fieldLabel: gettext('Issuer Name'),
+                                   name: 'issuer',
+                                   value: 'Proxmox Web UI',
+                                   qrupdate: true,
+                               },
+                               {
+                                   xtype: 'textfield',
+                                   fieldLabel: gettext('Account Name'),
+                                   name: 'name',
+                                   cbind: {
+                                       value: '{userid}',
+                                   },
+                                   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('Code'),
+                           labelWidth: 120,
+                           reference: 'challenge',
+                           padding: '0 5',
+                           emptyText: gettext('verify TOTP authentication 
code')
+                       }
+                   ]
+               },
+               {
+                   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',
+           padding: '0 5',
+           labelWidth: 120,
+           emptyText: gettext('verify current password')
+       }
+    ],
+
+    buttons: [
+       {
+           text: gettext('Randomize'),
+           reference: 'randomize-button',
+           handler: 'randomizeSecret',
+           bind: {
+               hidden: '{!in_totp_tab}',
+               disabled: '{!user_tfa}'
+           }
+       },
+       {
+           text: gettext('Apply'),
+           handler: 'applySettings',
+           bind: {
+               hidden: '{!in_totp_tab}',
+               disabled: '{!user_tfa}'
+           }
+       },
+       {
+           xtype: 'button',
+           text: gettext('Register U2F Device'),
+           handler: 'startU2FRegistration',
+           bind: {
+               hidden: '{in_totp_tab}'
+           }
+       },
+       {
+           text: gettext('Delete'),
+           reference: 'delete-button',
+           handler: 'deleteTFA',
+           bind: {
+               disabled: '{tfa_required}'
+           }
+       }
+    ],
+
+    initComponent: function() {
+       var me = this;
+
+       me.qrdiv = document.createElement('center');
+       me.qrcode = new QRCode(me.qrdiv, {
+           //text: "This is not the qr code you're looking for",
+           width: 256,
+           height: 256,
+           correctLevel: QRCode.CorrectLevel.M
+       });
+
+       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();
+    }
+});
diff --git a/www/manager6/dc/UserView.js b/www/manager6/dc/UserView.js
index 4d0c5595..f5c830ec 100644
--- a/www/manager6/dc/UserView.js
+++ b/www/manager6/dc/UserView.js
@@ -78,6 +78,19 @@ Ext.define('PVE.dc.UserView', {
            }
        });
 
+       var tfachange_btn = new Proxmox.button.Button({
+           text: gettext('TFA'),
+           disabled: true,
+           selModel: sm,
+           handler: function(btn, event, rec) {
+               var win = Ext.create('PVE.window.TFAEdit',{
+                    userid: rec.data.userid
+               });
+               win.on('destroy', reload);
+               win.show();
+           }
+       });
+
         var tbar = [
             {
                text: gettext('Add'),
@@ -89,7 +102,7 @@ Ext.define('PVE.dc.UserView', {
                     win.show();
                }
             },
-           edit_btn, remove_btn, pwchange_btn
+           edit_btn, remove_btn, pwchange_btn, tfachange_btn
         ];
 
        var render_username = function(userid) {
diff --git a/www/manager6/window/LoginWindow.js 
b/www/manager6/window/LoginWindow.js
index 5967c92f..93b06061 100644
--- a/www/manager6/window/LoginWindow.js
+++ b/www/manager6/window/LoginWindow.js
@@ -13,39 +13,108 @@ Ext.define('PVE.window.LoginWindow', {
            var saveunField = this.lookupReference('saveunField');
            var view = this.getView();
 
-           if(form.isValid()){
-               view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+           if (!form.isValid()) {
+               return;
+           }
+
+           var perform_u2f_fn;
+           var finish_u2f_fn;
+
+           var failure_fn = function(resp) {
+               view.el.unmask();
+               var handler = function() {
+                   var uf = me.lookupReference('usernameField');
+                   uf.focus(true, true);
+               };
+
+               Ext.MessageBox.alert(gettext('Error'),
+                                    gettext("Login failed. Please try again"),
+                                    handler);
+           };
+
+           var success_fn = function(data) {
+               var handler = view.handler || Ext.emptyFn;
+               handler.call(me, data);
+               view.close();
+           };
+
+           view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+
+           // set or clear username
+           var sp = Ext.state.Manager.getProvider();
+           if (saveunField.getValue() === true) {
+               sp.set(unField.getStateId(), unField.getValue());
+           } else {
+               sp.clear(unField.getStateId());
+           }
+           sp.set(saveunField.getStateId(), saveunField.getValue());
+
+           form.submit({
+               failure: function(f, resp){
+                   failure_fn(resp);
+               },
+               success: function(f, resp){
+                   view.el.unmask();
 
-               // set or clear username
-               var sp = Ext.state.Manager.getProvider();
-               if (saveunField.getValue() === true) {
-                   sp.set(unField.getStateId(), unField.getValue());
-               } else {
-                   sp.clear(unField.getStateId());
+                   var data = resp.result.data;
+                   if (Ext.isDefined(data.U2FChallenge)) {
+                       perform_u2f_fn(data);
+                   } else {
+                       success_fn(data);
+                   }
                }
-               sp.set(saveunField.getStateId(), saveunField.getValue());
+           });
+
+           perform_u2f_fn = function(data) {
+               // Store first factor login information first:
+               data.LoggedOut = true;
+               Proxmox.Utils.setAuthData(data);
+               // Show the message:
+               var msg = Ext.Msg.show({
+                   title: 'U2F: '+gettext('Verification'),
+                   message: gettext('Please press the button on your U2F 
Device'),
+                   buttons: []
+               });
+               var chlg = data.U2FChallenge;
+               var key = {
+                   version: chlg.version,
+                   keyHandle: chlg.keyHandle
+               };
+               u2f.sign(chlg.appId, chlg.challenge, [key], function(res) {
+                   msg.close();
+                   if (res.errorCode) {
+                       Proxmox.Utils.authClear();
+                       Ext.Msg.alert(gettext('Error'), "U2F Error: 
"+res.errorCode);
+                       return;
+                   }
+                   delete res.errorCode;
+                   finish_u2f_fn(res);
+               });
+           };
 
-               form.submit({
-                   failure: function(f, resp){
+           finish_u2f_fn = function(res) {
+               view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+               var params = { response: JSON.stringify(res) };
+               Proxmox.Utils.API2Request({
+                   url: '/api2/extjs/access/tfa',
+                   params: params,
+                   method: 'POST',
+                   timeout: 5000, // it'll delay both success & failure
+                   success: function(resp, opts) {
                        view.el.unmask();
-                       var handler = function() {
-                           var uf = me.lookupReference('usernameField');
-                           uf.focus(true, true);
-                       };
-
-                       Ext.MessageBox.alert(gettext('Error'),
-                                            gettext("Login failed. Please try 
again"),
-                                            handler);
+                       // Fill in what we copy over from the 1st factor:
+                       var data = resp.result.data;
+                       data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
+                       data.username = Proxmox.UserName;
+                       // Finish logging in:
+                       success_fn(data);
                    },
-                   success: function(f, resp){
-                       view.el.unmask();
-
-                       var handler = view.handler || Ext.emptyFn;
-                       handler.call(me, resp.result.data);
-                       view.close();
+                   failure: function(resp, opts) {
+                       Proxmox.Utils.authClear();
+                       failure_fn(resp);
                    }
                });
-           }
+           };
        },
 
        control: {
-- 
2.11.0


_______________________________________________
pve-devel mailing list
pve-devel@pve.proxmox.com
https://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to