]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/dc/TFAEdit.js
ui: user view: show tfa type name
[pve-manager.git] / www / manager6 / dc / TFAEdit.js
CommitLineData
6a1c9c29 1/*global u2f,QRCode,Uint8Array*/
34c08874 2/*jslint confusion: true*/
24d2ed8c
WB
3Ext.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});