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