ui: support u2f authentication and configuration
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
ca52c8e381
commit
9f197c22b5
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -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 \
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
|
463
www/manager6/dc/TFAEdit.js
Normal file
463
www/manager6/dc/TFAEdit.js
Normal file
@ -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();
|
||||
}
|
||||
});
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
// 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 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();
|
||||
|
||||
var data = resp.result.data;
|
||||
if (Ext.isDefined(data.U2FChallenge)) {
|
||||
perform_u2f_fn(data);
|
||||
} else {
|
||||
success_fn(data);
|
||||
}
|
||||
}
|
||||
sp.set(saveunField.getStateId(), saveunField.getValue());
|
||||
});
|
||||
|
||||
form.submit({
|
||||
failure: function(f, resp){
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
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: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user