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