tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

FxAccountsConfig.sys.mjs (10988B)


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