]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/TFAEdit.js
www: add TFA view to config
[pve-manager.git] / www / manager6 / dc / TFAEdit.js
1 /*global u2f,QRCode*/
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(me.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 Proxmox.Utils.render_u2f_error(error),
48 );
49 },
50
51 doU2FChallenge: function(res) {
52 let me = this;
53
54 let challenge = res.result.data;
55 me.lookup('password').setDisabled(true);
56 let 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(challenge.appId, [challenge], [], function(response) {
63 msg.close();
64 if (response.errorCode) {
65 me.showError(response.errorCode);
66 } else {
67 me.respondToU2FChallenge(response);
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 this.getView().updateQrCode();
182 },
183 },
184 'field': {
185 validitychange: function(field, valid) {
186 var me = this;
187 var viewModel = me.getViewModel();
188 var form = me.lookup('totp_form');
189 var challenge = me.lookup('challenge');
190 var password = me.lookup('password');
191 viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
192 },
193 },
194 '#': {
195 show: function() {
196 let view = this.getView();
197
198 Proxmox.Utils.API2Request({
199 url: '/access/users/' + encodeURIComponent(view.userid) + '/tfa',
200 waitMsgTarget: view.down('#tfatabs'),
201 method: 'GET',
202 success: function(response, opts) {
203 let data = response.result.data;
204 view.afterLoading(data.realm, data.user);
205 },
206 failure: function(response, opts) {
207 view.close();
208 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
209 },
210 });
211
212 view.qrdiv = document.createElement('center');
213 view.qrcode = new QRCode(view.qrdiv, {
214 width: 256,
215 height: 256,
216 correctLevel: QRCode.CorrectLevel.M,
217 });
218 view.down('#qrbox').getEl().appendChild(view.qrdiv);
219
220 if (Proxmox.UserName === 'root@pam') {
221 view.lookup('password').setVisible(false);
222 view.lookup('password').setDisabled(true);
223 }
224 },
225 },
226 '#tfatabs': {
227 tabchange: function(panel, newcard) {
228 this.getViewModel().set('in_totp_tab', newcard.itemId === 'totp-panel');
229 },
230 },
231 },
232
233 applySettings: function() {
234 let me = this;
235 let values = me.lookup('totp_form').getValues();
236 let params = {
237 userid: me.getView().userid,
238 action: 'new',
239 key: 'v2-' + values.secret,
240 config: PVE.Parser.printPropertyString({
241 type: 'oath',
242 digits: values.digits,
243 step: values.step,
244 }),
245 // this is used to verify that the client generates the correct codes:
246 response: me.lookup('challenge').value,
247 };
248
249 if (Proxmox.UserName !== 'root@pam') {
250 params.password = me.lookup('password').value;
251 }
252
253 Proxmox.Utils.API2Request({
254 url: '/api2/extjs/access/tfa',
255 params: params,
256 method: 'PUT',
257 waitMsgTarget: me.getView(),
258 success: function(response, opts) {
259 me.getView().close();
260 },
261 failure: function(response, opts) {
262 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
263 },
264 });
265 },
266
267 deleteTFA: function() {
268 let me = this;
269 let params = {
270 userid: me.getView().userid,
271 action: 'delete',
272 };
273
274 if (Proxmox.UserName !== 'root@pam') {
275 params.password = me.lookup('password').value;
276 }
277
278 Proxmox.Utils.API2Request({
279 url: '/api2/extjs/access/tfa',
280 params: params,
281 method: 'PUT',
282 waitMsgTarget: me.getView(),
283 success: function(response, opts) {
284 me.getView().close();
285 },
286 failure: function(response, opts) {
287 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
288 },
289 });
290 },
291
292 randomizeSecret: function() {
293 let me = this;
294 let rnd = new Uint8Array(32);
295 window.crypto.getRandomValues(rnd);
296 let data = '';
297 rnd.forEach(function(b) {
298 // secret must be base32, so just use the first 5 bits
299 b = b & 0x1f;
300 if (b < 26) {
301 data += String.fromCharCode(b + 0x41); // A..Z
302 } else {
303 data += String.fromCharCode(b-26 + 0x32); // 2..7
304 }
305 });
306 me.getViewModel().set('secret', data);
307 },
308
309 startU2FRegistration: function() {
310 let me = this;
311
312 let params = {
313 userid: me.getView().userid,
314 action: 'new',
315 };
316
317 if (Proxmox.UserName !== 'root@pam') {
318 params.password = me.lookup('password').value;
319 }
320
321 Proxmox.Utils.API2Request({
322 url: '/api2/extjs/access/tfa',
323 params: params,
324 method: 'PUT',
325 waitMsgTarget: me.getView(),
326 success: function(response) {
327 me.getView().doU2FChallenge(response);
328 },
329 failure: function(response, opts) {
330 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
331 },
332 });
333 },
334 },
335
336 items: [
337 {
338 xtype: 'tabpanel',
339 itemId: 'tfatabs',
340 reference: 'tfatabs',
341 border: false,
342 bind: {
343 activeTab: '{selectedTab}',
344 },
345 items: [
346 {
347 xtype: 'panel',
348 title: 'TOTP',
349 itemId: 'totp-panel',
350 reference: 'totp_panel',
351 tfa_type: 'totp',
352 border: false,
353 bind: {
354 disabled: '{!canSetupTOTP}',
355 },
356 layout: {
357 type: 'vbox',
358 align: 'stretch',
359 },
360 items: [
361 {
362 xtype: 'form',
363 layout: 'anchor',
364 border: false,
365 reference: 'totp_form',
366 fieldDefaults: {
367 anchor: '100%',
368 padding: '0 5',
369 },
370 items: [
371 {
372 xtype: 'displayfield',
373 fieldLabel: gettext('User name'),
374 renderer: Ext.String.htmlEncode,
375 cbind: {
376 value: '{userid}',
377 },
378 },
379 {
380 layout: 'hbox',
381 border: false,
382 padding: '0 0 5 0',
383 items: [{
384 xtype: 'textfield',
385 fieldLabel: gettext('Secret'),
386 emptyText: gettext('Unchanged'),
387 name: 'secret',
388 reference: 'tfa_secret',
389 regex: /^[A-Z2-7=]+$/,
390 regexText: 'Must be base32 [A-Z2-7=]',
391 maskRe: /[A-Z2-7=]/,
392 qrupdate: true,
393 bind: {
394 value: "{secret}",
395 },
396 flex: 4,
397 },
398 {
399 xtype: 'button',
400 text: gettext('Randomize'),
401 reference: 'randomize_button',
402 handler: 'randomizeSecret',
403 flex: 1,
404 }],
405 },
406 {
407 xtype: 'numberfield',
408 fieldLabel: gettext('Time period'),
409 name: 'step',
410 // Google Authenticator ignores this and generates bogus data
411 hidden: true,
412 value: 30,
413 minValue: 10,
414 qrupdate: true,
415 },
416 {
417 xtype: 'numberfield',
418 fieldLabel: gettext('Digits'),
419 name: 'digits',
420 value: 6,
421 // Google Authenticator ignores this and generates bogus data
422 hidden: true,
423 minValue: 6,
424 maxValue: 8,
425 qrupdate: true,
426 },
427 {
428 xtype: 'textfield',
429 fieldLabel: gettext('Issuer Name'),
430 name: 'issuer',
431 value: 'Proxmox Web UI',
432 qrupdate: true,
433 },
434 ],
435 },
436 {
437 xtype: 'box',
438 itemId: 'qrbox',
439 visible: false, // will be enabled when generating a qr code
440 bind: {
441 visible: '{!secretEmpty}',
442 },
443 style: {
444 'background-color': 'white',
445 padding: '5px',
446 width: '266px',
447 height: '266px',
448 },
449 },
450 {
451 xtype: 'textfield',
452 fieldLabel: gettext('Verification Code'),
453 allowBlank: false,
454 reference: 'challenge',
455 bind: {
456 disabled: '{!showTOTPVerifiction}',
457 visible: '{showTOTPVerifiction}',
458 },
459 padding: '0 5',
460 emptyText: gettext('Scan QR code and enter TOTP auth. code to verify'),
461 },
462 ],
463 },
464 {
465 title: 'U2F',
466 itemId: 'u2f-panel',
467 reference: 'u2f_panel',
468 tfa_type: 'u2f',
469 border: false,
470 padding: '5 5',
471 layout: {
472 type: 'vbox',
473 align: 'middle',
474 },
475 bind: {
476 disabled: '{!canSetupU2F}',
477 },
478 items: [
479 {
480 xtype: 'label',
481 width: 500,
482 text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.'),
483 },
484 ],
485 },
486 ],
487 },
488 {
489 xtype: 'textfield',
490 inputType: 'password',
491 fieldLabel: gettext('Password'),
492 minLength: 5,
493 reference: 'password',
494 allowBlank: false,
495 validateBlank: true,
496 padding: '0 0 5 5',
497 emptyText: gettext('verify current password'),
498 },
499 ],
500
501 buttons: [
502 {
503 xtype: 'proxmoxHelpButton',
504 },
505 '->',
506 {
507 text: gettext('Apply'),
508 handler: 'applySettings',
509 bind: {
510 hidden: '{!in_totp_tab}',
511 disabled: '{!valid}',
512 },
513 },
514 {
515 xtype: 'button',
516 text: gettext('Register U2F Device'),
517 handler: 'startU2FRegistration',
518 bind: {
519 hidden: '{in_totp_tab}',
520 disabled: '{tfa_type}',
521 },
522 },
523 {
524 text: gettext('Delete'),
525 reference: 'delete_button',
526 disabled: true,
527 handler: 'deleteTFA',
528 bind: {
529 disabled: '{!canDeleteTFA}',
530 },
531 },
532 ],
533
534 initComponent: function() {
535 var me = this;
536
537 if (!me.userid) {
538 throw "no userid given";
539 }
540
541 me.callParent();
542
543 Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth');
544 },
545 });