]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/TFAEdit.js
7d19127dcd520001aea4e1cd8491ef0fc387ffb9
[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 },
107 formulas: {
108 canDeleteTFA: function(get) {
109 return (get('tfa_type') !== null && !get('tfa_required'));
110 },
111 canSetupTOTP: function(get) {
112 var tfa = get('tfa_type');
113 return (tfa === null || tfa === 'totp' || tfa === 1);
114 },
115 canSetupU2F: function(get) {
116 var tfa = get('tfa_type');
117 return (get('u2f_available') && (tfa === null || tfa === 'u2f' || tfa === 1));
118 }
119 }
120 },
121
122 afterLoading: function(realm_tfa_type, user_tfa_type) {
123 var me = this;
124 var viewmodel = me.getViewModel();
125 if (user_tfa_type === 'oath') {
126 user_tfa_type = 'totp';
127 }
128 viewmodel.set('tfa_type', user_tfa_type || null);
129 if (!realm_tfa_type) {
130 // There's no TFA enforced by the realm, everything works.
131 viewmodel.set('u2f_available', true);
132 viewmodel.set('tfa_required', false);
133 } else if (realm_tfa_type === 'oath') {
134 // The realm explicitly requires TOTP
135 if (user_tfa_type !== 'totp' && user_tfa_type !== null) {
136 // user had a different tfa method, so
137 // we have to change back to the totp tab and
138 // generate a secret
139 viewmodel.set('tfa_type', null);
140 me.lookup('tfatabs').setActiveTab(me.lookup('totp_panel'));
141 me.getController().randomizeSecret();
142 }
143 viewmodel.set('tfa_required', true);
144 viewmodel.set('u2f_available', false);
145 } else {
146 // The realm enforces some other TFA type (yubico)
147 me.close();
148 Ext.Msg.alert(
149 gettext('Error'),
150 Ext.String.format(
151 gettext("Custom 2nd factor configuration is not supported on realms with '{0}' TFA."),
152 realm_tfa_type
153 )
154 );
155 }
156 },
157
158 controller: {
159 xclass: 'Ext.app.ViewController',
160 control: {
161 'field[qrupdate=true]': {
162 change: function() {
163 var me = this.getView();
164 me.updateQrCode();
165 }
166 },
167 'field': {
168 validitychange: function(field, valid) {
169 var me = this;
170 var viewModel = me.getViewModel();
171 var form = me.lookup('totp_form');
172 var challenge = me.lookup('challenge');
173 var password = me.lookup('password');
174 viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
175 }
176 },
177 '#': {
178 show: function() {
179 var me = this.getView();
180 var viewmodel = this.getViewModel();
181
182 var loadMaskContainer = me.down('#tfatabs');
183 Proxmox.Utils.API2Request({
184 url: '/access/users/' + encodeURIComponent(me.userid) + '/tfa',
185 waitMsgTarget: loadMaskContainer,
186 method: 'GET',
187 success: function(response, opts) {
188 var data = response.result.data;
189 me.afterLoading(data.realm, data.user);
190 },
191 failure: function(response, opts) {
192 Proxmox.Utils.setErrorMask(loadMaskContainer, response.htmlStatus);
193 }
194 });
195
196 me.qrdiv = document.createElement('center');
197 me.qrcode = new QRCode(me.qrdiv, {
198 width: 256,
199 height: 256,
200 correctLevel: QRCode.CorrectLevel.M
201 });
202 me.down('#qrbox').getEl().appendChild(me.qrdiv);
203
204 viewmodel.set('tfa_type', me.tfa_type || null);
205 if (!me.tfa_type) {
206 this.randomizeSecret();
207 } else {
208 me.down('#qrbox').setVisible(false);
209 me.lookup('challenge').setVisible(false);
210 if (me.tfa_type === 'u2f') {
211 var u2f_panel = me.lookup('u2f_panel');
212 me.lookup('tfatabs').setActiveTab(u2f_panel);
213 }
214 }
215
216 if (Proxmox.UserName === 'root@pam') {
217 me.lookup('password').setVisible(false);
218 me.lookup('password').setDisabled(true);
219 }
220 }
221 },
222 '#tfatabs': {
223 tabchange: function(panel, newcard) {
224 var viewmodel = this.getViewModel();
225 viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel');
226 }
227 }
228 },
229
230 applySettings: function() {
231 var me = this;
232 var values = me.lookup('totp_form').getValues();
233 var params = {
234 userid: me.getView().userid,
235 action: 'new',
236 key: 'v2-' + values.secret,
237 config: PVE.Parser.printPropertyString({
238 type: 'oath',
239 digits: values.digits,
240 step: values.step
241 }),
242 // this is used to verify that the client generates the correct codes:
243 response: me.lookup('challenge').value
244 };
245
246 if (Proxmox.UserName !== 'root@pam') {
247 params.password = me.lookup('password').value;
248 }
249
250 Proxmox.Utils.API2Request({
251 url: '/api2/extjs/access/tfa',
252 params: params,
253 method: 'PUT',
254 waitMsgTarget: me.getView(),
255 success: function(response, opts) {
256 me.getView().close();
257 },
258 failure: function(response, opts) {
259 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
260 }
261 });
262 },
263
264 deleteTFA: function() {
265 var me = this;
266 var values = me.lookup('totp_form').getValues();
267 var params = {
268 userid: me.getView().userid,
269 action: 'delete'
270 };
271
272 if (Proxmox.UserName !== 'root@pam') {
273 params.password = me.lookup('password').value;
274 }
275
276 Proxmox.Utils.API2Request({
277 url: '/api2/extjs/access/tfa',
278 params: params,
279 method: 'PUT',
280 waitMsgTarget: me.getView(),
281 success: function(response, opts) {
282 me.getView().close();
283 },
284 failure: function(response, opts) {
285 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
286 }
287 });
288 },
289
290 randomizeSecret: function() {
291 var me = this;
292 var rnd = new Uint8Array(16);
293 window.crypto.getRandomValues(rnd);
294 var data = '';
295 rnd.forEach(function(b) {
296 // secret must be base32, so just use the first 5 bits
297 b = b & 0x1f;
298 if (b < 26) {
299 // A..Z
300 data += String.fromCharCode(b + 0x41);
301 } else {
302 // 2..7
303 data += String.fromCharCode(b-26 + 0x32);
304 }
305 });
306 me.lookup('tfa_secret').setValue(data);
307 },
308
309 startU2FRegistration: function() {
310 var me = this;
311
312 var 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 items: [
343 {
344 xtype: 'panel',
345 title: 'TOTP',
346 itemId: 'totp-panel',
347 reference: 'totp_panel',
348 tfa_type: 'totp',
349 border: false,
350 bind: {
351 disabled: '{!canSetupTOTP}'
352 },
353 layout: {
354 type: 'vbox',
355 align: 'stretch'
356 },
357 items: [
358 {
359 xtype: 'form',
360 layout: 'anchor',
361 border: false,
362 reference: 'totp_form',
363 fieldDefaults: {
364 anchor: '100%',
365 padding: '0 5'
366 },
367 items: [
368 {
369 xtype: 'displayfield',
370 fieldLabel: gettext('User name'),
371 cbind: {
372 value: '{userid}'
373 }
374 },
375 {
376 layout: 'hbox',
377 border: false,
378 padding: '0 0 5 0',
379 items: [{
380 xtype: 'textfield',
381 fieldLabel: gettext('Secret'),
382 emptyText: gettext('Unchanged'),
383 name: 'secret',
384 reference: 'tfa_secret',
385 regex: /^[A-Z2-7=]+$/,
386 regexText: 'Must be base32 [A-Z2-7=]',
387 maskRe: /[A-Z2-7=]/,
388 qrupdate: true,
389 flex: 4
390 },
391 {
392 xtype: 'button',
393 text: gettext('Randomize'),
394 reference: 'randomize_button',
395 handler: 'randomizeSecret',
396 flex: 1
397 }]
398 },
399 {
400 xtype: 'numberfield',
401 fieldLabel: gettext('Time period'),
402 name: 'step',
403 // Google Authenticator ignores this and generates bogus data
404 hidden: true,
405 value: 30,
406 minValue: 10,
407 qrupdate: true
408 },
409 {
410 xtype: 'numberfield',
411 fieldLabel: gettext('Digits'),
412 name: 'digits',
413 value: 6,
414 // Google Authenticator ignores this and generates bogus data
415 hidden: true,
416 minValue: 6,
417 maxValue: 8,
418 qrupdate: true
419 },
420 {
421 xtype: 'textfield',
422 fieldLabel: gettext('Issuer Name'),
423 name: 'issuer',
424 value: 'Proxmox Web UI',
425 qrupdate: true
426 }
427 ]
428 },
429 {
430 xtype: 'box',
431 itemId: 'qrbox',
432 visible: false, // will be enabled when generating a qr code
433 style: {
434 'background-color': 'white',
435 padding: '5px',
436 width: '266px',
437 height: '266px'
438 }
439 },
440 {
441 xtype: 'textfield',
442 fieldLabel: gettext('Verification Code'),
443 allowBlank: false,
444 reference: 'challenge',
445 padding: '0 5',
446 emptyText: gettext('Scan QR code and enter TOTP auth. code to verify')
447 }
448 ]
449 },
450 {
451 title: 'U2F',
452 itemId: 'u2f-panel',
453 reference: 'u2f_panel',
454 tfa_type: 'u2f',
455 border: false,
456 padding: '5 5',
457 layout: {
458 type: 'vbox',
459 align: 'middle'
460 },
461 bind: {
462 disabled: '{!canSetupU2F}'
463 },
464 items: [
465 {
466 xtype: 'label',
467 width: 500,
468 text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.')
469 }
470 ]
471 }
472 ]
473 },
474 {
475 xtype: 'textfield',
476 inputType: 'password',
477 fieldLabel: gettext('Password'),
478 minLength: 5,
479 reference: 'password',
480 allowBlank: false,
481 validateBlank: true,
482 padding: '0 0 5 5',
483 emptyText: gettext('verify current password')
484 }
485 ],
486
487 buttons: [
488 {
489 xtype: 'proxmoxHelpButton'
490 },
491 '->',
492 {
493 text: gettext('Apply'),
494 handler: 'applySettings',
495 bind: {
496 hidden: '{!in_totp_tab}',
497 disabled: '{!valid}'
498 }
499 },
500 {
501 xtype: 'button',
502 text: gettext('Register U2F Device'),
503 handler: 'startU2FRegistration',
504 bind: {
505 hidden: '{in_totp_tab}',
506 disabled: '{tfa_type}'
507 }
508 },
509 {
510 text: gettext('Delete'),
511 reference: 'delete_button',
512 disabled: true,
513 handler: 'deleteTFA',
514 bind: {
515 disabled: '{!canDeleteTFA}'
516 }
517 }
518 ],
519
520 initComponent: function() {
521 var me = this;
522
523 if (!me.userid) {
524 throw "no userid given";
525 }
526
527 me.callParent();
528
529 Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth');
530 }
531 });