]>
Commit | Line | Data |
---|---|---|
6a1c9c29 | 1 | /*global u2f,QRCode,Uint8Array*/ |
34c08874 | 2 | /*jslint confusion: true*/ |
24d2ed8c WB |
3 | Ext.define('PVE.window.TFAEdit', { |
4 | extend: 'Ext.window.Window', | |
5 | mixins: ['Proxmox.Mixin.CBind'], | |
6 | ||
db2af549 TL |
7 | onlineHelp: 'pveum_tfa_auth', // fake to ensure this gets a link target |
8 | ||
24d2ed8c WB |
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; | |
b688436d | 23 | var values = me.lookup('totp_form').getValues(); |
24d2ed8c WB |
24 | var algorithm = values.algorithm; |
25 | if (!algorithm) { | |
26 | algorithm = 'SHA1'; | |
27 | } | |
28 | ||
29 | me.qrcode.makeCode( | |
54416bb3 | 30 | 'otpauth://totp/' + encodeURIComponent(me.userid) + |
24d2ed8c WB |
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) { | |
24d2ed8c WB |
43 | Ext.Msg.alert( |
44 | gettext('Error'), | |
2d41c7e6 | 45 | PVE.Utils.render_u2f_error(error) |
24d2ed8c WB |
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, | |
ec505260 | 103 | tfa_type: null, // dependencies of formulas should not be undefined |
0509c444 | 104 | valid: false, |
34c08874 | 105 | u2f_available: true |
aa8d628f TL |
106 | }, |
107 | formulas: { | |
108 | canDeleteTFA: function(get) { | |
6dfb9ec4 | 109 | return (get('tfa_type') !== null && !get('tfa_required')); |
b0184fc4 TL |
110 | }, |
111 | canSetupTOTP: function(get) { | |
112 | var tfa = get('tfa_type'); | |
6dfb9ec4 | 113 | return (tfa === null || tfa === 'totp' || tfa === 1); |
b0184fc4 TL |
114 | }, |
115 | canSetupU2F: function(get) { | |
116 | var tfa = get('tfa_type'); | |
6dfb9ec4 | 117 | return (get('u2f_available') && (tfa === null || tfa === 'u2f' || tfa === 1)); |
aa8d628f | 118 | } |
24d2ed8c WB |
119 | } |
120 | }, | |
121 | ||
5bb10629 | 122 | afterLoading: function(realm_tfa_type, user_tfa_type) { |
24d2ed8c WB |
123 | var me = this; |
124 | var viewmodel = me.getViewModel(); | |
5bb10629 DC |
125 | if (user_tfa_type === 'oath') { |
126 | user_tfa_type = 'totp'; | |
127 | } | |
128 | viewmodel.set('tfa_type', user_tfa_type || null); | |
24d2ed8c WB |
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 | |
5bb10629 DC |
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 | } | |
24d2ed8c WB |
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 | } | |
24d2ed8c WB |
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 | }, | |
0509c444 DC |
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'); | |
cea0f764 TL |
173 | var password = me.lookup('password'); |
174 | viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid()); | |
0509c444 DC |
175 | } |
176 | }, | |
24d2ed8c WB |
177 | '#': { |
178 | show: function() { | |
179 | var me = this.getView(); | |
aa8d628f TL |
180 | var viewmodel = this.getViewModel(); |
181 | ||
5bb10629 DC |
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 | ||
7b8d68fe TL |
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 | }); | |
24d2ed8c | 202 | me.down('#qrbox').getEl().appendChild(me.qrdiv); |
aa8d628f | 203 | |
6dfb9ec4 | 204 | viewmodel.set('tfa_type', me.tfa_type || null); |
ac3daab8 | 205 | if (!me.tfa_type) { |
28ec45b2 TL |
206 | this.randomizeSecret(); |
207 | } else { | |
208 | me.down('#qrbox').setVisible(false); | |
209 | me.lookup('challenge').setVisible(false); | |
1b16f713 | 210 | if (me.tfa_type === 'u2f') { |
b0184fc4 TL |
211 | var u2f_panel = me.lookup('u2f_panel'); |
212 | me.lookup('tfatabs').setActiveTab(u2f_panel); | |
1b16f713 | 213 | } |
28ec45b2 | 214 | } |
24d2ed8c WB |
215 | |
216 | if (Proxmox.UserName === 'root@pam') { | |
217 | me.lookup('password').setVisible(false); | |
218 | me.lookup('password').setDisabled(true); | |
219 | } | |
24d2ed8c WB |
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; | |
b688436d | 232 | var values = me.lookup('totp_form').getValues(); |
24d2ed8c WB |
233 | var params = { |
234 | userid: me.getView().userid, | |
235 | action: 'new', | |
236 | key: values.secret, | |
237 | config: PVE.Parser.printPropertyString({ | |
238 | type: 'oath', | |
239 | digits: values.digits, | |
34c08874 | 240 | step: values.step |
24d2ed8c WB |
241 | }), |
242 | // this is used to verify that the client generates the correct codes: | |
34c08874 | 243 | response: me.lookup('challenge').value |
24d2ed8c WB |
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; | |
b688436d | 266 | var values = me.lookup('totp_form').getValues(); |
24d2ed8c WB |
267 | var params = { |
268 | userid: me.getView().userid, | |
34c08874 | 269 | action: 'delete' |
24d2ed8c WB |
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) { | |
72bff50c | 296 | // secret must be base32, so just use the first 5 bits |
24d2ed8c WB |
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 | }); | |
b688436d | 306 | me.lookup('tfa_secret').setValue(data); |
24d2ed8c WB |
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', | |
1b16f713 | 340 | reference: 'tfatabs', |
24d2ed8c WB |
341 | border: false, |
342 | items: [ | |
343 | { | |
344 | xtype: 'panel', | |
345 | title: 'TOTP', | |
346 | itemId: 'totp-panel', | |
5bb10629 | 347 | reference: 'totp_panel', |
ac3daab8 | 348 | tfa_type: 'totp', |
24d2ed8c | 349 | border: false, |
b0184fc4 TL |
350 | bind: { |
351 | disabled: '{!canSetupTOTP}' | |
352 | }, | |
24d2ed8c WB |
353 | layout: { |
354 | type: 'vbox', | |
355 | align: 'stretch' | |
356 | }, | |
357 | items: [ | |
358 | { | |
359 | xtype: 'form', | |
360 | layout: 'anchor', | |
361 | border: false, | |
b688436d | 362 | reference: 'totp_form', |
24d2ed8c | 363 | fieldDefaults: { |
24d2ed8c | 364 | anchor: '100%', |
34c08874 | 365 | padding: '0 5' |
24d2ed8c WB |
366 | }, |
367 | items: [ | |
2f212470 TL |
368 | { |
369 | xtype: 'displayfield', | |
2f212470 TL |
370 | fieldLabel: gettext('User name'), |
371 | cbind: { | |
372 | value: '{userid}' | |
373 | } | |
374 | }, | |
24d2ed8c | 375 | { |
a8740316 TL |
376 | layout: 'hbox', |
377 | border: false, | |
9390af7b | 378 | padding: '0 0 5 0', |
a8740316 TL |
379 | items: [{ |
380 | xtype: 'textfield', | |
381 | fieldLabel: gettext('Secret'), | |
28ec45b2 | 382 | emptyText: gettext('Unchanged'), |
a8740316 TL |
383 | name: 'secret', |
384 | reference: 'tfa_secret', | |
0bc11f81 TL |
385 | regex: /^[A-Z2-7=]+$/, |
386 | regexText: 'Must be base32 [A-Z2-7=]', | |
7bc70192 | 387 | maskRe: /[A-Z2-7=]/, |
a8740316 TL |
388 | qrupdate: true, |
389 | flex: 4 | |
24d2ed8c | 390 | }, |
a8740316 TL |
391 | { |
392 | xtype: 'button', | |
393 | text: gettext('Randomize'), | |
394 | reference: 'randomize_button', | |
395 | handler: 'randomizeSecret', | |
396 | flex: 1 | |
397 | }] | |
24d2ed8c WB |
398 | }, |
399 | { | |
400 | xtype: 'numberfield', | |
401 | fieldLabel: gettext('Time period'), | |
402 | name: 'step', | |
2c2197a3 TL |
403 | // Google Authenticator ignores this and generates bogus data |
404 | hidden: true, | |
24d2ed8c WB |
405 | value: 30, |
406 | minValue: 10, | |
34c08874 | 407 | qrupdate: true |
24d2ed8c WB |
408 | }, |
409 | { | |
410 | xtype: 'numberfield', | |
411 | fieldLabel: gettext('Digits'), | |
412 | name: 'digits', | |
413 | value: 6, | |
2c2197a3 TL |
414 | // Google Authenticator ignores this and generates bogus data |
415 | hidden: true, | |
24d2ed8c WB |
416 | minValue: 6, |
417 | maxValue: 8, | |
34c08874 | 418 | qrupdate: true |
24d2ed8c WB |
419 | }, |
420 | { | |
421 | xtype: 'textfield', | |
422 | fieldLabel: gettext('Issuer Name'), | |
423 | name: 'issuer', | |
424 | value: 'Proxmox Web UI', | |
34c08874 | 425 | qrupdate: true |
24d2ed8c WB |
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', | |
34c08874 | 437 | height: '266px' |
24d2ed8c WB |
438 | } |
439 | }, | |
440 | { | |
441 | xtype: 'textfield', | |
0abfdbfa | 442 | fieldLabel: gettext('Verification Code'), |
0509c444 | 443 | allowBlank: false, |
24d2ed8c WB |
444 | reference: 'challenge', |
445 | padding: '0 5', | |
7b8d68fe | 446 | emptyText: gettext('Scan QR code and enter TOTP auth. code to verify') |
24d2ed8c WB |
447 | } |
448 | ] | |
449 | }, | |
450 | { | |
451 | title: 'U2F', | |
452 | itemId: 'u2f-panel', | |
b688436d | 453 | reference: 'u2f_panel', |
ac3daab8 | 454 | tfa_type: 'u2f', |
24d2ed8c WB |
455 | border: false, |
456 | padding: '5 5', | |
457 | layout: { | |
458 | type: 'vbox', | |
459 | align: 'middle' | |
460 | }, | |
461 | bind: { | |
b0184fc4 | 462 | disabled: '{!canSetupU2F}' |
24d2ed8c WB |
463 | }, |
464 | items: [ | |
465 | { | |
466 | xtype: 'label', | |
467 | width: 500, | |
34c08874 | 468 | text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.') |
24d2ed8c WB |
469 | } |
470 | ] | |
471 | } | |
472 | ] | |
473 | }, | |
474 | { | |
475 | xtype: 'textfield', | |
476 | inputType: 'password', | |
477 | fieldLabel: gettext('Password'), | |
478 | minLength: 5, | |
479 | reference: 'password', | |
cea0f764 TL |
480 | allowBlank: false, |
481 | validateBlank: true, | |
482 | padding: '0 0 5 5', | |
24d2ed8c WB |
483 | emptyText: gettext('verify current password') |
484 | } | |
485 | ], | |
486 | ||
487 | buttons: [ | |
db2af549 TL |
488 | { |
489 | xtype: 'proxmoxHelpButton' | |
490 | }, | |
491 | '->', | |
24d2ed8c WB |
492 | { |
493 | text: gettext('Apply'), | |
494 | handler: 'applySettings', | |
495 | bind: { | |
496 | hidden: '{!in_totp_tab}', | |
0509c444 | 497 | disabled: '{!valid}' |
24d2ed8c WB |
498 | } |
499 | }, | |
500 | { | |
501 | xtype: 'button', | |
502 | text: gettext('Register U2F Device'), | |
503 | handler: 'startU2FRegistration', | |
504 | bind: { | |
ac3daab8 | 505 | hidden: '{in_totp_tab}', |
b0184fc4 | 506 | disabled: '{tfa_type}' |
24d2ed8c WB |
507 | } |
508 | }, | |
509 | { | |
510 | text: gettext('Delete'), | |
b688436d | 511 | reference: 'delete_button', |
1bfa0588 | 512 | disabled: true, |
24d2ed8c WB |
513 | handler: 'deleteTFA', |
514 | bind: { | |
aa8d628f | 515 | disabled: '{!canDeleteTFA}' |
24d2ed8c WB |
516 | } |
517 | } | |
518 | ], | |
519 | ||
520 | initComponent: function() { | |
521 | var me = this; | |
522 | ||
5bb10629 DC |
523 | if (!me.userid) { |
524 | throw "no userid given"; | |
525 | } | |
24d2ed8c WB |
526 | |
527 | me.callParent(); | |
db2af549 TL |
528 | |
529 | Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth'); | |
24d2ed8c WB |
530 | } |
531 | }); |