]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/TFAEdit.js
ui: eslint: fix trailing comma and comma related whitespaces errors
[pve-manager.git] / www / manager6 / dc / TFAEdit.js
1 /*global u2f,QRCode,Uint8Array*/
2 Ext.define('PVE.window.TFAEdit', {
3 extend: 'Ext.window.Window',
4 mixins: ['Proxmox.Mixin.CBind'],
5
6 onlineHelp: 'pveum_tfa_auth', // fake to ensure this gets a link target
7
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',
17 align: 'stretch',
18 },
19
20 updateQrCode: function() {
21 var me = this;
22 var values = me.lookup('totp_form').getValues();
23 var algorithm = values.algorithm;
24 if (!algorithm) {
25 algorithm = 'SHA1';
26 }
27
28 me.qrcode.makeCode(
29 'otpauth://totp/' +
30 encodeURIComponent(values.issuer) +
31 ':' +
32 encodeURIComponent(values.userid) +
33 '?secret=' + values.secret +
34 '&period=' + values.step +
35 '&digits=' + values.digits +
36 '&algorithm=' + algorithm +
37 '&issuer=' + encodeURIComponent(values.issuer),
38 );
39
40 me.lookup('challenge').setVisible(true);
41 me.down('#qrbox').setVisible(true);
42 },
43
44 showError: function(error) {
45 Ext.Msg.alert(
46 gettext('Error'),
47 PVE.Utils.render_u2f_error(error),
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'),
59 buttons: [],
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',
78 response: JSON.stringify(data),
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.'),
92 buttons: Ext.Msg.OK,
93 });
94 },
95 failure: function(response, opts) {
96 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
97 },
98 });
99 },
100
101 viewModel: {
102 data: {
103 in_totp_tab: true,
104 tfa_required: false,
105 tfa_type: null, // dependencies of formulas should not be undefined
106 valid: false,
107 u2f_available: true,
108 secret: "",
109 },
110 formulas: {
111 showTOTPVerifiction: function(get) {
112 return get('secret').length > 0 && get('canSetupTOTP');
113 },
114 canDeleteTFA: function(get) {
115 return (get('tfa_type') !== null && !get('tfa_required'));
116 },
117 canSetupTOTP: function(get) {
118 var tfa = get('tfa_type');
119 return (tfa === null || tfa === 'totp' || tfa === 1);
120 },
121 canSetupU2F: function(get) {
122 var tfa = get('tfa_type');
123 return (get('u2f_available') && (tfa === null || tfa === 'u2f' || tfa === 1));
124 },
125 secretEmpty: function(get) {
126 return get('secret').length === 0;
127 },
128 selectedTab: function(get) {
129 return (get('tfa_type') || 'totp') + '-panel';
130 },
131 },
132 },
133
134 afterLoading: function(realm_tfa_type, user_tfa_type) {
135 var me = this;
136 var viewmodel = me.getViewModel();
137 if (user_tfa_type === 'oath') {
138 user_tfa_type = 'totp';
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();
145 }
146
147 viewmodel.set('tfa_type', user_tfa_type || null);
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
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
158 viewmodel.set('tfa_type', 'totp');
159 me.getController().randomizeSecret();
160 }
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."),
170 realm_tfa_type,
171 ),
172 );
173 }
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();
183 },
184 },
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');
191 var password = me.lookup('password');
192 viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
193 },
194 },
195 '#': {
196 show: function() {
197 var me = this.getView();
198 var viewmodel = this.getViewModel();
199
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) {
210 me.close();
211 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
212 },
213 });
214
215 me.qrdiv = document.createElement('center');
216 me.qrcode = new QRCode(me.qrdiv, {
217 width: 256,
218 height: 256,
219 correctLevel: QRCode.CorrectLevel.M,
220 });
221 me.down('#qrbox').getEl().appendChild(me.qrdiv);
222
223 if (Proxmox.UserName === 'root@pam') {
224 me.lookup('password').setVisible(false);
225 me.lookup('password').setDisabled(true);
226 }
227 },
228 },
229 '#tfatabs': {
230 tabchange: function(panel, newcard) {
231 var viewmodel = this.getViewModel();
232 viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel');
233 },
234 },
235 },
236
237 applySettings: function() {
238 var me = this;
239 var values = me.lookup('totp_form').getValues();
240 var params = {
241 userid: me.getView().userid,
242 action: 'new',
243 key: 'v2-' + values.secret,
244 config: PVE.Parser.printPropertyString({
245 type: 'oath',
246 digits: values.digits,
247 step: values.step,
248 }),
249 // this is used to verify that the client generates the correct codes:
250 response: me.lookup('challenge').value,
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);
267 },
268 });
269 },
270
271 deleteTFA: function() {
272 var me = this;
273 var values = me.lookup('totp_form').getValues();
274 var params = {
275 userid: me.getView().userid,
276 action: 'delete',
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);
293 },
294 });
295 },
296
297 randomizeSecret: function() {
298 var me = this;
299 var rnd = new Uint8Array(32);
300 window.crypto.getRandomValues(rnd);
301 var data = '';
302 rnd.forEach(function(b) {
303 // secret must be base32, so just use the first 5 bits
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 });
313 me.getViewModel().set('secret', data);
314 },
315
316 startU2FRegistration: function() {
317 var me = this;
318
319 var params = {
320 userid: me.getView().userid,
321 action: 'new',
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);
338 },
339 });
340 },
341 },
342
343 items: [
344 {
345 xtype: 'tabpanel',
346 itemId: 'tfatabs',
347 reference: 'tfatabs',
348 border: false,
349 bind: {
350 activeTab: '{selectedTab}',
351 },
352 items: [
353 {
354 xtype: 'panel',
355 title: 'TOTP',
356 itemId: 'totp-panel',
357 reference: 'totp_panel',
358 tfa_type: 'totp',
359 border: false,
360 bind: {
361 disabled: '{!canSetupTOTP}',
362 },
363 layout: {
364 type: 'vbox',
365 align: 'stretch',
366 },
367 items: [
368 {
369 xtype: 'form',
370 layout: 'anchor',
371 border: false,
372 reference: 'totp_form',
373 fieldDefaults: {
374 anchor: '100%',
375 padding: '0 5',
376 },
377 items: [
378 {
379 xtype: 'displayfield',
380 fieldLabel: gettext('User name'),
381 renderer: Ext.String.htmlEncode,
382 cbind: {
383 value: '{userid}',
384 },
385 },
386 {
387 layout: 'hbox',
388 border: false,
389 padding: '0 0 5 0',
390 items: [{
391 xtype: 'textfield',
392 fieldLabel: gettext('Secret'),
393 emptyText: gettext('Unchanged'),
394 name: 'secret',
395 reference: 'tfa_secret',
396 regex: /^[A-Z2-7=]+$/,
397 regexText: 'Must be base32 [A-Z2-7=]',
398 maskRe: /[A-Z2-7=]/,
399 qrupdate: true,
400 bind: {
401 value: "{secret}",
402 },
403 flex: 4,
404 },
405 {
406 xtype: 'button',
407 text: gettext('Randomize'),
408 reference: 'randomize_button',
409 handler: 'randomizeSecret',
410 flex: 1,
411 }],
412 },
413 {
414 xtype: 'numberfield',
415 fieldLabel: gettext('Time period'),
416 name: 'step',
417 // Google Authenticator ignores this and generates bogus data
418 hidden: true,
419 value: 30,
420 minValue: 10,
421 qrupdate: true,
422 },
423 {
424 xtype: 'numberfield',
425 fieldLabel: gettext('Digits'),
426 name: 'digits',
427 value: 6,
428 // Google Authenticator ignores this and generates bogus data
429 hidden: true,
430 minValue: 6,
431 maxValue: 8,
432 qrupdate: true,
433 },
434 {
435 xtype: 'textfield',
436 fieldLabel: gettext('Issuer Name'),
437 name: 'issuer',
438 value: 'Proxmox Web UI',
439 qrupdate: true,
440 },
441 ],
442 },
443 {
444 xtype: 'box',
445 itemId: 'qrbox',
446 visible: false, // will be enabled when generating a qr code
447 bind: {
448 visible: '{!secretEmpty}',
449 },
450 style: {
451 'background-color': 'white',
452 padding: '5px',
453 width: '266px',
454 height: '266px',
455 },
456 },
457 {
458 xtype: 'textfield',
459 fieldLabel: gettext('Verification Code'),
460 allowBlank: false,
461 reference: 'challenge',
462 bind: {
463 disabled: '{!showTOTPVerifiction}',
464 visible: '{showTOTPVerifiction}',
465 },
466 padding: '0 5',
467 emptyText: gettext('Scan QR code and enter TOTP auth. code to verify'),
468 },
469 ],
470 },
471 {
472 title: 'U2F',
473 itemId: 'u2f-panel',
474 reference: 'u2f_panel',
475 tfa_type: 'u2f',
476 border: false,
477 padding: '5 5',
478 layout: {
479 type: 'vbox',
480 align: 'middle',
481 },
482 bind: {
483 disabled: '{!canSetupU2F}',
484 },
485 items: [
486 {
487 xtype: 'label',
488 width: 500,
489 text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.'),
490 },
491 ],
492 },
493 ],
494 },
495 {
496 xtype: 'textfield',
497 inputType: 'password',
498 fieldLabel: gettext('Password'),
499 minLength: 5,
500 reference: 'password',
501 allowBlank: false,
502 validateBlank: true,
503 padding: '0 0 5 5',
504 emptyText: gettext('verify current password'),
505 },
506 ],
507
508 buttons: [
509 {
510 xtype: 'proxmoxHelpButton',
511 },
512 '->',
513 {
514 text: gettext('Apply'),
515 handler: 'applySettings',
516 bind: {
517 hidden: '{!in_totp_tab}',
518 disabled: '{!valid}',
519 },
520 },
521 {
522 xtype: 'button',
523 text: gettext('Register U2F Device'),
524 handler: 'startU2FRegistration',
525 bind: {
526 hidden: '{in_totp_tab}',
527 disabled: '{tfa_type}',
528 },
529 },
530 {
531 text: gettext('Delete'),
532 reference: 'delete_button',
533 disabled: true,
534 handler: 'deleteTFA',
535 bind: {
536 disabled: '{!canDeleteTFA}',
537 },
538 },
539 ],
540
541 initComponent: function() {
542 var me = this;
543
544 if (!me.userid) {
545 throw "no userid given";
546 }
547
548 me.callParent();
549
550 Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth');
551 },
552 });