]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/window/AddWebauthn.js
Buttons: add AltText
[proxmox-widget-toolkit.git] / src / window / AddWebauthn.js
1 Ext.define('Proxmox.window.AddWebauthn', {
2 extend: 'Ext.window.Window',
3 alias: 'widget.pmxAddWebauthn',
4 mixins: ['Proxmox.Mixin.CBind'],
5
6 onlineHelp: 'user_mgmt',
7
8 modal: true,
9 resizable: false,
10 title: gettext('Add a Webauthn login token'),
11 width: 512,
12
13 user: undefined,
14 fixedUser: false,
15
16 initComponent: function() {
17 let me = this;
18 me.callParent();
19 Ext.GlobalEvents.fireEvent('proxmoxShowHelp', me.onlineHelp);
20 },
21
22 viewModel: {
23 data: {
24 valid: false,
25 userid: null,
26 },
27 },
28
29 controller: {
30 xclass: 'Ext.app.ViewController',
31
32 control: {
33 'field': {
34 validitychange: function(field, valid) {
35 let me = this;
36 let viewmodel = me.getViewModel();
37 let form = me.lookup('webauthn_form');
38 viewmodel.set('valid', form.isValid());
39 },
40 },
41 '#': {
42 show: function() {
43 let me = this;
44 let view = me.getView();
45
46 if (Proxmox.UserName === 'root@pam') {
47 view.lookup('password').setVisible(false);
48 view.lookup('password').setDisabled(true);
49 }
50 },
51 },
52 },
53
54 registerWebauthn: async function() {
55 let me = this;
56 let values = me.lookup('webauthn_form').getValues();
57 values.type = "webauthn";
58
59 let userid = values.user;
60 delete values.user;
61
62 me.getView().mask(gettext('Please wait...'), 'x-mask-loading');
63
64 try {
65 let register_response = await Proxmox.Async.api2({
66 url: `/api2/extjs/access/tfa/${userid}`,
67 method: 'POST',
68 params: values,
69 });
70
71 let data = register_response.result.data;
72 if (!data.challenge) {
73 throw "server did not respond with a challenge";
74 }
75
76 let creds = JSON.parse(data.challenge);
77
78 // Fix this up before passing it to the browser, but keep a copy of the original
79 // string to pass in the response:
80 let challenge_str = creds.publicKey.challenge;
81 creds.publicKey.challenge = Proxmox.Utils.base64url_to_bytes(challenge_str);
82 creds.publicKey.user.id =
83 Proxmox.Utils.base64url_to_bytes(creds.publicKey.user.id);
84
85 // convert existing authenticators structure
86 creds.publicKey.excludeCredentials =
87 (creds.publicKey.excludeCredentials || [])
88 .map((credential) => ({
89 id: Proxmox.Utils.base64url_to_bytes(credential.id),
90 type: credential.type,
91 }));
92
93 let msg = Ext.Msg.show({
94 title: `Webauthn: ${gettext('Setup')}`,
95 message: gettext('Please press the button on your Webauthn Device'),
96 buttons: [],
97 });
98
99 let token_response;
100 try {
101 token_response = await navigator.credentials.create(creds);
102 } catch (error) {
103 let errmsg = error.message;
104 if (error.name === 'InvalidStateError') {
105 errmsg = gettext('Is this token already registered?');
106 }
107 throw gettext('An error occurred during token registration.') +
108 `<br>${error.name}: ${errmsg}`;
109 }
110
111 // We cannot pass ArrayBuffers to the API, so extract & convert the data.
112 let response = {
113 id: token_response.id,
114 type: token_response.type,
115 rawId: Proxmox.Utils.bytes_to_base64url(token_response.rawId),
116 response: {
117 attestationObject: Proxmox.Utils.bytes_to_base64url(
118 token_response.response.attestationObject,
119 ),
120 clientDataJSON: Proxmox.Utils.bytes_to_base64url(
121 token_response.response.clientDataJSON,
122 ),
123 },
124 };
125
126 msg.close();
127
128 let params = {
129 type: "webauthn",
130 challenge: challenge_str,
131 value: JSON.stringify(response),
132 };
133
134 if (values.password) {
135 params.password = values.password;
136 }
137
138 await Proxmox.Async.api2({
139 url: `/api2/extjs/access/tfa/${userid}`,
140 method: 'POST',
141 params,
142 });
143 } catch (response) {
144 let error = response;
145 console.error(error); // for debugging if it's not displayable...
146 if (typeof error === "object") {
147 // in case it came from an api request:
148 error = error.result?.message;
149 }
150
151 Ext.Msg.alert(gettext('Error'), error);
152 }
153
154 me.getView().close();
155 },
156 },
157
158 items: [
159 {
160 xtype: 'form',
161 reference: 'webauthn_form',
162 layout: 'anchor',
163 border: false,
164 bodyPadding: 10,
165 fieldDefaults: {
166 anchor: '100%',
167 },
168 items: [
169 {
170 xtype: 'pmxDisplayEditField',
171 name: 'user',
172 cbind: {
173 editable: (get) => !get('fixedUser'),
174 value: () => Proxmox.UserName,
175 },
176 fieldLabel: gettext('User'),
177 editConfig: {
178 xtype: 'pmxUserSelector',
179 allowBlank: false,
180 },
181 renderer: Ext.String.htmlEncode,
182 listeners: {
183 change: function(field, newValue, oldValue) {
184 let vm = this.up('window').getViewModel();
185 vm.set('userid', newValue);
186 },
187 },
188 },
189 {
190 xtype: 'textfield',
191 fieldLabel: gettext('Description'),
192 allowBlank: false,
193 name: 'description',
194 maxLength: 256,
195 emptyText: gettext('For example: TFA device ID, required to identify multiple factors.'),
196 },
197 {
198 xtype: 'textfield',
199 name: 'password',
200 reference: 'password',
201 fieldLabel: gettext('Verify Password'),
202 inputType: 'password',
203 minLength: 5,
204 allowBlank: false,
205 validateBlank: true,
206 cbind: {
207 hidden: () => Proxmox.UserName === 'root@pam',
208 disabled: () => Proxmox.UserName === 'root@pam',
209 emptyText: () =>
210 Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
211 },
212 },
213 ],
214 },
215 ],
216
217 buttons: [
218 {
219 xtype: 'proxmoxHelpButton',
220 },
221 '->',
222 {
223 xtype: 'button',
224 text: gettext('Register Webauthn Device'),
225 handler: 'registerWebauthn',
226 bind: {
227 disabled: '{!valid}',
228 },
229 },
230 ],
231 });