all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH widget-toolkit 4/7] add Proxmox.window.TfaLoginWindow
Date: Tue,  9 Nov 2021 12:27:18 +0100	[thread overview]
Message-ID: <20211109112721.130935-30-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20211109112721.130935-1-w.bumiller@proxmox.com>

copied from pbs and added u2f tab

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
 src/Makefile            |   1 +
 src/window/TfaWindow.js | 429 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 430 insertions(+)
 create mode 100644 src/window/TfaWindow.js

diff --git a/src/Makefile b/src/Makefile
index fe915dd..afb0cb2 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -78,6 +78,7 @@ JSSRC=					\
 	window/FileBrowser.js		\
 	window/AuthEditBase.js		\
 	window/AuthEditOpenId.js	\
+	window/TfaWindow.js		\
 	node/APT.js			\
 	node/APTRepositories.js		\
 	node/NetworkEdit.js		\
diff --git a/src/window/TfaWindow.js b/src/window/TfaWindow.js
new file mode 100644
index 0000000..5026fb8
--- /dev/null
+++ b/src/window/TfaWindow.js
@@ -0,0 +1,429 @@
+/*global u2f*/
+Ext.define('Proxmox.window.TfaLoginWindow', {
+    extend: 'Ext.window.Window',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    title: gettext("Second login factor required"),
+
+    modal: true,
+    resizable: false,
+    width: 512,
+    layout: {
+	type: 'vbox',
+	align: 'stretch',
+    },
+
+    defaultButton: 'tfaButton',
+
+    viewModel: {
+	data: {
+	    confirmText: gettext('Confirm Second Factor'),
+	    canConfirm: false,
+	    availableChallenge: {},
+	},
+    },
+
+    cancelled: true,
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	init: function(view) {
+	    let me = this;
+	    let vm = me.getViewModel();
+
+	    if (!view.userid) {
+		throw "no userid given";
+	    }
+	    if (!view.ticket) {
+		throw "no ticket given";
+	    }
+	    const challenge = view.challenge;
+	    if (!challenge) {
+		throw "no challenge given";
+	    }
+
+	    let lastTabId = me.getLastTabUsed();
+	    let initialTab = -1, i = 0;
+	    for (const k of ['webauthn', 'totp', 'recovery', 'u2f']) {
+		const available = !!challenge[k];
+		vm.set(`availableChallenge.${k}`, available);
+
+		if (available) {
+		    if (i === lastTabId) {
+			initialTab = i;
+		    } else if (initialTab < 0) {
+			initialTab = i;
+		    }
+		}
+		i++;
+	    }
+	    view.down('tabpanel').setActiveTab(initialTab);
+
+	    if (challenge.recovery) {
+		me.lookup('availableRecovery').update(Ext.String.htmlEncode(
+		    gettext('Available recovery keys: ') + view.challenge.recovery.join(', '),
+		));
+		me.lookup('availableRecovery').setVisible(true);
+		if (view.challenge.recovery.length <= 3) {
+		    me.lookup('recoveryLow').setVisible(true);
+		}
+	    }
+
+	    if (challenge.webauthn && initialTab === 0) {
+		let _promise = me.loginWebauthn();
+	    } else if (challenge.u2f && initialTab === 3) {
+		let _promise = me.loginU2F();
+	    }
+	},
+	control: {
+	    'tabpanel': {
+		tabchange: function(tabPanel, newCard, oldCard) {
+		    // for now every TFA method has at max one field, so keep it simple..
+		    let oldField = oldCard.down('field');
+		    if (oldField) {
+			oldField.setDisabled(true);
+		    }
+		    let newField = newCard.down('field');
+		    if (newField) {
+			newField.setDisabled(false);
+			newField.focus();
+			newField.validate();
+		    }
+
+		    let confirmText = newCard.confirmText || gettext('Confirm Second Factor');
+		    this.getViewModel().set('confirmText', confirmText);
+
+		    this.saveLastTabUsed(tabPanel, newCard);
+		},
+	    },
+	    'field': {
+		validitychange: function(field, valid) {
+		    // triggers only for enabled fields and we disable the one from the
+		    // non-visible tab, so we can just directly use the valid param
+		    this.getViewModel().set('canConfirm', valid);
+		},
+		afterrender: field => field.focus(), // ensure focus after initial render
+	    },
+	},
+
+	saveLastTabUsed: function(tabPanel, card) {
+	    let id = tabPanel.items.indexOf(card);
+	    window.localStorage.setItem('Proxmox.TFALogin.lastTab', JSON.stringify({ id }));
+	},
+
+	getLastTabUsed: function() {
+	    let data = window.localStorage.getItem('Proxmox.TFALogin.lastTab');
+	    if (typeof data === 'string') {
+		let last = JSON.parse(data);
+		return last.id;
+	    }
+	    return null;
+	},
+
+	onClose: function() {
+	    let me = this;
+	    let view = me.getView();
+
+	    if (!view.cancelled) {
+		return;
+	    }
+
+	    view.onReject();
+	},
+
+	cancel: function() {
+	    this.getView().close();
+	},
+
+	loginTotp: function() {
+	    let me = this;
+
+	    let code = me.lookup('totp').getValue();
+	    let _promise = me.finishChallenge(`totp:${code}`);
+	},
+
+	loginWebauthn: async function() {
+	    let me = this;
+	    let view = me.getView();
+
+	    me.lookup('webAuthnWaiting').setVisible(true);
+	    me.lookup('webAuthnError').setVisible(false);
+
+	    let challenge = view.challenge.webauthn;
+
+	    if (typeof challenge.string !== 'string') {
+		// Byte array fixup, keep challenge string:
+		challenge.string = challenge.publicKey.challenge;
+		challenge.publicKey.challenge = Proxmox.Utils.base64url_to_bytes(challenge.string);
+		for (const cred of challenge.publicKey.allowCredentials) {
+		    cred.id = Proxmox.Utils.base64url_to_bytes(cred.id);
+		}
+	    }
+
+	    let controller = new AbortController();
+	    challenge.signal = controller.signal;
+
+	    let hwrsp;
+	    try {
+		//Promise.race( ...
+		hwrsp = await navigator.credentials.get(challenge);
+	    } catch (error) {
+		// we do NOT want to fail login because of canceling the challenge actively,
+		// in some browser that's the only way to switch over to another method as the
+		// disallow user input during the time the challenge is active
+		// checking for error.code === DOMException.ABORT_ERR only works in firefox -.-
+		this.getViewModel().set('canConfirm', true);
+		// FIXME: better handling, show some message, ...?
+		me.lookup('webAuthnError').setData({
+		    error: Ext.htmlEncode(error.toString()),
+		});
+		me.lookup('webAuthnError').setVisible(true);
+		return;
+	    } finally {
+		let waitingMessage = me.lookup('webAuthnWaiting');
+		if (waitingMessage) {
+		    waitingMessage.setVisible(false);
+		}
+	    }
+
+	    let response = {
+		id: hwrsp.id,
+		type: hwrsp.type,
+		challenge: challenge.string,
+		rawId: Proxmox.Utils.bytes_to_base64url(hwrsp.rawId),
+		response: {
+		    authenticatorData: Proxmox.Utils.bytes_to_base64url(
+			hwrsp.response.authenticatorData,
+		    ),
+		    clientDataJSON: Proxmox.Utils.bytes_to_base64url(hwrsp.response.clientDataJSON),
+		    signature: Proxmox.Utils.bytes_to_base64url(hwrsp.response.signature),
+		},
+	    };
+
+	    await me.finishChallenge("webauthn:" + JSON.stringify(response));
+	},
+
+	loginU2F: async function() {
+	    let me = this;
+	    let view = me.getView();
+
+	    me.lookup('u2fWaiting').setVisible(true);
+	    me.lookup('u2fError').setVisible(false);
+
+	    let hwrsp;
+	    try {
+		hwrsp = await new Promise((resolve, reject) => {
+		    try {
+			let data = view.challenge.u2f;
+			let chlg = data.challenge;
+			u2f.sign(chlg.appId, chlg.challenge, data.keys, resolve);
+		    } catch (error) {
+			reject(error);
+		    }
+		});
+		if (hwrsp.errorCode) {
+		    throw Proxmox.Utils.render_u2f_error(hwrsp.errorCode);
+		}
+		delete hwrsp.errorCode;
+	    } catch (error) {
+		this.getViewModel().set('canConfirm', true);
+		me.lookup('u2fError').setData({
+		    error: Ext.htmlEncode(error.toString()),
+		});
+		me.lookup('u2fError').setVisible(true);
+		return;
+	    } finally {
+		let waitingMessage = me.lookup('u2fWaiting');
+		if (waitingMessage) {
+		    waitingMessage.setVisible(false);
+		}
+	    }
+
+	    await me.finishChallenge("u2f:" + JSON.stringify(hwrsp));
+	},
+
+	loginRecovery: function() {
+	    let me = this;
+
+	    let key = me.lookup('recoveryKey').getValue();
+	    let _promise = me.finishChallenge(`recovery:${key}`);
+	},
+
+	loginTFA: function() {
+	    let me = this;
+	    // avoid triggering more than once during challenge
+	    me.getViewModel().set('canConfirm', false);
+	    let view = me.getView();
+	    let tfaPanel = view.down('tabpanel').getActiveTab();
+	    me[tfaPanel.handler]();
+	},
+
+	finishChallenge: function(password) {
+	    let me = this;
+	    let view = me.getView();
+	    view.cancelled = false;
+
+	    let params = {
+		username: view.userid,
+		'tfa-challenge': view.ticket,
+		password,
+	    };
+
+	    let resolve = view.onResolve;
+	    let reject = view.onReject;
+	    view.close();
+
+	    return Proxmox.Async.api2({
+		url: '/api2/extjs/access/ticket',
+		method: 'POST',
+		params,
+	    })
+	    .then(resolve)
+	    .catch(reject);
+	},
+    },
+
+    listeners: {
+	close: 'onClose',
+    },
+
+    items: [{
+	xtype: 'tabpanel',
+	region: 'center',
+	layout: 'fit',
+	bodyPadding: 10,
+	items: [
+	    {
+		xtype: 'panel',
+		title: 'WebAuthn',
+		iconCls: 'fa fa-fw fa-shield',
+		confirmText: gettext('Start WebAuthn challenge'),
+		handler: 'loginWebauthn',
+		bind: {
+		    disabled: '{!availableChallenge.webauthn}',
+		},
+		items: [
+		    {
+			xtype: 'box',
+			html: gettext('Please insert your authentication device and press its button'),
+		    },
+		    {
+			xtype: 'box',
+			html: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`,
+			reference: 'webAuthnWaiting',
+			hidden: true,
+		    },
+		    {
+			xtype: 'box',
+			data: {
+			    error: '',
+			},
+			tpl: '<i class="fa fa-warning warning"></i> {error}',
+			reference: 'webAuthnError',
+			hidden: true,
+		    },
+		],
+	    },
+	    {
+		xtype: 'panel',
+		title: gettext('TOTP App'),
+		iconCls: 'fa fa-fw fa-clock-o',
+		handler: 'loginTotp',
+		bind: {
+		    disabled: '{!availableChallenge.totp}',
+		},
+		items: [
+		    {
+			xtype: 'textfield',
+			fieldLabel: gettext('Please enter your TOTP verification code'),
+			labelWidth: 300,
+			name: 'totp',
+			disabled: true,
+			reference: 'totp',
+			allowBlank: false,
+			regex: /^[0-9]{2,16}$/,
+			regexText: gettext('TOTP codes usually consist of six decimal digits'),
+		    },
+		],
+	    },
+	    {
+		xtype: 'panel',
+		title: gettext('Recovery Key'),
+		iconCls: 'fa fa-fw fa-file-text-o',
+		handler: 'loginRecovery',
+		bind: {
+		    disabled: '{!availableChallenge.recovery}',
+		},
+		items: [
+		    {
+			xtype: 'box',
+			reference: 'availableRecovery',
+			hidden: true,
+		    },
+		    {
+			xtype: 'textfield',
+			fieldLabel: gettext('Please enter one of your single-use recovery keys'),
+			labelWidth: 300,
+			name: 'recoveryKey',
+			disabled: true,
+			reference: 'recoveryKey',
+			allowBlank: false,
+			regex: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
+			regexText: gettext('Does not look like a valid recovery key'),
+		    },
+		    {
+			xtype: 'box',
+			reference: 'recoveryLow',
+			hidden: true,
+			html: '<i class="fa fa-exclamation-triangle warning"></i>'
+			    + gettext('Less than {0} recovery keys available. Please generate a new set after login!'),
+		    },
+		],
+	    },
+	    {
+		xtype: 'panel',
+		title: 'U2F',
+		iconCls: 'fa fa-fw fa-shield',
+		confirmText: gettext('Start U2F challenge'),
+		handler: 'loginU2F',
+		bind: {
+		    disabled: '{!availableChallenge.u2f}',
+		},
+		items: [
+		    {
+			xtype: 'box',
+			html: gettext('Please insert your authentication device and press its button'),
+		    },
+		    {
+			xtype: 'box',
+			html: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`,
+			reference: 'u2fWaiting',
+			hidden: true,
+		    },
+		    {
+			xtype: 'box',
+			data: {
+			    error: '',
+			},
+			tpl: '<i class="fa fa-warning warning"></i> {error}',
+			reference: 'u2fError',
+			hidden: true,
+		    },
+		],
+	    },
+	],
+    }],
+
+    buttons: [
+	{
+	    handler: 'loginTFA',
+	    reference: 'tfaButton',
+	    disabled: true,
+	    bind: {
+		text: '{confirmText}',
+		disabled: '{!canConfirm}',
+	    },
+	},
+    ],
+});
-- 
2.30.2





  parent reply	other threads:[~2021-11-09 11:28 UTC|newest]

