]>
Commit | Line | Data |
---|---|---|
64176447 WB |
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) { | |
834cc847 | 144 | let error = response; |
64176447 | 145 | console.error(error); // for debugging if it's not displayable... |
834cc847 WB |
146 | if (typeof error === "object") { |
147 | // in case it came from an api request: | |
148 | error = error.result?.message; | |
149 | } | |
150 | ||
64176447 WB |
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 | }); |