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