]>
Commit | Line | Data |
---|---|---|
12f237de | 1 | /*global u2f,QRCode*/ |
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 | ':' + | |
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 | }); |