]>
Commit | Line | Data |
---|---|---|
6a1c9c29 | 1 | /*global u2f,QRCode,Uint8Array*/ |
24d2ed8c WB |
2 | Ext.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 | ':' + | |
32 | encodeURIComponent(values.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'), | |
f6710aac | 47 | PVE.Utils.render_u2f_error(error), |
24d2ed8c WB |
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'), | |
f6710aac | 59 | buttons: [], |
24d2ed8c WB |
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', | |
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() { | |
181 | var me = this.getView(); | |
182 | me.updateQrCode(); | |
f6710aac | 183 | }, |
24d2ed8c | 184 | }, |
0509c444 DC |
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'); | |
cea0f764 TL |
191 | var password = me.lookup('password'); |
192 | viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid()); | |
f6710aac | 193 | }, |
0509c444 | 194 | }, |
24d2ed8c WB |
195 | '#': { |
196 | show: function() { | |
197 | var me = this.getView(); | |
aa8d628f TL |
198 | var viewmodel = this.getViewModel(); |
199 | ||
5bb10629 DC |
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) { | |
49b54908 DC |
210 | me.close(); |
211 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
f6710aac | 212 | }, |
5bb10629 DC |
213 | }); |
214 | ||
7b8d68fe TL |
215 | me.qrdiv = document.createElement('center'); |
216 | me.qrcode = new QRCode(me.qrdiv, { | |
217 | width: 256, | |
218 | height: 256, | |
f6710aac | 219 | correctLevel: QRCode.CorrectLevel.M, |
7b8d68fe | 220 | }); |
24d2ed8c | 221 | me.down('#qrbox').getEl().appendChild(me.qrdiv); |
aa8d628f | 222 | |
24d2ed8c WB |
223 | if (Proxmox.UserName === 'root@pam') { |
224 | me.lookup('password').setVisible(false); | |
225 | me.lookup('password').setDisabled(true); | |
226 | } | |
f6710aac | 227 | }, |
24d2ed8c WB |
228 | }, |
229 | '#tfatabs': { | |
230 | tabchange: function(panel, newcard) { | |
231 | var viewmodel = this.getViewModel(); | |
232 | viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel'); | |
f6710aac TL |
233 | }, |
234 | }, | |
24d2ed8c WB |
235 | }, |
236 | ||
237 | applySettings: function() { | |
238 | var me = this; | |
b688436d | 239 | var values = me.lookup('totp_form').getValues(); |
24d2ed8c WB |
240 | var params = { |
241 | userid: me.getView().userid, | |
242 | action: 'new', | |
20ce59c3 | 243 | key: 'v2-' + values.secret, |
24d2ed8c WB |
244 | config: PVE.Parser.printPropertyString({ |
245 | type: 'oath', | |
246 | digits: values.digits, | |
f6710aac | 247 | step: values.step, |
24d2ed8c WB |
248 | }), |
249 | // this is used to verify that the client generates the correct codes: | |
f6710aac | 250 | response: me.lookup('challenge').value, |
24d2ed8c WB |
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); | |
f6710aac | 267 | }, |
24d2ed8c WB |
268 | }); |
269 | }, | |
270 | ||
271 | deleteTFA: function() { | |
272 | var me = this; | |
b688436d | 273 | var values = me.lookup('totp_form').getValues(); |
24d2ed8c WB |
274 | var params = { |
275 | userid: me.getView().userid, | |
f6710aac | 276 | action: 'delete', |
24d2ed8c WB |
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); | |
f6710aac | 293 | }, |
24d2ed8c WB |
294 | }); |
295 | }, | |
296 | ||
297 | randomizeSecret: function() { | |
298 | var me = this; | |
7cfd876e | 299 | var rnd = new Uint8Array(32); |
24d2ed8c WB |
300 | window.crypto.getRandomValues(rnd); |
301 | var data = ''; | |
302 | rnd.forEach(function(b) { | |
72bff50c | 303 | // secret must be base32, so just use the first 5 bits |
24d2ed8c WB |
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 | }); | |
bfdfea50 | 313 | me.getViewModel().set('secret', data); |
24d2ed8c WB |
314 | }, |
315 | ||
316 | startU2FRegistration: function() { | |
317 | var me = this; | |
318 | ||
319 | var params = { | |
320 | userid: me.getView().userid, | |
f6710aac | 321 | action: 'new', |
24d2ed8c WB |
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); | |
f6710aac | 338 | }, |
24d2ed8c | 339 | }); |
f6710aac | 340 | }, |
24d2ed8c WB |
341 | }, |
342 | ||
343 | items: [ | |
344 | { | |
345 | xtype: 'tabpanel', | |
346 | itemId: 'tfatabs', | |
1b16f713 | 347 | reference: 'tfatabs', |
24d2ed8c | 348 | border: false, |
bfdfea50 DC |
349 | bind: { |
350 | activeTab: '{selectedTab}', | |
351 | }, | |
24d2ed8c WB |
352 | items: [ |
353 | { | |
354 | xtype: 'panel', | |
355 | title: 'TOTP', | |
356 | itemId: 'totp-panel', | |
5bb10629 | 357 | reference: 'totp_panel', |
ac3daab8 | 358 | tfa_type: 'totp', |
24d2ed8c | 359 | border: false, |
b0184fc4 | 360 | bind: { |
f6710aac | 361 | disabled: '{!canSetupTOTP}', |
b0184fc4 | 362 | }, |
24d2ed8c WB |
363 | layout: { |
364 | type: 'vbox', | |
f6710aac | 365 | align: 'stretch', |
24d2ed8c WB |
366 | }, |
367 | items: [ | |
368 | { | |
369 | xtype: 'form', | |
370 | layout: 'anchor', | |
371 | border: false, | |
b688436d | 372 | reference: 'totp_form', |
24d2ed8c | 373 | fieldDefaults: { |
24d2ed8c | 374 | anchor: '100%', |
f6710aac | 375 | padding: '0 5', |
24d2ed8c WB |
376 | }, |
377 | items: [ | |
2f212470 TL |
378 | { |
379 | xtype: 'displayfield', | |
2f212470 | 380 | fieldLabel: gettext('User name'), |
1011b569 | 381 | renderer: Ext.String.htmlEncode, |
2f212470 | 382 | cbind: { |
f6710aac TL |
383 | value: '{userid}', |
384 | }, | |
2f212470 | 385 | }, |
24d2ed8c | 386 | { |
a8740316 TL |
387 | layout: 'hbox', |
388 | border: false, | |
9390af7b | 389 | padding: '0 0 5 0', |
a8740316 TL |
390 | items: [{ |
391 | xtype: 'textfield', | |
392 | fieldLabel: gettext('Secret'), | |
28ec45b2 | 393 | emptyText: gettext('Unchanged'), |
a8740316 TL |
394 | name: 'secret', |
395 | reference: 'tfa_secret', | |
0bc11f81 TL |
396 | regex: /^[A-Z2-7=]+$/, |
397 | regexText: 'Must be base32 [A-Z2-7=]', | |
7bc70192 | 398 | maskRe: /[A-Z2-7=]/, |
a8740316 | 399 | qrupdate: true, |
bfdfea50 DC |
400 | bind: { |
401 | value: "{secret}", | |
402 | }, | |
f6710aac | 403 | flex: 4, |
24d2ed8c | 404 | }, |
a8740316 TL |
405 | { |
406 | xtype: 'button', | |
407 | text: gettext('Randomize'), | |
408 | reference: 'randomize_button', | |
409 | handler: 'randomizeSecret', | |
f6710aac TL |
410 | flex: 1, |
411 | }], | |
24d2ed8c WB |
412 | }, |
413 | { | |
414 | xtype: 'numberfield', | |
415 | fieldLabel: gettext('Time period'), | |
416 | name: 'step', | |
2c2197a3 TL |
417 | // Google Authenticator ignores this and generates bogus data |
418 | hidden: true, | |
24d2ed8c WB |
419 | value: 30, |
420 | minValue: 10, | |
f6710aac | 421 | qrupdate: true, |
24d2ed8c WB |
422 | }, |
423 | { | |
424 | xtype: 'numberfield', | |
425 | fieldLabel: gettext('Digits'), | |
426 | name: 'digits', | |
427 | value: 6, | |
2c2197a3 TL |
428 | // Google Authenticator ignores this and generates bogus data |
429 | hidden: true, | |
24d2ed8c WB |
430 | minValue: 6, |
431 | maxValue: 8, | |
f6710aac | 432 | qrupdate: true, |
24d2ed8c WB |
433 | }, |
434 | { | |
435 | xtype: 'textfield', | |
436 | fieldLabel: gettext('Issuer Name'), | |
437 | name: 'issuer', | |
438 | value: 'Proxmox Web UI', | |
f6710aac TL |
439 | qrupdate: true, |
440 | }, | |
441 | ], | |
24d2ed8c WB |
442 | }, |
443 | { | |
444 | xtype: 'box', | |
445 | itemId: 'qrbox', | |
446 | visible: false, // will be enabled when generating a qr code | |
bfdfea50 DC |
447 | bind: { |
448 | visible: '{!secretEmpty}', | |
449 | }, | |
24d2ed8c WB |
450 | style: { |
451 | 'background-color': 'white', | |
452 | padding: '5px', | |
453 | width: '266px', | |
f6710aac TL |
454 | height: '266px', |
455 | }, | |
24d2ed8c WB |
456 | }, |
457 | { | |
458 | xtype: 'textfield', | |
0abfdbfa | 459 | fieldLabel: gettext('Verification Code'), |
0509c444 | 460 | allowBlank: false, |
24d2ed8c | 461 | reference: 'challenge', |
bfdfea50 DC |
462 | bind: { |
463 | disabled: '{!showTOTPVerifiction}', | |
464 | visible: '{showTOTPVerifiction}', | |
465 | }, | |
24d2ed8c | 466 | padding: '0 5', |
f6710aac TL |
467 | emptyText: gettext('Scan QR code and enter TOTP auth. code to verify'), |
468 | }, | |
469 | ], | |
24d2ed8c WB |
470 | }, |
471 | { | |
472 | title: 'U2F', | |
473 | itemId: 'u2f-panel', | |
b688436d | 474 | reference: 'u2f_panel', |
ac3daab8 | 475 | tfa_type: 'u2f', |
24d2ed8c WB |
476 | border: false, |
477 | padding: '5 5', | |
478 | layout: { | |
479 | type: 'vbox', | |
f6710aac | 480 | align: 'middle', |
24d2ed8c WB |
481 | }, |
482 | bind: { | |
f6710aac | 483 | disabled: '{!canSetupU2F}', |
24d2ed8c WB |
484 | }, |
485 | items: [ | |
486 | { | |
487 | xtype: 'label', | |
488 | width: 500, | |
f6710aac TL |
489 | text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.'), |
490 | }, | |
491 | ], | |
492 | }, | |
493 | ], | |
24d2ed8c WB |
494 | }, |
495 | { | |
496 | xtype: 'textfield', | |
497 | inputType: 'password', | |
498 | fieldLabel: gettext('Password'), | |
499 | minLength: 5, | |
500 | reference: 'password', | |
cea0f764 TL |
501 | allowBlank: false, |
502 | validateBlank: true, | |
503 | padding: '0 0 5 5', | |
f6710aac TL |
504 | emptyText: gettext('verify current password'), |
505 | }, | |
24d2ed8c WB |
506 | ], |
507 | ||
508 | buttons: [ | |
db2af549 | 509 | { |
f6710aac | 510 | xtype: 'proxmoxHelpButton', |
db2af549 TL |
511 | }, |
512 | '->', | |
24d2ed8c WB |
513 | { |
514 | text: gettext('Apply'), | |
515 | handler: 'applySettings', | |
516 | bind: { | |
517 | hidden: '{!in_totp_tab}', | |
f6710aac TL |
518 | disabled: '{!valid}', |
519 | }, | |
24d2ed8c WB |
520 | }, |
521 | { | |
522 | xtype: 'button', | |
523 | text: gettext('Register U2F Device'), | |
524 | handler: 'startU2FRegistration', | |
525 | bind: { | |
ac3daab8 | 526 | hidden: '{in_totp_tab}', |
f6710aac TL |
527 | disabled: '{tfa_type}', |
528 | }, | |
24d2ed8c WB |
529 | }, |
530 | { | |
531 | text: gettext('Delete'), | |
b688436d | 532 | reference: 'delete_button', |
1bfa0588 | 533 | disabled: true, |
24d2ed8c WB |
534 | handler: 'deleteTFA', |
535 | bind: { | |
f6710aac TL |
536 | disabled: '{!canDeleteTFA}', |
537 | }, | |
538 | }, | |
24d2ed8c WB |
539 | ], |
540 | ||
541 | initComponent: function() { | |
542 | var me = this; | |
543 | ||
5bb10629 DC |
544 | if (!me.userid) { |
545 | throw "no userid given"; | |
546 | } | |
24d2ed8c WB |
547 | |
548 | me.callParent(); | |
db2af549 TL |
549 | |
550 | Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth'); | |
f6710aac | 551 | }, |
24d2ed8c | 552 | }); |