]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/dc/TFAEdit.js
ui: eslint: fix trailing comma and comma related whitespaces errors
[pve-manager.git] / www / manager6 / dc / TFAEdit.js
CommitLineData
6a1c9c29 1/*global u2f,QRCode,Uint8Array*/
24d2ed8c
WB
2Ext.define('PVE.window.TFAEdit', {
3 extend: 'Ext.window.Window',
4 mixins: ['Proxmox.Mixin.CBind'],
5
db2af549
TL
6 onlineHelp: 'pveum_tfa_auth', // fake to ensure this gets a link target
7
24d2ed8c
WB
8 modal: true,
9 resizable: false,
10 title: gettext('Two Factor Authentication'),
11 subject: 'TFA',
12 url: '/api2/extjs/access/tfa',
13 width: 512,
14
15 layout: {
16 type: 'vbox',
f6710aac 17 align: 'stretch',
24d2ed8c
WB
18 },
19
20 updateQrCode: function() {
21 var me = this;
b688436d 22 var values = me.lookup('totp_form').getValues();
24d2ed8c
WB
23 var algorithm = values.algorithm;
24 if (!algorithm) {
25 algorithm = 'SHA1';
26 }
27
28 me.qrcode.makeCode(
e3aed879
TL
29 'otpauth://totp/' +
30 encodeURIComponent(values.issuer) +
31 ':' +
32 encodeURIComponent(values.userid) +
24d2ed8c
WB
33 '?secret=' + values.secret +
34 '&period=' + values.step +
35 '&digits=' + values.digits +
36 '&algorithm=' + algorithm +
f6710aac 37 '&issuer=' + encodeURIComponent(values.issuer),
24d2ed8c
WB
38 );
39
40 me.lookup('challenge').setVisible(true);
41 me.down('#qrbox').setVisible(true);
42 },
43
44 showError: function(error) {
24d2ed8c
WB
45 Ext.Msg.alert(
46 gettext('Error'),
f6710aac 47 PVE.Utils.render_u2f_error(error),
24d2ed8c
WB
48 );
49 },
50
51 doU2FChallenge: function(response) {
52 var me = this;
53
54 var data = response.result.data;
55 me.lookup('password').setDisabled(true);
56 var msg = Ext.Msg.show({
57 title: 'U2F: '+gettext('Setup'),
58 message: gettext('Please press the button on your U2F Device'),
f6710aac 59 buttons: [],
24d2ed8c
WB
60 });
61 Ext.Function.defer(function() {
62 u2f.register(data.appId, [data], [], function(data) {
63 msg.close();
64 if (data.errorCode) {
65 me.showError(data.errorCode);
66 } else {
67 me.respondToU2FChallenge(data);
68 }
69 });
70 }, 500, me);
71 },
72
73 respondToU2FChallenge: function(data) {
74 var me = this;
75 var params = {
76 userid: me.userid,
77 action: 'confirm',
f6710aac 78 response: JSON.stringify(data),
24d2ed8c
WB
79 };
80 if (Proxmox.UserName !== 'root@pam') {
81 params.password = me.lookup('password').value;
82 }
83 Proxmox.Utils.API2Request({
84 url: '/api2/extjs/access/tfa',
85 params: params,
86 method: 'PUT',
87 success: function() {
88 me.close();
89 Ext.Msg.show({
90 title: gettext('Success'),
91 message: gettext('U2F Device successfully connected.'),
f6710aac 92 buttons: Ext.Msg.OK,
24d2ed8c
WB
93 });
94 },
95 failure: function(response, opts) {
96 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
f6710aac 97 },
24d2ed8c
WB
98 });
99 },
100
101 viewModel: {
102 data: {
103 in_totp_tab: true,
104 tfa_required: false,
ec505260 105 tfa_type: null, // dependencies of formulas should not be undefined
0509c444 106 valid: false,
bfdfea50
DC
107 u2f_available: true,
108 secret: "",
aa8d628f
TL
109 },
110 formulas: {
bfdfea50
DC
111 showTOTPVerifiction: function(get) {
112 return get('secret').length > 0 && get('canSetupTOTP');
113 },
aa8d628f 114 canDeleteTFA: function(get) {
6dfb9ec4 115 return (get('tfa_type') !== null && !get('tfa_required'));
b0184fc4
TL
116 },
117 canSetupTOTP: function(get) {
118 var tfa = get('tfa_type');
6dfb9ec4 119 return (tfa === null || tfa === 'totp' || tfa === 1);
b0184fc4
TL
120 },
121 canSetupU2F: function(get) {
122 var tfa = get('tfa_type');
6dfb9ec4 123 return (get('u2f_available') && (tfa === null || tfa === 'u2f' || tfa === 1));
bfdfea50
DC
124 },
125 secretEmpty: function(get) {
126 return get('secret').length === 0;
127 },
128 selectedTab: function(get) {
129 return (get('tfa_type') || 'totp') + '-panel';
130 },
f6710aac 131 },
24d2ed8c
WB
132 },
133
5bb10629 134 afterLoading: function(realm_tfa_type, user_tfa_type) {
24d2ed8c
WB
135 var me = this;
136 var viewmodel = me.getViewModel();
5bb10629
DC
137 if (user_tfa_type === 'oath') {
138 user_tfa_type = 'totp';
bfdfea50
DC
139 viewmodel.set('secret', '');
140 }
141
142 // if the user has no tfa, generate a secret for him
143 if (!user_tfa_type) {
144 me.getController().randomizeSecret();
5bb10629 145 }
bfdfea50 146
5bb10629 147 viewmodel.set('tfa_type', user_tfa_type || null);
24d2ed8c
WB
148 if (!realm_tfa_type) {
149 // There's no TFA enforced by the realm, everything works.
150 viewmodel.set('u2f_available', true);
151 viewmodel.set('tfa_required', false);
152 } else if (realm_tfa_type === 'oath') {
153 // The realm explicitly requires TOTP
5bb10629
DC
154 if (user_tfa_type !== 'totp' && user_tfa_type !== null) {
155 // user had a different tfa method, so
156 // we have to change back to the totp tab and
157 // generate a secret
bfdfea50 158 viewmodel.set('tfa_type', 'totp');
5bb10629
DC
159 me.getController().randomizeSecret();
160 }
24d2ed8c
WB
161 viewmodel.set('tfa_required', true);
162 viewmodel.set('u2f_available', false);
163 } else {
164 // The realm enforces some other TFA type (yubico)
165 me.close();
166 Ext.Msg.alert(
167 gettext('Error'),
168 Ext.String.format(
169 gettext("Custom 2nd factor configuration is not supported on realms with '{0}' TFA."),
f6710aac
TL
170 realm_tfa_type,
171 ),
24d2ed8c
WB
172 );
173 }
24d2ed8c
WB
174 },
175
176 controller: {
177 xclass: 'Ext.app.ViewController',
178 control: {
179 'field[qrupdate=true]': {
180 change: function() {
181 var me = this.getView();
182 me.updateQrCode();
f6710aac 183 },
24d2ed8c 184 },
0509c444
DC
185 'field': {
186 validitychange: function(field, valid) {
187 var me = this;
188 var viewModel = me.getViewModel();
189 var form = me.lookup('totp_form');
190 var challenge = me.lookup('challenge');
cea0f764
TL
191 var password = me.lookup('password');
192 viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
f6710aac 193 },
0509c444 194 },
24d2ed8c
WB
195 '#': {
196 show: function() {
197 var me = this.getView();
aa8d628f
TL
198 var viewmodel = this.getViewModel();
199
5bb10629
DC
200 var loadMaskContainer = me.down('#tfatabs');
201 Proxmox.Utils.API2Request({
202 url: '/access/users/' + encodeURIComponent(me.userid) + '/tfa',
203 waitMsgTarget: loadMaskContainer,
204 method: 'GET',
205 success: function(response, opts) {
206 var data = response.result.data;
207 me.afterLoading(data.realm, data.user);
208 },
209 failure: function(response, opts) {
49b54908
DC
210 me.close();
211 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
f6710aac 212 },
5bb10629
DC
213 });
214
7b8d68fe
TL
215 me.qrdiv = document.createElement('center');
216 me.qrcode = new QRCode(me.qrdiv, {
217 width: 256,
218 height: 256,
f6710aac 219 correctLevel: QRCode.CorrectLevel.M,
7b8d68fe 220 });
24d2ed8c 221 me.down('#qrbox').getEl().appendChild(me.qrdiv);
aa8d628f 222
24d2ed8c
WB
223 if (Proxmox.UserName === 'root@pam') {
224 me.lookup('password').setVisible(false);
225 me.lookup('password').setDisabled(true);
226 }
f6710aac 227 },
24d2ed8c
WB
228 },
229 '#tfatabs': {
230 tabchange: function(panel, newcard) {
231 var viewmodel = this.getViewModel();
232 viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel');
f6710aac
TL
233 },
234 },
24d2ed8c
WB
235 },
236
237 applySettings: function() {
238 var me = this;
b688436d 239 var values = me.lookup('totp_form').getValues();
24d2ed8c
WB
240 var params = {
241 userid: me.getView().userid,
242 action: 'new',
20ce59c3 243 key: 'v2-' + values.secret,
24d2ed8c
WB
244 config: PVE.Parser.printPropertyString({
245 type: 'oath',
246 digits: values.digits,
f6710aac 247 step: values.step,
24d2ed8c
WB
248 }),
249 // this is used to verify that the client generates the correct codes:
f6710aac 250 response: me.lookup('challenge').value,
24d2ed8c
WB
251 };
252
253 if (Proxmox.UserName !== 'root@pam') {
254 params.password = me.lookup('password').value;
255 }
256
257 Proxmox.Utils.API2Request({
258 url: '/api2/extjs/access/tfa',
259 params: params,
260 method: 'PUT',
261 waitMsgTarget: me.getView(),
262 success: function(response, opts) {
263 me.getView().close();
264 },
265 failure: function(response, opts) {
266 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
f6710aac 267 },
24d2ed8c
WB
268 });
269 },
270
271 deleteTFA: function() {
272 var me = this;
b688436d 273 var values = me.lookup('totp_form').getValues();
24d2ed8c
WB
274 var params = {
275 userid: me.getView().userid,
f6710aac 276 action: 'delete',
24d2ed8c
WB
277 };
278
279 if (Proxmox.UserName !== 'root@pam') {
280 params.password = me.lookup('password').value;
281 }
282
283 Proxmox.Utils.API2Request({
284 url: '/api2/extjs/access/tfa',
285 params: params,
286 method: 'PUT',
287 waitMsgTarget: me.getView(),
288 success: function(response, opts) {
289 me.getView().close();
290 },
291 failure: function(response, opts) {
292 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
f6710aac 293 },
24d2ed8c
WB
294 });
295 },
296
297 randomizeSecret: function() {
298 var me = this;
7cfd876e 299 var rnd = new Uint8Array(32);
24d2ed8c
WB
300 window.crypto.getRandomValues(rnd);
301 var data = '';
302 rnd.forEach(function(b) {
72bff50c 303 // secret must be base32, so just use the first 5 bits
24d2ed8c
WB
304 b = b & 0x1f;
305 if (b < 26) {
306 // A..Z
307 data += String.fromCharCode(b + 0x41);
308 } else {
309 // 2..7
310 data += String.fromCharCode(b-26 + 0x32);
311 }
312 });
bfdfea50 313 me.getViewModel().set('secret', data);
24d2ed8c
WB
314 },
315
316 startU2FRegistration: function() {
317 var me = this;
318
319 var params = {
320 userid: me.getView().userid,
f6710aac 321 action: 'new',
24d2ed8c
WB
322 };
323
324 if (Proxmox.UserName !== 'root@pam') {
325 params.password = me.lookup('password').value;
326 }
327
328 Proxmox.Utils.API2Request({
329 url: '/api2/extjs/access/tfa',
330 params: params,
331 method: 'PUT',
332 waitMsgTarget: me.getView(),
333 success: function(response) {
334 me.getView().doU2FChallenge(response);
335 },
336 failure: function(response, opts) {
337 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
f6710aac 338 },
24d2ed8c 339 });
f6710aac 340 },
24d2ed8c
WB
341 },
342
343 items: [
344 {
345 xtype: 'tabpanel',
346 itemId: 'tfatabs',
1b16f713 347 reference: 'tfatabs',
24d2ed8c 348 border: false,
bfdfea50
DC
349 bind: {
350 activeTab: '{selectedTab}',
351 },
24d2ed8c
WB
352 items: [
353 {
354 xtype: 'panel',
355 title: 'TOTP',
356 itemId: 'totp-panel',
5bb10629 357 reference: 'totp_panel',
ac3daab8 358 tfa_type: 'totp',
24d2ed8c 359 border: false,
b0184fc4 360 bind: {
f6710aac 361 disabled: '{!canSetupTOTP}',
b0184fc4 362 },
24d2ed8c
WB
363 layout: {
364 type: 'vbox',
f6710aac 365 align: 'stretch',
24d2ed8c
WB
366 },
367 items: [
368 {
369 xtype: 'form',
370 layout: 'anchor',
371 border: false,
b688436d 372 reference: 'totp_form',
24d2ed8c 373 fieldDefaults: {
24d2ed8c 374 anchor: '100%',
f6710aac 375 padding: '0 5',
24d2ed8c
WB
376 },
377 items: [
2f212470
TL
378 {
379 xtype: 'displayfield',
2f212470 380 fieldLabel: gettext('User name'),
1011b569 381 renderer: Ext.String.htmlEncode,
2f212470 382 cbind: {
f6710aac
TL
383 value: '{userid}',
384 },
2f212470 385 },
24d2ed8c 386 {
a8740316
TL
387 layout: 'hbox',
388 border: false,
9390af7b 389 padding: '0 0 5 0',
a8740316
TL
390 items: [{
391 xtype: 'textfield',
392 fieldLabel: gettext('Secret'),
28ec45b2 393 emptyText: gettext('Unchanged'),
a8740316
TL
394 name: 'secret',
395 reference: 'tfa_secret',
0bc11f81
TL
396 regex: /^[A-Z2-7=]+$/,
397 regexText: 'Must be base32 [A-Z2-7=]',
7bc70192 398 maskRe: /[A-Z2-7=]/,
a8740316 399 qrupdate: true,
bfdfea50
DC
400 bind: {
401 value: "{secret}",
402 },
f6710aac 403 flex: 4,
24d2ed8c 404 },
a8740316
TL
405 {
406 xtype: 'button',
407 text: gettext('Randomize'),
408 reference: 'randomize_button',
409 handler: 'randomizeSecret',
f6710aac
TL
410 flex: 1,
411 }],
24d2ed8c
WB
412 },
413 {
414 xtype: 'numberfield',
415 fieldLabel: gettext('Time period'),
416 name: 'step',
2c2197a3
TL
417 // Google Authenticator ignores this and generates bogus data
418 hidden: true,
24d2ed8c
WB
419 value: 30,
420 minValue: 10,
f6710aac 421 qrupdate: true,
24d2ed8c
WB
422 },
423 {
424 xtype: 'numberfield',
425 fieldLabel: gettext('Digits'),
426 name: 'digits',
427 value: 6,
2c2197a3
TL
428 // Google Authenticator ignores this and generates bogus data
429 hidden: true,
24d2ed8c
WB
430 minValue: 6,
431 maxValue: 8,
f6710aac 432 qrupdate: true,
24d2ed8c
WB
433 },
434 {
435 xtype: 'textfield',
436 fieldLabel: gettext('Issuer Name'),
437 name: 'issuer',
438 value: 'Proxmox Web UI',
f6710aac
TL
439 qrupdate: true,
440 },
441 ],
24d2ed8c
WB
442 },
443 {
444 xtype: 'box',
445 itemId: 'qrbox',
446 visible: false, // will be enabled when generating a qr code
bfdfea50
DC
447 bind: {
448 visible: '{!secretEmpty}',
449 },
24d2ed8c
WB
450 style: {
451 'background-color': 'white',
452 padding: '5px',
453 width: '266px',
f6710aac
TL
454 height: '266px',
455 },
24d2ed8c
WB
456 },
457 {
458 xtype: 'textfield',
0abfdbfa 459 fieldLabel: gettext('Verification Code'),
0509c444 460 allowBlank: false,
24d2ed8c 461 reference: 'challenge',
bfdfea50
DC
462 bind: {
463 disabled: '{!showTOTPVerifiction}',
464 visible: '{showTOTPVerifiction}',
465 },
24d2ed8c 466 padding: '0 5',
f6710aac
TL
467 emptyText: gettext('Scan QR code and enter TOTP auth. code to verify'),
468 },
469 ],
24d2ed8c
WB
470 },
471 {
472 title: 'U2F',
473 itemId: 'u2f-panel',
b688436d 474 reference: 'u2f_panel',
ac3daab8 475 tfa_type: 'u2f',
24d2ed8c
WB
476 border: false,
477 padding: '5 5',
478 layout: {
479 type: 'vbox',
f6710aac 480 align: 'middle',
24d2ed8c
WB
481 },
482 bind: {
f6710aac 483 disabled: '{!canSetupU2F}',
24d2ed8c
WB
484 },
485 items: [
486 {
487 xtype: 'label',
488 width: 500,
f6710aac
TL
489 text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.'),
490 },
491 ],
492 },
493 ],
24d2ed8c
WB
494 },
495 {
496 xtype: 'textfield',
497 inputType: 'password',
498 fieldLabel: gettext('Password'),
499 minLength: 5,
500 reference: 'password',
cea0f764
TL
501 allowBlank: false,
502 validateBlank: true,
503 padding: '0 0 5 5',
f6710aac
TL
504 emptyText: gettext('verify current password'),
505 },
24d2ed8c
WB
506 ],
507
508 buttons: [
db2af549 509 {
f6710aac 510 xtype: 'proxmoxHelpButton',
db2af549
TL
511 },
512 '->',
24d2ed8c
WB
513 {
514 text: gettext('Apply'),
515 handler: 'applySettings',
516 bind: {
517 hidden: '{!in_totp_tab}',
f6710aac
TL
518 disabled: '{!valid}',
519 },
24d2ed8c
WB
520 },
521 {
522 xtype: 'button',
523 text: gettext('Register U2F Device'),
524 handler: 'startU2FRegistration',
525 bind: {
ac3daab8 526 hidden: '{in_totp_tab}',
f6710aac
TL
527 disabled: '{tfa_type}',
528 },
24d2ed8c
WB
529 },
530 {
531 text: gettext('Delete'),
b688436d 532 reference: 'delete_button',
1bfa0588 533 disabled: true,
24d2ed8c
WB
534 handler: 'deleteTFA',
535 bind: {
f6710aac
TL
536 disabled: '{!canDeleteTFA}',
537 },
538 },
24d2ed8c
WB
539 ],
540
541 initComponent: function() {
542 var me = this;
543
5bb10629
DC
544 if (!me.userid) {
545 throw "no userid given";
546 }
24d2ed8c
WB
547
548 me.callParent();
db2af549
TL
549
550 Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth');
f6710aac 551 },
24d2ed8c 552});