]>
Commit | Line | Data |
---|---|---|
34c08874 | 1 | /*jslint confusion: true*/ |
24d2ed8c WB |
2 | Ext.define('PVE.window.TFAEdit', { |
3 | extend: 'Ext.window.Window', | |
4 | mixins: ['Proxmox.Mixin.CBind'], | |
5 | ||
6 | modal: true, | |
7 | resizable: false, | |
8 | title: gettext('Two Factor Authentication'), | |
9 | subject: 'TFA', | |
10 | url: '/api2/extjs/access/tfa', | |
11 | width: 512, | |
12 | ||
13 | layout: { | |
14 | type: 'vbox', | |
15 | align: 'stretch' | |
16 | }, | |
17 | ||
18 | updateQrCode: function() { | |
19 | var me = this; | |
20 | var values = me.lookup('totp-form').getValues(); | |
21 | var algorithm = values.algorithm; | |
22 | if (!algorithm) { | |
23 | algorithm = 'SHA1'; | |
24 | } | |
25 | ||
26 | me.qrcode.makeCode( | |
27 | 'otpauth://totp/' + encodeURIComponent(values.name) + | |
28 | '?secret=' + values.secret + | |
29 | '&period=' + values.step + | |
30 | '&digits=' + values.digits + | |
31 | '&algorithm=' + algorithm + | |
32 | '&issuer=' + encodeURIComponent(values.issuer) | |
33 | ); | |
34 | ||
35 | me.lookup('challenge').setVisible(true); | |
36 | me.down('#qrbox').setVisible(true); | |
37 | }, | |
38 | ||
39 | showError: function(error) { | |
40 | var ErrorNames = { | |
41 | '1': gettext('Other Error'), | |
42 | '2': gettext('Bad Request'), | |
43 | '3': gettext('Configuration Unsupported'), | |
44 | '4': gettext('Device Ineligible'), | |
45 | '5': gettext('Timeout') | |
46 | }; | |
47 | Ext.Msg.alert( | |
48 | gettext('Error'), | |
49 | "U2F Error: " + (ErrorNames[error] || Proxmox.Utils.unknownText) | |
50 | ); | |
51 | }, | |
52 | ||
53 | doU2FChallenge: function(response) { | |
54 | var me = this; | |
55 | ||
56 | var data = response.result.data; | |
57 | me.lookup('password').setDisabled(true); | |
58 | var msg = Ext.Msg.show({ | |
59 | title: 'U2F: '+gettext('Setup'), | |
60 | message: gettext('Please press the button on your U2F Device'), | |
61 | buttons: [] | |
62 | }); | |
63 | Ext.Function.defer(function() { | |
64 | u2f.register(data.appId, [data], [], function(data) { | |
65 | msg.close(); | |
66 | if (data.errorCode) { | |
67 | me.showError(data.errorCode); | |
68 | } else { | |
69 | me.respondToU2FChallenge(data); | |
70 | } | |
71 | }); | |
72 | }, 500, me); | |
73 | }, | |
74 | ||
75 | respondToU2FChallenge: function(data) { | |
76 | var me = this; | |
77 | var params = { | |
78 | userid: me.userid, | |
79 | action: 'confirm', | |
80 | response: JSON.stringify(data) | |
81 | }; | |
82 | if (Proxmox.UserName !== 'root@pam') { | |
83 | params.password = me.lookup('password').value; | |
84 | } | |
85 | Proxmox.Utils.API2Request({ | |
86 | url: '/api2/extjs/access/tfa', | |
87 | params: params, | |
88 | method: 'PUT', | |
89 | success: function() { | |
90 | me.close(); | |
91 | Ext.Msg.show({ | |
92 | title: gettext('Success'), | |
93 | message: gettext('U2F Device successfully connected.'), | |
94 | buttons: Ext.Msg.OK | |
95 | }); | |
96 | }, | |
97 | failure: function(response, opts) { | |
98 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
99 | } | |
100 | }); | |
101 | }, | |
102 | ||
103 | viewModel: { | |
104 | data: { | |
105 | in_totp_tab: true, | |
106 | tfa_required: false, | |
34c08874 | 107 | u2f_available: true |
24d2ed8c WB |
108 | } |
109 | }, | |
110 | ||
111 | afterLoadingRealm: function(realm_tfa_type) { | |
112 | var me = this; | |
113 | var viewmodel = me.getViewModel(); | |
114 | if (!realm_tfa_type) { | |
115 | // There's no TFA enforced by the realm, everything works. | |
116 | viewmodel.set('u2f_available', true); | |
117 | viewmodel.set('tfa_required', false); | |
118 | } else if (realm_tfa_type === 'oath') { | |
119 | // The realm explicitly requires TOTP | |
120 | viewmodel.set('tfa_required', true); | |
121 | viewmodel.set('u2f_available', false); | |
122 | } else { | |
123 | // The realm enforces some other TFA type (yubico) | |
124 | me.close(); | |
125 | Ext.Msg.alert( | |
126 | gettext('Error'), | |
127 | Ext.String.format( | |
128 | gettext("Custom 2nd factor configuration is not supported on realms with '{0}' TFA."), | |
129 | realm_tfa_type | |
130 | ) | |
131 | ); | |
132 | } | |
133 | //me.lookup('delete-button').setDisabled(has_tfa_configured); | |
134 | //me.lookup('u2f-panel').setDisabled(has_tfa_configured); | |
135 | }, | |
136 | ||
137 | controller: { | |
138 | xclass: 'Ext.app.ViewController', | |
139 | control: { | |
140 | 'field[qrupdate=true]': { | |
141 | change: function() { | |
142 | var me = this.getView(); | |
143 | me.updateQrCode(); | |
144 | } | |
145 | }, | |
146 | '#': { | |
147 | show: function() { | |
148 | var me = this.getView(); | |
149 | me.down('#qrbox').getEl().appendChild(me.qrdiv); | |
150 | me.down('#qrbox').setVisible(false); | |
151 | ||
152 | if (Proxmox.UserName === 'root@pam') { | |
153 | me.lookup('password').setVisible(false); | |
154 | me.lookup('password').setDisabled(true); | |
155 | } | |
156 | me.lookup('challenge').setVisible(false); | |
157 | } | |
158 | }, | |
159 | '#tfatabs': { | |
160 | tabchange: function(panel, newcard) { | |
161 | var viewmodel = this.getViewModel(); | |
162 | viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel'); | |
163 | } | |
164 | } | |
165 | }, | |
166 | ||
167 | applySettings: function() { | |
168 | var me = this; | |
169 | var values = me.lookup('totp-form').getValues(); | |
170 | var params = { | |
171 | userid: me.getView().userid, | |
172 | action: 'new', | |
173 | key: values.secret, | |
174 | config: PVE.Parser.printPropertyString({ | |
175 | type: 'oath', | |
176 | digits: values.digits, | |
34c08874 | 177 | step: values.step |
24d2ed8c WB |
178 | }), |
179 | // this is used to verify that the client generates the correct codes: | |
34c08874 | 180 | response: me.lookup('challenge').value |
24d2ed8c WB |
181 | }; |
182 | ||
183 | if (Proxmox.UserName !== 'root@pam') { | |
184 | params.password = me.lookup('password').value; | |
185 | } | |
186 | ||
187 | Proxmox.Utils.API2Request({ | |
188 | url: '/api2/extjs/access/tfa', | |
189 | params: params, | |
190 | method: 'PUT', | |
191 | waitMsgTarget: me.getView(), | |
192 | success: function(response, opts) { | |
193 | me.getView().close(); | |
194 | }, | |
195 | failure: function(response, opts) { | |
196 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
197 | } | |
198 | }); | |
199 | }, | |
200 | ||
201 | deleteTFA: function() { | |
202 | var me = this; | |
203 | var values = me.lookup('totp-form').getValues(); | |
204 | var params = { | |
205 | userid: me.getView().userid, | |
34c08874 | 206 | action: 'delete' |
24d2ed8c WB |
207 | }; |
208 | ||
209 | if (Proxmox.UserName !== 'root@pam') { | |
210 | params.password = me.lookup('password').value; | |
211 | } | |
212 | ||
213 | Proxmox.Utils.API2Request({ | |
214 | url: '/api2/extjs/access/tfa', | |
215 | params: params, | |
216 | method: 'PUT', | |
217 | waitMsgTarget: me.getView(), | |
218 | success: function(response, opts) { | |
219 | me.getView().close(); | |
220 | }, | |
221 | failure: function(response, opts) { | |
222 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
223 | } | |
224 | }); | |
225 | }, | |
226 | ||
227 | randomizeSecret: function() { | |
228 | var me = this; | |
229 | var rnd = new Uint8Array(16); | |
230 | window.crypto.getRandomValues(rnd); | |
231 | var data = ''; | |
232 | rnd.forEach(function(b) { | |
233 | // just use the first 5 bit | |
234 | b = b & 0x1f; | |
235 | if (b < 26) { | |
236 | // A..Z | |
237 | data += String.fromCharCode(b + 0x41); | |
238 | } else { | |
239 | // 2..7 | |
240 | data += String.fromCharCode(b-26 + 0x32); | |
241 | } | |
242 | }); | |
243 | me.lookup('tfa-secret').setValue(data); | |
244 | }, | |
245 | ||
246 | startU2FRegistration: function() { | |
247 | var me = this; | |
248 | ||
249 | var params = { | |
250 | userid: me.getView().userid, | |
251 | action: 'new' | |
252 | }; | |
253 | ||
254 | if (Proxmox.UserName !== 'root@pam') { | |
255 | params.password = me.lookup('password').value; | |
256 | } | |
257 | ||
258 | Proxmox.Utils.API2Request({ | |
259 | url: '/api2/extjs/access/tfa', | |
260 | params: params, | |
261 | method: 'PUT', | |
262 | waitMsgTarget: me.getView(), | |
263 | success: function(response) { | |
264 | me.getView().doU2FChallenge(response); | |
265 | }, | |
266 | failure: function(response, opts) { | |
267 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
268 | } | |
269 | }); | |
270 | } | |
271 | }, | |
272 | ||
273 | items: [ | |
274 | { | |
275 | xtype: 'tabpanel', | |
276 | itemId: 'tfatabs', | |
277 | border: false, | |
278 | items: [ | |
279 | { | |
280 | xtype: 'panel', | |
281 | title: 'TOTP', | |
282 | itemId: 'totp-panel', | |
283 | border: false, | |
284 | layout: { | |
285 | type: 'vbox', | |
286 | align: 'stretch' | |
287 | }, | |
288 | items: [ | |
289 | { | |
290 | xtype: 'form', | |
291 | layout: 'anchor', | |
292 | border: false, | |
293 | reference: 'totp-form', | |
294 | fieldDefaults: { | |
295 | labelWidth: 120, | |
296 | anchor: '100%', | |
34c08874 | 297 | padding: '0 5' |
24d2ed8c WB |
298 | }, |
299 | items: [ | |
300 | { | |
a8740316 TL |
301 | layout: 'hbox', |
302 | border: false, | |
303 | padding: '5 0', | |
304 | items: [{ | |
305 | xtype: 'textfield', | |
306 | fieldLabel: gettext('Secret'), | |
307 | name: 'secret', | |
308 | reference: 'tfa_secret', | |
309 | validateValue: function(value) { | |
310 | return value.match(/^[A-Z2-7=]$/); | |
311 | }, | |
312 | qrupdate: true, | |
313 | flex: 4 | |
24d2ed8c | 314 | }, |
a8740316 TL |
315 | { |
316 | xtype: 'button', | |
317 | text: gettext('Randomize'), | |
318 | reference: 'randomize_button', | |
319 | handler: 'randomizeSecret', | |
320 | flex: 1 | |
321 | }] | |
24d2ed8c WB |
322 | }, |
323 | { | |
324 | xtype: 'numberfield', | |
325 | fieldLabel: gettext('Time period'), | |
326 | name: 'step', | |
327 | value: 30, | |
328 | minValue: 10, | |
34c08874 | 329 | qrupdate: true |
24d2ed8c WB |
330 | }, |
331 | { | |
332 | xtype: 'numberfield', | |
333 | fieldLabel: gettext('Digits'), | |
334 | name: 'digits', | |
335 | value: 6, | |
336 | minValue: 6, | |
337 | maxValue: 8, | |
34c08874 | 338 | qrupdate: true |
24d2ed8c WB |
339 | }, |
340 | { | |
341 | xtype: 'textfield', | |
342 | fieldLabel: gettext('Issuer Name'), | |
343 | name: 'issuer', | |
344 | value: 'Proxmox Web UI', | |
34c08874 | 345 | qrupdate: true |
24d2ed8c WB |
346 | }, |
347 | { | |
348 | xtype: 'textfield', | |
ef50fb29 | 349 | hidden: true, |
24d2ed8c WB |
350 | name: 'name', |
351 | cbind: { | |
34c08874 | 352 | value: '{userid}' |
24d2ed8c | 353 | }, |
34c08874 | 354 | qrupdate: true |
24d2ed8c WB |
355 | } |
356 | ] | |
357 | }, | |
358 | { | |
359 | xtype: 'box', | |
360 | itemId: 'qrbox', | |
361 | visible: false, // will be enabled when generating a qr code | |
362 | style: { | |
363 | 'background-color': 'white', | |
364 | padding: '5px', | |
365 | width: '266px', | |
34c08874 | 366 | height: '266px' |
24d2ed8c WB |
367 | } |
368 | }, | |
369 | { | |
370 | xtype: 'textfield', | |
371 | fieldLabel: gettext('Code'), | |
372 | labelWidth: 120, | |
373 | reference: 'challenge', | |
374 | padding: '0 5', | |
375 | emptyText: gettext('verify TOTP authentication code') | |
376 | } | |
377 | ] | |
378 | }, | |
379 | { | |
380 | title: 'U2F', | |
381 | itemId: 'u2f-panel', | |
382 | reference: 'u2f-panel', | |
383 | border: false, | |
384 | padding: '5 5', | |
385 | layout: { | |
386 | type: 'vbox', | |
387 | align: 'middle' | |
388 | }, | |
389 | bind: { | |
390 | disabled: '{!u2f_available}' | |
391 | }, | |
392 | items: [ | |
393 | { | |
394 | xtype: 'label', | |
395 | width: 500, | |
34c08874 | 396 | text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.') |
24d2ed8c WB |
397 | } |
398 | ] | |
399 | } | |
400 | ] | |
401 | }, | |
402 | { | |
403 | xtype: 'textfield', | |
404 | inputType: 'password', | |
405 | fieldLabel: gettext('Password'), | |
406 | minLength: 5, | |
407 | reference: 'password', | |
408 | padding: '0 5', | |
409 | labelWidth: 120, | |
410 | emptyText: gettext('verify current password') | |
411 | } | |
412 | ], | |
413 | ||
414 | buttons: [ | |
24d2ed8c WB |
415 | { |
416 | text: gettext('Apply'), | |
417 | handler: 'applySettings', | |
418 | bind: { | |
419 | hidden: '{!in_totp_tab}', | |
420 | disabled: '{!user_tfa}' | |
421 | } | |
422 | }, | |
423 | { | |
424 | xtype: 'button', | |
425 | text: gettext('Register U2F Device'), | |
426 | handler: 'startU2FRegistration', | |
427 | bind: { | |
428 | hidden: '{in_totp_tab}' | |
429 | } | |
430 | }, | |
431 | { | |
432 | text: gettext('Delete'), | |
433 | reference: 'delete-button', | |
434 | handler: 'deleteTFA', | |
435 | bind: { | |
436 | disabled: '{tfa_required}' | |
437 | } | |
438 | } | |
439 | ], | |
440 | ||
441 | initComponent: function() { | |
442 | var me = this; | |
443 | ||
444 | me.qrdiv = document.createElement('center'); | |
445 | me.qrcode = new QRCode(me.qrdiv, { | |
446 | //text: "This is not the qr code you're looking for", | |
447 | width: 256, | |
448 | height: 256, | |
449 | correctLevel: QRCode.CorrectLevel.M | |
450 | }); | |
451 | ||
452 | var store = new Ext.data.Store({ | |
453 | model: 'pve-domains', | |
454 | autoLoad: true | |
455 | }); | |
456 | ||
457 | store.on('load', function() { | |
458 | var user_realm = me.userid.split('@')[1]; | |
459 | var realm = me.store.findRecord('realm', user_realm); | |
460 | me.afterLoadingRealm(realm && realm.data && realm.data.tfa); | |
461 | }, me); | |
462 | ||
463 | Ext.apply(me, { store: store }); | |
464 | ||
465 | me.callParent(); | |
466 | } | |
467 | }); |