Thread overview: 43+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-09 11:26 [pve-devel] [PATCH multiple 0/9] PBS-like TFA support in PVE Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 1/6] import basic skeleton Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 2/6] import pve-rs Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 3/6] move apt to /perl-apt, use PERLMOD_PRODUCT env var Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 4/6] pve: add tfa api Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 5/6] build fix: pmg-rs is not here yet Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 6/6] Add some dev tips to a README Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 01/10] use rust parser for TFA config Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 02/10] update read_user_tfa_type call Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 03/10] use PBS-like auth api call flow Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 04/10] handle yubico authentication in new path Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 05/10] move TFA api path into its own module Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 06/10] add pbs-style TFA API implementation Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 07/10] support registering yubico otp keys Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 08/10] update tfa cleanup when deleting users Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 09/10] pveum: update tfa delete command Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 10/10] set/remove 'x' for tfa keys in user.cfg in new api Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH cluster] add webauthn configuration to datacenter.cfg Wolfgang Bumiller
2021-11-10 10:12   ` [pve-devel] applied: " Thomas Lamprecht
2021-11-09 11:27 ` [pve-devel] [PATCH common] Ticket: uri-escape colons Wolfgang Bumiller
2021-11-09 12:26   ` [pve-devel] applied: " Thomas Lamprecht
2021-11-09 11:27 ` [pve-devel] [PATCH manager 1/7] www: use render_u2f_error from wtk Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 2/7] www: use UserSelector " Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 3/7] use u2f-api.js and qrcode.min.js " Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 4/7] www: switch to new tfa login format Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 5/7] www: use af-address-book-o for realms Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 6/7] www: add TFA view to config Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 7/7] www: redirect user TFA button to TFA view Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 1/7] add pmxUserSelector Wolfgang Bumiller
2021-11-10  8:29   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 2/7] add Utils used for u2f and webauthn Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 3/7] add u2f-api.js and qrcode.min.js Wolfgang Bumiller
2021-11-10  8:31   ` Dominik Csapak
2021-11-09 11:27 ` Wolfgang Bumiller [this message]
2021-11-10  8:30   ` [pve-devel] applied: [PATCH widget-toolkit 4/7] add Proxmox.window.TfaLoginWindow Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 5/7] add totp, wa and recovery creation and tfa edit windows Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 6/7] add Proxmox.panel.TfaView Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 7/7] add yubico otp windows & login support Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-11 15:52 ` [pve-devel] applied-series: [PATCH multiple 0/9] PBS-like TFA support in PVE Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211109112721.130935-30-w.bumiller@proxmox.com \
    --to=w.bumiller@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal