Bug 1805294 [wpt PR 37463] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / services / fxaccounts / FxAccountsConfig.jsm
blobea485c56d32118e42c38e611cbac0d0b080d9f48
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
5 var EXPORTED_SYMBOLS = ["FxAccountsConfig"];
7 const { RESTRequest } = ChromeUtils.import(
8   "resource://services-common/rest.js"
9 );
10 const { log } = ChromeUtils.import(
11   "resource://gre/modules/FxAccountsCommon.js"
13 const { XPCOMUtils } = ChromeUtils.importESModule(
14   "resource://gre/modules/XPCOMUtils.sys.mjs"
17 const lazy = {};
19 XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
20   return ChromeUtils.import(
21     "resource://gre/modules/FxAccounts.jsm"
22   ).getFxAccountsSingleton();
23 });
25 ChromeUtils.defineModuleGetter(
26   lazy,
27   "EnsureFxAccountsWebChannel",
28   "resource://gre/modules/FxAccountsWebChannel.jsm"
31 XPCOMUtils.defineLazyPreferenceGetter(
32   lazy,
33   "ROOT_URL",
34   "identity.fxaccounts.remote.root"
36 XPCOMUtils.defineLazyPreferenceGetter(
37   lazy,
38   "CONTEXT_PARAM",
39   "identity.fxaccounts.contextParam"
41 XPCOMUtils.defineLazyPreferenceGetter(
42   lazy,
43   "REQUIRES_HTTPS",
44   "identity.fxaccounts.allowHttp",
45   false,
46   null,
47   val => !val
50 const CONFIG_PREFS = [
51   "identity.fxaccounts.remote.root",
52   "identity.fxaccounts.auth.uri",
53   "identity.fxaccounts.remote.oauth.uri",
54   "identity.fxaccounts.remote.profile.uri",
55   "identity.fxaccounts.remote.pairing.uri",
56   "identity.sync.tokenserver.uri",
58 const SYNC_PARAM = "sync";
60 var FxAccountsConfig = {
61   async promiseEmailURI(email, entrypoint, extraParams = {}) {
62     return this._buildURL("", {
63       extraParams: { entrypoint, email, service: SYNC_PARAM, ...extraParams },
64     });
65   },
67   async promiseConnectAccountURI(entrypoint, extraParams = {}) {
68     return this._buildURL("", {
69       extraParams: {
70         entrypoint,
71         action: "email",
72         service: SYNC_PARAM,
73         ...extraParams,
74       },
75     });
76   },
78   async promiseForceSigninURI(entrypoint, extraParams = {}) {
79     return this._buildURL("force_auth", {
80       extraParams: { entrypoint, service: SYNC_PARAM, ...extraParams },
81       addAccountIdentifiers: true,
82     });
83   },
85   async promiseManageURI(entrypoint, extraParams = {}) {
86     return this._buildURL("settings", {
87       extraParams: { entrypoint, ...extraParams },
88       addAccountIdentifiers: true,
89     });
90   },
92   async promiseChangeAvatarURI(entrypoint, extraParams = {}) {
93     return this._buildURL("settings/avatar/change", {
94       extraParams: { entrypoint, ...extraParams },
95       addAccountIdentifiers: true,
96     });
97   },
99   async promiseManageDevicesURI(entrypoint, extraParams = {}) {
100     return this._buildURL("settings/clients", {
101       extraParams: { entrypoint, ...extraParams },
102       addAccountIdentifiers: true,
103     });
104   },
106   async promiseConnectDeviceURI(entrypoint, extraParams = {}) {
107     return this._buildURL("connect_another_device", {
108       extraParams: { entrypoint, service: SYNC_PARAM, ...extraParams },
109       addAccountIdentifiers: true,
110     });
111   },
113   async promisePairingURI(extraParams = {}) {
114     return this._buildURL("pair", {
115       extraParams,
116       includeDefaultParams: false,
117     });
118   },
120   async promiseOAuthURI(extraParams = {}) {
121     return this._buildURL("oauth", {
122       extraParams,
123       includeDefaultParams: false,
124     });
125   },
127   async promiseMetricsFlowURI(entrypoint, extraParams = {}) {
128     return this._buildURL("metrics-flow", {
129       extraParams: { entrypoint, ...extraParams },
130       includeDefaultParams: false,
131     });
132   },
134   get defaultParams() {
135     return { context: lazy.CONTEXT_PARAM };
136   },
138   /**
139    * @param path should be parsable by the URL constructor first parameter.
140    * @param {bool} [options.includeDefaultParams] If true include the default search params.
141    * @param {Object.<string, string>} [options.extraParams] Additionnal search params.
142    * @param {bool} [options.addAccountIdentifiers] if true we add the current logged-in user uid and email to the search params.
143    */
144   async _buildURL(
145     path,
146     {
147       includeDefaultParams = true,
148       extraParams = {},
149       addAccountIdentifiers = false,
150     }
151   ) {
152     await this.ensureConfigured();
153     const url = new URL(path, lazy.ROOT_URL);
154     if (lazy.REQUIRES_HTTPS && url.protocol != "https:") {
155       throw new Error("Firefox Accounts server must use HTTPS");
156     }
157     const params = {
158       ...(includeDefaultParams ? this.defaultParams : null),
159       ...extraParams,
160     };
161     for (let [k, v] of Object.entries(params)) {
162       url.searchParams.append(k, v);
163     }
164     if (addAccountIdentifiers) {
165       const accountData = await this.getSignedInUser();
166       if (!accountData) {
167         return null;
168       }
169       url.searchParams.append("uid", accountData.uid);
170       url.searchParams.append("email", accountData.email);
171     }
172     return url.href;
173   },
175   async _buildURLFromString(href, extraParams = {}) {
176     const url = new URL(href);
177     for (let [k, v] of Object.entries(extraParams)) {
178       url.searchParams.append(k, v);
179     }
180     return url.href;
181   },
183   resetConfigURLs() {
184     let autoconfigURL = this.getAutoConfigURL();
185     if (!autoconfigURL) {
186       return;
187     }
188     // They have the autoconfig uri pref set, so we clear all the prefs that we
189     // will have initialized, which will leave them pointing at production.
190     for (let pref of CONFIG_PREFS) {
191       Services.prefs.clearUserPref(pref);
192     }
193     // Reset the webchannel.
194     lazy.EnsureFxAccountsWebChannel();
195   },
197   getAutoConfigURL() {
198     let pref = Services.prefs.getCharPref(
199       "identity.fxaccounts.autoconfig.uri",
200       ""
201     );
202     if (!pref) {
203       // no pref / empty pref means we don't bother here.
204       return "";
205     }
206     let rootURL = Services.urlFormatter.formatURL(pref);
207     if (rootURL.endsWith("/")) {
208       rootURL = rootURL.slice(0, -1);
209     }
210     return rootURL;
211   },
213   async ensureConfigured() {
214     let isSignedIn = !!(await this.getSignedInUser());
215     if (!isSignedIn) {
216       await this.updateConfigURLs();
217     }
218   },
220   // Returns true if this user is using the FxA "production" systems, false
221   // if using any other configuration, including self-hosting or the FxA
222   // non-production systems such as "dev" or "staging".
223   // It's typically used as a proxy for "is this likely to be a self-hosted
224   // user?", but it's named this way to make the implementation less
225   // surprising. As a result, it's fairly conservative and would prefer to have
226   // a false-negative than a false-position as it determines things which users
227   // might consider sensitive (notably, telemetry).
228   // Note also that while it's possible to self-host just sync and not FxA, we
229   // don't make that distinction - that's a self-hoster from the POV of this
230   // function.
231   isProductionConfig() {
232     // Specifically, if the autoconfig URLs, or *any* of the URLs that
233     // we consider configurable are modified, we assume self-hosted.
234     if (this.getAutoConfigURL()) {
235       return false;
236     }
237     for (let pref of CONFIG_PREFS) {
238       if (Services.prefs.prefHasUserValue(pref)) {
239         return false;
240       }
241     }
242     return true;
243   },
245   // Read expected client configuration from the fxa auth server
246   // (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration)
247   // and replace all the relevant our prefs with the information found there.
248   // This is only done before sign-in and sign-up, and even then only if the
249   // `identity.fxaccounts.autoconfig.uri` preference is set.
250   async updateConfigURLs() {
251     let rootURL = this.getAutoConfigURL();
252     if (!rootURL) {
253       return;
254     }
255     const config = await this.fetchConfigDocument(rootURL);
256     try {
257       // Update the prefs directly specified by the config.
258       let authServerBase = config.auth_server_base_url;
259       if (!authServerBase.endsWith("/v1")) {
260         authServerBase += "/v1";
261       }
262       Services.prefs.setCharPref(
263         "identity.fxaccounts.auth.uri",
264         authServerBase
265       );
266       Services.prefs.setCharPref(
267         "identity.fxaccounts.remote.oauth.uri",
268         config.oauth_server_base_url + "/v1"
269       );
270       // At the time of landing this, our servers didn't yet answer with pairing_server_base_uri.
271       // Remove this condition check once Firefox 68 is stable.
272       if (config.pairing_server_base_uri) {
273         Services.prefs.setCharPref(
274           "identity.fxaccounts.remote.pairing.uri",
275           config.pairing_server_base_uri
276         );
277       }
278       Services.prefs.setCharPref(
279         "identity.fxaccounts.remote.profile.uri",
280         config.profile_server_base_url + "/v1"
281       );
282       Services.prefs.setCharPref(
283         "identity.sync.tokenserver.uri",
284         config.sync_tokenserver_base_url + "/1.0/sync/1.5"
285       );
286       Services.prefs.setCharPref("identity.fxaccounts.remote.root", rootURL);
288       // Ensure the webchannel is pointed at the correct uri
289       lazy.EnsureFxAccountsWebChannel();
290     } catch (e) {
291       log.error(
292         "Failed to initialize configuration preferences from autoconfig object",
293         e
294       );
295       throw e;
296     }
297   },
299   // Read expected client configuration from the fxa auth server
300   // (or from the provided rootURL, if present) and return it as an object.
301   async fetchConfigDocument(rootURL = null) {
302     if (!rootURL) {
303       rootURL = lazy.ROOT_URL;
304     }
305     let configURL = rootURL + "/.well-known/fxa-client-configuration";
306     let request = new RESTRequest(configURL);
307     request.setHeader("Accept", "application/json");
309     // Catch and rethrow the error inline.
310     let resp = await request.get().catch(e => {
311       log.error(`Failed to get configuration object from "${configURL}"`, e);
312       throw e;
313     });
314     if (!resp.success) {
315       // Note: 'resp.body' is included with the error log below as we are not concerned
316       // that the body will contain PII, but if that changes it should be excluded.
317       log.error(
318         `Received HTTP response code ${resp.status} from configuration object request:
319         ${resp.body}`
320       );
321       throw new Error(
322         `HTTP status ${resp.status} from configuration object request`
323       );
324     }
325     log.debug("Got successful configuration response", resp.body);
326     try {
327       return JSON.parse(resp.body);
328     } catch (e) {
329       log.error(
330         `Failed to parse configuration preferences from ${configURL}`,
331         e
332       );
333       throw e;
334     }
335   },
337   // For test purposes, returns a Promise.
338   getSignedInUser() {
339     return lazy.fxAccounts.getSignedInUser();
340   },