[pve-devel] [PATCH v2 manager 5/5] ui: support u2f authentication and configuration
Wolfgang Bumiller
w.bumiller at proxmox.com
Tue Apr 2 12:22:08 CEST 2019
Signed-off-by: Wolfgang Bumiller <w.bumiller at 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 at 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 at 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 at 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 at 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 at 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
More information about the pve-devel
mailing list