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/. */
5 import { RESTRequest } from "resource://services-common/rest.sys.mjs";
7 const { log } = ChromeUtils.import(
8 "resource://gre/modules/FxAccountsCommon.js"
10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
14 XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
15 return ChromeUtils.importESModule(
16 "resource://gre/modules/FxAccounts.sys.mjs"
17 ).getFxAccountsSingleton();
20 ChromeUtils.defineESModuleGetters(lazy, {
21 EnsureFxAccountsWebChannel:
22 "resource://gre/modules/FxAccountsWebChannel.sys.mjs",
25 XPCOMUtils.defineLazyPreferenceGetter(
28 "identity.fxaccounts.remote.root"
30 XPCOMUtils.defineLazyPreferenceGetter(
33 "identity.fxaccounts.contextParam"
35 XPCOMUtils.defineLazyPreferenceGetter(
38 "identity.fxaccounts.allowHttp",
44 const CONFIG_PREFS = [
45 "identity.fxaccounts.remote.root",
46 "identity.fxaccounts.auth.uri",
47 "identity.fxaccounts.remote.oauth.uri",
48 "identity.fxaccounts.remote.profile.uri",
49 "identity.fxaccounts.remote.pairing.uri",
50 "identity.sync.tokenserver.uri",
52 const SYNC_PARAM = "sync";
54 export var FxAccountsConfig = {
55 async promiseEmailURI(email, entrypoint, extraParams = {}) {
56 return this._buildURL("", {
57 extraParams: { entrypoint, email, service: SYNC_PARAM, ...extraParams },
61 async promiseConnectAccountURI(entrypoint, extraParams = {}) {
62 return this._buildURL("", {
72 async promiseForceSigninURI(entrypoint, extraParams = {}) {
73 return this._buildURL("force_auth", {
74 extraParams: { entrypoint, service: SYNC_PARAM, ...extraParams },
75 addAccountIdentifiers: true,
79 async promiseManageURI(entrypoint, extraParams = {}) {
80 return this._buildURL("settings", {
81 extraParams: { entrypoint, ...extraParams },
82 addAccountIdentifiers: true,
86 async promiseChangeAvatarURI(entrypoint, extraParams = {}) {
87 return this._buildURL("settings/avatar/change", {
88 extraParams: { entrypoint, ...extraParams },
89 addAccountIdentifiers: true,
93 async promiseManageDevicesURI(entrypoint, extraParams = {}) {
94 return this._buildURL("settings/clients", {
95 extraParams: { entrypoint, ...extraParams },
96 addAccountIdentifiers: true,
100 async promiseConnectDeviceURI(entrypoint, extraParams = {}) {
101 return this._buildURL("connect_another_device", {
102 extraParams: { entrypoint, service: SYNC_PARAM, ...extraParams },
103 addAccountIdentifiers: true,
107 async promisePairingURI(extraParams = {}) {
108 return this._buildURL("pair", {
110 includeDefaultParams: false,
114 async promiseOAuthURI(extraParams = {}) {
115 return this._buildURL("oauth", {
117 includeDefaultParams: false,
121 async promiseMetricsFlowURI(entrypoint, extraParams = {}) {
122 return this._buildURL("metrics-flow", {
123 extraParams: { entrypoint, ...extraParams },
124 includeDefaultParams: false,
128 get defaultParams() {
129 return { context: lazy.CONTEXT_PARAM };
133 * @param path should be parsable by the URL constructor first parameter.
134 * @param {bool} [options.includeDefaultParams] If true include the default search params.
135 * @param {Object.<string, string>} [options.extraParams] Additionnal search params.
136 * @param {bool} [options.addAccountIdentifiers] if true we add the current logged-in user uid and email to the search params.
141 includeDefaultParams = true,
143 addAccountIdentifiers = false,
146 await this.ensureConfigured();
147 const url = new URL(path, lazy.ROOT_URL);
148 if (lazy.REQUIRES_HTTPS && url.protocol != "https:") {
149 throw new Error("Firefox Accounts server must use HTTPS");
152 ...(includeDefaultParams ? this.defaultParams : null),
155 for (let [k, v] of Object.entries(params)) {
156 url.searchParams.append(k, v);
158 if (addAccountIdentifiers) {
159 const accountData = await this.getSignedInUser();
163 url.searchParams.append("uid", accountData.uid);
164 url.searchParams.append("email", accountData.email);
169 async _buildURLFromString(href, extraParams = {}) {
170 const url = new URL(href);
171 for (let [k, v] of Object.entries(extraParams)) {
172 url.searchParams.append(k, v);
178 let autoconfigURL = this.getAutoConfigURL();
179 if (!autoconfigURL) {
182 // They have the autoconfig uri pref set, so we clear all the prefs that we
183 // will have initialized, which will leave them pointing at production.
184 for (let pref of CONFIG_PREFS) {
185 Services.prefs.clearUserPref(pref);
187 // Reset the webchannel.
188 lazy.EnsureFxAccountsWebChannel();
192 let pref = Services.prefs.getCharPref(
193 "identity.fxaccounts.autoconfig.uri",
197 // no pref / empty pref means we don't bother here.
200 let rootURL = Services.urlFormatter.formatURL(pref);
201 if (rootURL.endsWith("/")) {
202 rootURL = rootURL.slice(0, -1);
207 async ensureConfigured() {
208 let isSignedIn = !!(await this.getSignedInUser());
210 await this.updateConfigURLs();
214 // Returns true if this user is using the FxA "production" systems, false
215 // if using any other configuration, including self-hosting or the FxA
216 // non-production systems such as "dev" or "staging".
217 // It's typically used as a proxy for "is this likely to be a self-hosted
218 // user?", but it's named this way to make the implementation less
219 // surprising. As a result, it's fairly conservative and would prefer to have
220 // a false-negative than a false-position as it determines things which users
221 // might consider sensitive (notably, telemetry).
222 // Note also that while it's possible to self-host just sync and not FxA, we
223 // don't make that distinction - that's a self-hoster from the POV of this
225 isProductionConfig() {
226 // Specifically, if the autoconfig URLs, or *any* of the URLs that
227 // we consider configurable are modified, we assume self-hosted.
228 if (this.getAutoConfigURL()) {
231 for (let pref of CONFIG_PREFS) {
232 if (Services.prefs.prefHasUserValue(pref)) {
239 // Read expected client configuration from the fxa auth server
240 // (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration)
241 // and replace all the relevant our prefs with the information found there.
242 // This is only done before sign-in and sign-up, and even then only if the
243 // `identity.fxaccounts.autoconfig.uri` preference is set.
244 async updateConfigURLs() {
245 let rootURL = this.getAutoConfigURL();
249 const config = await this.fetchConfigDocument(rootURL);
251 // Update the prefs directly specified by the config.
252 let authServerBase = config.auth_server_base_url;
253 if (!authServerBase.endsWith("/v1")) {
254 authServerBase += "/v1";
256 Services.prefs.setCharPref(
257 "identity.fxaccounts.auth.uri",
260 Services.prefs.setCharPref(
261 "identity.fxaccounts.remote.oauth.uri",
262 config.oauth_server_base_url + "/v1"
264 // At the time of landing this, our servers didn't yet answer with pairing_server_base_uri.
265 // Remove this condition check once Firefox 68 is stable.
266 if (config.pairing_server_base_uri) {
267 Services.prefs.setCharPref(
268 "identity.fxaccounts.remote.pairing.uri",
269 config.pairing_server_base_uri
272 Services.prefs.setCharPref(
273 "identity.fxaccounts.remote.profile.uri",
274 config.profile_server_base_url + "/v1"
276 Services.prefs.setCharPref(
277 "identity.sync.tokenserver.uri",
278 config.sync_tokenserver_base_url + "/1.0/sync/1.5"
280 Services.prefs.setCharPref("identity.fxaccounts.remote.root", rootURL);
282 // Ensure the webchannel is pointed at the correct uri
283 lazy.EnsureFxAccountsWebChannel();
286 "Failed to initialize configuration preferences from autoconfig object",
293 // Read expected client configuration from the fxa auth server
294 // (or from the provided rootURL, if present) and return it as an object.
295 async fetchConfigDocument(rootURL = null) {
297 rootURL = lazy.ROOT_URL;
299 let configURL = rootURL + "/.well-known/fxa-client-configuration";
300 let request = new RESTRequest(configURL);
301 request.setHeader("Accept", "application/json");
303 // Catch and rethrow the error inline.
304 let resp = await request.get().catch(e => {
305 log.error(`Failed to get configuration object from "${configURL}"`, e);
309 // Note: 'resp.body' is included with the error log below as we are not concerned
310 // that the body will contain PII, but if that changes it should be excluded.
312 `Received HTTP response code ${resp.status} from configuration object request:
316 `HTTP status ${resp.status} from configuration object request`
319 log.debug("Got successful configuration response", resp.body);
321 return JSON.parse(resp.body);
324 `Failed to parse configuration preferences from ${configURL}`,
331 // For test purposes, returns a Promise.
333 return lazy.fxAccounts.getSignedInUser();