Bug 1427674 - Unify FxA content server URL preferences. r?markh,tcsc draft
authorEdouard Oger <eoger@fastmail.com>
Tue, 30 Jan 2018 13:21:35 -0500
changeset 752453 abfc93b85b6873c8b3244e08411601ca0ef54d2c
parent 752448 1144d18b470419404a43761d099b3a4e020d79fc
push id98273
push userbmo:eoger@fastmail.com
push dateThu, 08 Feb 2018 09:18:18 +0000
reviewersmarkh, tcsc
bugs1427674
milestone60.0a1
Bug 1427674 - Unify FxA content server URL preferences. r?markh,tcsc MozReview-Commit-ID: 3zhHGAzQr0R
browser/app/profile/firefox.js
browser/base/content/browser-sync.js
browser/components/customizableui/test/browser_synced_tabs_menu.js
browser/components/nsBrowserGlue.js
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/sync.js
browser/components/uitour/UITour.jsm
browser/components/uitour/test/browser_UITour_sync.js
browser/extensions/activity-stream/lib/SnippetsFeed.jsm
browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsConfig.jsm
services/fxaccounts/FxAccountsWebChannel.jsm
services/fxaccounts/tests/browser/browser_device_connected.js
services/fxaccounts/tests/xpcshell/test_accounts.js
services/fxaccounts/tests/xpcshell/test_accounts_config.js
services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
services/fxaccounts/tests/xpcshell/test_profile.js
services/fxaccounts/tests/xpcshell/xpcshell.ini
testing/profiles/prefs_general.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1404,55 +1404,31 @@ pref("geo.provider.use_gpsd", true);
 pref("network.disable.ipc.security", true);
 
 // CustomizableUI debug logging.
 pref("browser.uiCustomization.debug", false);
 
 // CustomizableUI state of the browser's user interface
 pref("browser.uiCustomization.state", "");
 
-// The remote content URL shown for FxA signup. Must use HTTPS.
-pref("identity.fxaccounts.remote.signup.uri", "https://accounts.firefox.com/signup?service=sync&context=fx_desktop_v3");
-
-// The URL where remote content that forces re-authentication for Firefox Accounts
-// should be fetched.  Must use HTTPS.
-pref("identity.fxaccounts.remote.force_auth.uri", "https://accounts.firefox.com/force_auth?service=sync&context=fx_desktop_v3");
-
-// The remote content URL shown for signin in. Must use HTTPS.
-pref("identity.fxaccounts.remote.signin.uri", "https://accounts.firefox.com/signin?service=sync&context=fx_desktop_v3");
-
-// The remote content URL shown for the email (FxA determines if we show sign-in or sign-up) endpoint. Must use HTTPS.
-pref("identity.fxaccounts.remote.email.uri", "https://accounts.firefox.com/?service=sync&context=fx_desktop_v3&action=email");
+// The remote FxA root content URL. Must use HTTPS.
+pref("identity.fxaccounts.remote.root", "https://accounts.firefox.com/");
 
-// The remote content URL where FxAccountsWebChannel messages originate.
-pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com/");
-
-// The value of the context query parameter passed in some fxa requests when config
-// discovery is enabled.
+// The value of the context query parameter passed in fxa requests.
 pref("identity.fxaccounts.contextParam", "fx_desktop_v3");
 
-// The URL we take the user to when they opt to "manage" their Firefox Account.
-// Note that this will always need to be in the same TLD as the
-// "identity.fxaccounts.remote.signup.uri" pref.
-pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings?service=sync&context=fx_desktop_v3");
-
-// The URL of the FxA device manager page
-pref("identity.fxaccounts.settings.devices.uri", "https://accounts.firefox.com/settings/clients?service=sync&context=fx_desktop_v3");
-
 // The remote URL of the FxA Profile Server
 pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
 
 // The remote URL of the FxA OAuth Server
 pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
 
 // Token server used by the FxA Sync identity.
 pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
 
-// The URL to a page that explains how to connect another device to Sync.
-pref("identity.fxaccounts.remote.connectdevice.uri", "https://accounts.firefox.com/connect_another_device?service=sync&context=fx_desktop_v3");
 // URLs for promo links to mobile browsers. Note that consumers are expected to
 // append a value for utm_campaign.
 pref("identity.mobilepromo.android", "https://www.mozilla.org/firefox/android/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=");
 pref("identity.mobilepromo.ios", "https://www.mozilla.org/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=");
 
 // Migrate any existing Firefox Account data from the default profile to the
 // Developer Edition profile.
 #ifdef MOZ_DEV_EDITION
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -2,16 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // This file is loaded into the browser window scope.
 /* eslint-env mozilla/browser-window */
 
 ChromeUtils.import("resource://services-sync/UIState.jsm");
 
+ChromeUtils.defineModuleGetter(this, "FxAccounts",
+  "resource://gre/modules/FxAccounts.jsm");
 ChromeUtils.defineModuleGetter(this, "EnsureFxAccountsWebChannel",
   "resource://gre/modules/FxAccountsWebChannel.jsm");
 ChromeUtils.defineModuleGetter(this, "Weave",
   "resource://services-sync/main.js");
 
 const MIN_STATUS_ANIMATION_DURATION = 1600;
 
 var gSync = {
@@ -86,18 +88,16 @@ var gSync = {
         "services.sync.engine.tabs.filteredUrls", null, null, rx => {
           try {
             return new RegExp(rx, "i");
           } catch (e) {
             Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
             return null;
           }
         });
-    XPCOMUtils.defineLazyPreferenceGetter(this, "FXA_CONNECT_DEVICE_URI",
-        "identity.fxaccounts.remote.connectdevice.uri");
     XPCOMUtils.defineLazyPreferenceGetter(this, "PRODUCT_INFO_BASE_URL",
         "app.productInfo.baseURL");
   },
 
   _maybeUpdateUIState() {
     // Update the UI.
     if (UIState.isReady()) {
       const state = UIState.get();
@@ -282,35 +282,34 @@ var gSync = {
       this.openPrefs("menupanel", "fxa");
       break;
     }
 
     PanelUI.hide();
   },
 
   async openSignInAgainPage(entryPoint) {
-    const url = await fxAccounts.promiseAccountsForceSigninURI(entryPoint);
+    const url = await FxAccounts.config.promiseForceSigninURI(entryPoint);
     switchToTabHavingURI(url, true, {
       replaceQueryString: true,
       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
     });
   },
 
   async openDevicesManagementPage(entryPoint) {
-    let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
+    let url = await FxAccounts.config.promiseManageDevicesURI(entryPoint);
     switchToTabHavingURI(url, true, {
       replaceQueryString: true,
       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
     });
   },
 
-  openConnectAnotherDevice(entryPoint) {
-    let url = new URL(this.FXA_CONNECT_DEVICE_URI);
-    url.searchParams.append("entrypoint", entryPoint);
-    openUILinkIn(url.href, "tab");
+  async openConnectAnotherDevice(entryPoint) {
+    const url = await FxAccounts.config.promiseConnectDeviceURI(entryPoint);
+    openUILinkIn(url, "tab");
   },
 
   openSendToDevicePromo() {
     let url = this.PRODUCT_INFO_BASE_URL;
     url += "send-tabs/?utm_source=" + Services.appinfo.name.toLowerCase();
     switchToTabHavingURI(url, true, { replaceQueryString: true });
   },
 
--- a/browser/components/customizableui/test/browser_synced_tabs_menu.js
+++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 requestLongerTimeout(2);
 
+ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
 let {SyncedTabs} = ChromeUtils.import("resource://services-sync/SyncedTabs.jsm", {});
 let {UIState} = ChromeUtils.import("resource://services-sync/UIState.jsm", {});
 
 ChromeUtils.defineModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
 
 // These are available on the widget implementation, but it seems impossible
 // to grab that impl at runtime.
 const DECKINDEX_TABS = 0;
@@ -35,36 +36,41 @@ let mockedInternal = {
   get isConfiguredToSyncTabs() { return true; },
   getTabClients() { return Promise.resolve([]); },
   syncTabs() { return Promise.resolve(); },
   hasSyncedThisSession: false,
 };
 
 
 add_task(async function setup() {
+  const getSignedInUser = FxAccounts.config.getSignedInUser;
+  FxAccounts.config.getSignedInUser = async () => Promise.resolve({uid: "uid", email: "foo@bar.com"});
+  Services.prefs.setCharPref("identity.fxaccounts.remote.root", "https://example.com/");
+
   let oldInternal = SyncedTabs._internal;
   SyncedTabs._internal = mockedInternal;
 
   let origNotifyStateUpdated = UIState._internal.notifyStateUpdated;
   // Sync start-up will interfere with our tests, don't let UIState send UI updates.
   UIState._internal.notifyStateUpdated = () => {};
 
   // Force gSync initialization
   gSync.init();
 
   registerCleanupFunction(() => {
+    FxAccounts.config.getSignedInUser = getSignedInUser;
+    Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
     UIState._internal.notifyStateUpdated = origNotifyStateUpdated;
     SyncedTabs._internal = oldInternal;
   });
 });
 
 // The test expects the about:preferences#sync page to open in the current tab
 async function openPrefsFromMenuPanel(expectedPanelId, entryPoint) {
   info("Check Sync button functionality");
-  Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
   CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
 
   await waitForOverflowButtonShown();
 
   // check the button's functionality
   await document.getElementById("nav-bar").overflowable.show();
 
   if (entryPoint == "uitour") {
@@ -116,17 +122,16 @@ async function openPrefsFromMenuPanel(ex
 
 function hideOverflow() {
   let panelHidePromise = promiseOverflowHidden(window);
   PanelUI.overflowPanel.hidePopup();
   return panelHidePromise;
 }
 
 async function asyncCleanup() {
-  Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
   // reset the panel UI to the default state
   await resetCustomization();
   ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
 
   // restore the tabs
   BrowserTestUtils.addTab(gBrowser, initialLocation);
   gBrowser.removeTab(newTab);
   UITour.tourBrowsersByWindow.delete(window);
@@ -149,44 +154,31 @@ add_task(asyncCleanup);
 // When Sync is configured in a "needs reauthentication" state.
 add_task(async function() {
   gSync.updateAllUI({ status: UIState.STATUS_LOGIN_FAILED, email: "foo@bar.com" });
   await openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs");
 });
 
 // Test the Connect Another Device button
 add_task(async function() {
-  Services.prefs.setCharPref("identity.fxaccounts.remote.connectdevice.uri", "http://example.com/connectdevice");
-
   gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" });
 
   let button = document.getElementById("PanelUI-remotetabs-connect-device-button");
   ok(button, "found the button");
 
   await document.getElementById("nav-bar").overflowable.show();
+  let expectedUrl = "https://example.com/connect_another_device?service=sync&context=" +
+                    "fx_desktop_v3&entrypoint=synced-tabs&uid=uid&email=foo%40bar.com";
+  let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedUrl);
   button.click();
   // the panel should have been closed.
   ok(!isOverflowOpen(), "click closed the panel");
-  // should be a new tab - wait for the load.
-  is(gBrowser.tabs.length, 2, "there's a new tab");
-  await new Promise(resolve => {
-    if (gBrowser.selectedBrowser.currentURI.spec == "about:blank") {
-      BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(resolve);
-      return;
-    }
-    // the new tab has already transitioned away from about:blank so we
-    // are good to go.
-    resolve();
-  });
+  await promiseTabOpened;
 
-  let expectedUrl = `http://example.com/connectdevice?entrypoint=synced-tabs`;
-  is(gBrowser.selectedBrowser.currentURI.spec, expectedUrl, "correct URL");
   gBrowser.removeTab(gBrowser.selectedTab);
-
-  Services.prefs.clearUserPref("identity.fxaccounts.remote.connectdevice.uri");
 });
 
 // Test the "Sync Now" button
 add_task(async function() {
   gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" });
 
   await document.getElementById("nav-bar").overflowable.show();
   let tabsUpdatedPromise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -32,16 +32,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   CustomizableUI: "resource:///modules/CustomizableUI.jsm",
   DateTimePickerHelper: "resource://gre/modules/DateTimePickerHelper.jsm",
   DirectoryLinksProvider: "resource:///modules/DirectoryLinksProvider.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   Feeds: "resource:///modules/Feeds.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   FileSource: "resource://gre/modules/L10nRegistry.jsm",
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
+  FxAccounts: "resource://gre/modules/FxAccounts.jsm",
   HybridContentTelemetry: "resource://gre/modules/HybridContentTelemetry.jsm",
   Integration: "resource://gre/modules/Integration.jsm",
   L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
   LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   LoginHelper: "resource://gre/modules/LoginHelper.jsm",
   LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
@@ -231,17 +232,16 @@ function BrowserGlue() {
                                      "@mozilla.org/widget/idleservice;1",
                                      "nsIIdleService");
 
   XPCOMUtils.defineLazyGetter(this, "_distributionCustomizer", function() {
                                 ChromeUtils.import("resource:///modules/distribution.js");
                                 return new DistributionCustomizer();
                               });
 
-  ChromeUtils.defineModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
   XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", "@mozilla.org/alerts-service;1", "nsIAlertsService");
 
   this._init();
 }
 
 BrowserGlue.prototype = {
   _saveSession: false,
   _migrationImportsDefaultBookmarks: false,
@@ -400,20 +400,16 @@ BrowserGlue.prototype = {
           this._distributionCustomizer.applyCustomizations();
           // To apply distribution bookmarks use "places-init-complete".
         } else if (data == "force-places-init") {
           this._initPlaces(false);
         } else if (data == "smart-bookmarks-init") {
           this.ensurePlacesDefaultQueriesInitialized().then(() => {
             Services.obs.notifyObservers(null, "test-smart-bookmarks-done");
           });
-        } else if (data == "mock-fxaccounts") {
-          Object.defineProperty(this, "fxAccounts", {
-            value: subject.wrappedJSObject
-          });
         } else if (data == "mock-alerts-service") {
           Object.defineProperty(this, "AlertsService", {
             value: subject.wrappedJSObject
           });
         } else if (data == "places-browser-init-complete") {
           if (this._placesBrowserInitComplete) {
             Services.obs.notifyObservers(null, "places-browser-init-complete");
           }
@@ -2642,17 +2638,17 @@ BrowserGlue.prototype = {
     let title = accountsBundle.GetStringFromName("deviceConnectedTitle");
     let body = accountsBundle.formatStringFromName("deviceConnectedBody" +
                                                    (deviceName ? "" : ".noDeviceName"),
                                                    [deviceName], 1);
 
     let clickCallback = async (subject, topic, data) => {
       if (topic != "alertclickcallback")
         return;
-      let url = await this.fxAccounts.promiseAccountsManageDevicesURI("device-connected-notification");
+      let url = await FxAccounts.config.promiseManageDevicesURI("device-connected-notification");
       let win = RecentWindow.getMostRecentBrowserWindow({private: false});
       if (!win) {
         this._openURLInNewWindow(url);
       } else {
         win.gBrowser.addTab(url);
       }
     };
 
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -94,16 +94,18 @@ const ICON_URL_APP = AppConstants.platfo
 const APP_ICON_ATTR_NAME = "appHandlerIcon";
 
 ChromeUtils.defineModuleGetter(this, "OS",
   "resource://gre/modules/osfile.jsm");
 
 if (AppConstants.MOZ_DEV_EDITION) {
   ChromeUtils.defineModuleGetter(this, "fxAccounts",
     "resource://gre/modules/FxAccounts.jsm");
+  ChromeUtils.defineModuleGetter(this, "FxAccounts",
+    "resource://gre/modules/FxAccounts.jsm");
 }
 
 Preferences.addAll([
   // Startup
   { id: "browser.startup.page", type: "int" },
   { id: "browser.startup.homepage", type: "wstring" },
 
   { id: "pref.browser.homepage.disable_button.current_page", type: "bool" },
@@ -710,17 +712,17 @@ var gMainPane = {
       return;
     }
     const user = await fxAccounts.getSignedInUser();
     if (user) {
       // We have a user, open Sync preferences in the same tab
       win.openUILinkIn("about:preferences#sync", "current");
       return;
     }
-    let url = await fxAccounts.promiseAccountsSignInURI("dev-edition-setup");
+    let url = await FxAccounts.config.promiseSignInURI("dev-edition-setup");
     let accountsTab = win.gBrowser.addTab(url);
     win.gBrowser.selectedTab = accountsTab;
   },
 
   // HOME PAGE
 
   /*
    * Preferences:
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -1,24 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* import-globals-from preferences.js */
 
 ChromeUtils.import("resource://services-sync/main.js");
+ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
   return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js", {});
 });
 
-ChromeUtils.defineModuleGetter(this, "fxAccounts",
-  "resource://gre/modules/FxAccounts.jsm");
-
 ChromeUtils.defineModuleGetter(this, "UIState",
   "resource://services-sync/UIState.jsm");
 
 const FXA_PAGE_LOGGED_OUT = 0;
 const FXA_PAGE_LOGGED_IN = 1;
 
 // Indexes into the "login status" deck.
 // We are in a successful verified state - everything should work!
@@ -141,28 +139,28 @@ var gSyncPane = {
 
     // Links for mobile devices before the user is logged in.
     let url = Services.prefs.getCharPref("identity.mobilepromo.android") + "sync-preferences";
     document.getElementById("fxaMobilePromo-android").setAttribute("href", url);
     url = Services.prefs.getCharPref("identity.mobilepromo.ios") + "sync-preferences";
     document.getElementById("fxaMobilePromo-ios").setAttribute("href", url);
 
     // Links for mobile devices shown after the user is logged in.
-    fxAccounts.promiseAccountsConnectDeviceURI(this._getEntryPoint()).then(connectURI => {
+    FxAccounts.config.promiseConnectDeviceURI(this._getEntryPoint()).then(connectURI => {
       document.getElementById("mobilePromo-singledevice").setAttribute("href", connectURI);
     });
 
-    fxAccounts.promiseAccountsManageDevicesURI(this._getEntryPoint()).then(manageURI => {
+    FxAccounts.config.promiseManageDevicesURI(this._getEntryPoint()).then(manageURI => {
       document.getElementById("mobilePromo-multidevice").setAttribute("href", manageURI);
     });
 
     document.getElementById("tosPP-small-ToS").setAttribute("href", Weave.Svc.Prefs.get("fxa.termsURL"));
     document.getElementById("tosPP-small-PP").setAttribute("href", Weave.Svc.Prefs.get("fxa.privacyURL"));
 
-    fxAccounts.promiseAccountsSignUpURI(this._getEntryPoint()).then(signUpURI => {
+    FxAccounts.config.promiseSignUpURI(this._getEntryPoint()).then(signUpURI => {
       document.getElementById("noFxaSignUp").setAttribute("href", signUpURI);
     });
 
     this.updateWeavePrefs();
 
     // Notify observers that the UI is now ready
     Services.obs.notifyObservers(window, "sync-pane-loaded");
   },
@@ -321,17 +319,17 @@ var gSyncPane = {
         if (profileImageElement.style.listStyleImage === bgImage) {
           profileImageElement.style.removeProperty("list-style-image");
         }
       };
       img.src = state.avatarURL;
     }
     // The "manage account" link embeds the uid, so we need to update this
     // if the account state changes.
-    fxAccounts.promiseAccountsManageURI(this._getEntryPoint()).then(accountsManageURI => {
+    FxAccounts.config.promiseManageURI(this._getEntryPoint()).then(accountsManageURI => {
       document.getElementById("verifiedManage").setAttribute("href", accountsManageURI);
     });
     let isUnverified = state.status == UIState.STATUS_NOT_VERIFIED;
     // The mobile promo links - which one is shown depends on the number of devices.
     let isMultiDevice = Weave.Service.clientsEngine.stats.numClients > 1;
     document.getElementById("mobilePromo-singledevice").hidden = isUnverified || isMultiDevice;
     document.getElementById("mobilePromo-multidevice").hidden = isUnverified || !isMultiDevice;
   },
@@ -357,43 +355,43 @@ var gSyncPane = {
       .getInterface(Ci.nsIWebNavigation)
       .QueryInterface(Ci.nsIDocShell)
       .chromeEventHandler;
     // And tell it to load our URL.
     browser.loadURI(url);
   },
 
   async signIn() {
-    const url = await fxAccounts.promiseAccountsSignInURI(this._getEntryPoint());
+    const url = await FxAccounts.config.promiseSignInURI(this._getEntryPoint());
     this.replaceTabWithUrl(url);
   },
 
   async reSignIn() {
     // There's a bit of an edge-case here - we might be forcing reauth when we've
     // lost the FxA account data - in which case we'll not get a URL as the re-auth
     // URL embeds account info and the server endpoint complains if we don't
     // supply it - So we just use the regular "sign in" URL in that case.
     let entryPoint = this._getEntryPoint();
-    const url = (await fxAccounts.promiseAccountsForceSigninURI(entryPoint)) ||
-                (await fxAccounts.promiseAccountsSignInURI(entryPoint));
+    const url = (await FxAccounts.config.promiseForceSigninURI(entryPoint)) ||
+                (await FxAccounts.config.promiseSignInURI(entryPoint));
     this.replaceTabWithUrl(url);
   },
 
 
   clickOrSpaceOrEnterPressed(event) {
     // Note: charCode is deprecated, but 'char' not yet implemented.
     // Replace charCode with char when implemented, see Bug 680830
     return ((event.type == "click" && event.button == 0) ||
       (event.type == "keypress" &&
         (event.charCode == KeyEvent.DOM_VK_SPACE || event.keyCode == KeyEvent.DOM_VK_RETURN)));
   },
 
   openChangeProfileImage(event) {
     if (this.clickOrSpaceOrEnterPressed(event)) {
-      fxAccounts.promiseAccountsChangeProfileURI(this._getEntryPoint(), "avatar")
+      FxAccounts.config.promiseChangeAvatarURI(this._getEntryPoint())
         .then(url => {
           this.openContentInBrowser(url, {
             replaceQueryString: true,
             triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
           });
         });
       // Prevent page from scrolling on the space key.
       event.preventDefault();
@@ -404,17 +402,17 @@ var gSyncPane = {
     if (this.clickOrSpaceOrEnterPressed(event)) {
       this.manageFirefoxAccount();
       // Prevent page from scrolling on the space key.
       event.preventDefault();
     }
   },
 
   manageFirefoxAccount() {
-    fxAccounts.promiseAccountsManageURI(this._getEntryPoint())
+    FxAccounts.config.promiseManageURI(this._getEntryPoint())
       .then(url => {
         this.openContentInBrowser(url, {
           replaceQueryString: true,
           triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
         });
       });
   },
 
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -13,17 +13,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 Cu.importGlobalProperties(["URL"]);
 
 ChromeUtils.defineModuleGetter(this, "BrowserUITelemetry",
   "resource:///modules/BrowserUITelemetry.jsm");
 ChromeUtils.defineModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
-ChromeUtils.defineModuleGetter(this, "fxAccounts",
+ChromeUtils.defineModuleGetter(this, "FxAccounts",
   "resource://gre/modules/FxAccounts.jsm");
 ChromeUtils.defineModuleGetter(this, "LightweightThemeManager",
   "resource://gre/modules/LightweightThemeManager.jsm");
 ChromeUtils.defineModuleGetter(this, "PageActions",
   "resource:///modules/PageActions.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "ProfileAge",
@@ -546,43 +546,44 @@ this.UITour = {
           return false;
         }
         window.openPreferences(data.pane, { origin: "UITour" });
         break;
       }
 
       case "showFirefoxAccounts": {
         Promise.resolve().then(() => {
-          return data.email ? fxAccounts.promiseAccountsEmailURI(data.email, "uitour") :
-                              fxAccounts.promiseAccountsSignUpURI("uitour");
+          return data.email ? FxAccounts.config.promiseEmailURI(data.email, "uitour") :
+                              FxAccounts.config.promiseSignUpURI("uitour");
         }).then(uri => {
           const url = new URL(uri);
           // Call our helper to validate extraURLCampaignParams and populate URLSearchParams
           if (!this._populateCampaignParams(url, data.extraURLCampaignParams)) {
             log.warn("showFirefoxAccounts: invalid campaign args specified");
             return;
           }
 
           // We want to replace the current tab.
           browser.loadURI(url.href);
         });
         break;
       }
 
       case "showConnectAnotherDevice": {
-        const url = new URL(Services.prefs.getCharPref("identity.fxaccounts.remote.connectdevice.uri"));
-        url.searchParams.append("entrypoint", "uitour");
-        // Call our helper to validate extraURLCampaignParams and populate URLSearchParams
-        if (!this._populateCampaignParams(url, data.extraURLCampaignParams)) {
-          log.warn("showConnectAnotherDevice: invalid campaign args specified");
-          return false;
-        }
+        FxAccounts.config.promiseConnectDeviceURI("uitour").then(uri => {
+          const url = new URL(uri);
+          // Call our helper to validate extraURLCampaignParams and populate URLSearchParams
+          if (!this._populateCampaignParams(url, data.extraURLCampaignParams)) {
+            log.warn("showConnectAnotherDevice: invalid campaign args specified");
+            return;
+          }
 
-        // We want to replace the current tab.
-        browser.loadURI(url.href);
+          // We want to replace the current tab.
+          browser.loadURI(url.href);
+        });
         break;
       }
 
       case "resetFirefox": {
         // Open a reset profile dialog window.
         if (ResetProfile.resetSupported()) {
           ResetProfile.openConfirmationDialog(window);
         }
--- a/browser/components/uitour/test/browser_UITour_sync.js
+++ b/browser/components/uitour/test/browser_UITour_sync.js
@@ -1,27 +1,24 @@
 "use strict";
 
 var gTestTab;
 var gContentAPI;
 var gContentWindow;
 
 registerCleanupFunction(function() {
-  Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
-  Services.prefs.clearUserPref("identity.fxaccounts.remote.email.uri");
+  Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
   Services.prefs.clearUserPref("services.sync.username");
 });
 
 add_task(setup_UITourTest);
 
 add_task(async function setup() {
-  Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri",
-                             "https://example.com/signup");
-  Services.prefs.setCharPref("identity.fxaccounts.remote.email.uri",
-                             "https://example.com/?action=email");
+  Services.prefs.setCharPref("identity.fxaccounts.remote.root",
+                             "https://example.com");
 });
 
 add_UITour_task(async function test_checkSyncSetup_disabled() {
   let result = await getConfigurationPromise("sync");
   is(result.setup, false, "Sync shouldn't be setup by default");
 });
 
 add_UITour_task(async function test_checkSyncSetup_enabled() {
@@ -58,45 +55,45 @@ add_UITour_task(async function test_chec
   is(result.totalDevices, 0, "totalDevices should be 0");
 });
 
 // The showFirefoxAccounts API is sync related, so we test that here too...
 add_UITour_task(async function test_firefoxAccountsNoParams() {
   info("Load https://accounts.firefox.com");
   await gContentAPI.showFirefoxAccounts();
   await BrowserTestUtils.browserLoaded(gTestTab.linkedBrowser, false,
-                                       "https://example.com/signup?entrypoint=uitour");
+                                       "https://example.com/signup?service=sync&context=fx_desktop_v3&entrypoint=uitour");
 });
 
 
 add_UITour_task(async function test_firefoxAccountsValidParams() {
   info("Load https://accounts.firefox.com");
   await gContentAPI.showFirefoxAccounts({ utm_foo: "foo", utm_bar: "bar" });
   await BrowserTestUtils.browserLoaded(gTestTab.linkedBrowser, false,
-                                       "https://example.com/signup?entrypoint=uitour&utm_foo=foo&utm_bar=bar");
+                                       "https://example.com/signup?service=sync&context=fx_desktop_v3&entrypoint=uitour&utm_foo=foo&utm_bar=bar");
 });
 
 add_UITour_task(async function test_firefoxAccountsWithEmail() {
   info("Load https://accounts.firefox.com");
   await gContentAPI.showFirefoxAccounts(null, "foo@bar.com");
   await BrowserTestUtils.browserLoaded(gTestTab.linkedBrowser, false,
-                                       "https://example.com/?action=email&email=foo%40bar.com&entrypoint=uitour");
+                                       "https://example.com/?service=sync&context=fx_desktop_v3&entrypoint=uitour&email=foo%40bar.com");
 });
 
 add_UITour_task(async function test_firefoxAccountsNonAlphaValue() {
   // All characters in the value are allowed, but they must be automatically escaped.
   // (we throw a unicode character in there too - it's not auto-utf8 encoded,
   // but that's ok, so long as it is escaped correctly.)
   let value = "foo& /=?:\\\xa9";
   // encodeURIComponent encodes spaces to %20 but we want "+"
   let expected = encodeURIComponent(value).replace(/%20/g, "+");
   info("Load https://accounts.firefox.com");
   await gContentAPI.showFirefoxAccounts({ utm_foo: value });
   await BrowserTestUtils.browserLoaded(gTestTab.linkedBrowser, false,
-                                       "https://example.com/signup?entrypoint=uitour&utm_foo=" + expected);
+                                       "https://example.com/signup?service=sync&context=fx_desktop_v3&entrypoint=uitour&utm_foo=" + expected);
 });
 
 // A helper to check the request was ignored due to invalid params.
 async function checkFxANotLoaded() {
   try {
     await waitForConditionPromise(() => {
       return gBrowser.selectedBrowser.currentURI.spec.startsWith("https://example.com");
     }, "Check if FxA opened");
--- a/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
@@ -6,17 +6,17 @@
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 const {actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
 
 ChromeUtils.defineModuleGetter(this, "ShellService",
   "resource:///modules/ShellService.jsm");
 ChromeUtils.defineModuleGetter(this, "ProfileAge",
   "resource://gre/modules/ProfileAge.jsm");
-ChromeUtils.defineModuleGetter(this, "fxAccounts",
+ChromeUtils.defineModuleGetter(this, "FxAccounts",
   "resource://gre/modules/FxAccounts.jsm");
 
 // Url to fetch snippets, in the urlFormatter service format.
 const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
 const TELEMETRY_PREF = "datareporting.healthreport.uploadEnabled";
 const FXA_USERNAME_PREF = "services.sync.username";
 const ONBOARDING_FINISHED_PREF = "browser.onboarding.notification.finished";
 // Prefix for any target matching a search engine.
@@ -120,17 +120,17 @@ this.SnippetsFeed = class SnippetsFeed {
     Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._refresh);
     Services.prefs.removeObserver(TELEMETRY_PREF, this._refresh);
     Services.prefs.removeObserver(FXA_USERNAME_PREF, this._refresh);
     Services.obs.removeObserver(this, SEARCH_ENGINE_OBSERVER_TOPIC);
     this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_RESET}));
   }
 
   async showFirefoxAccounts(browser) {
-    const url = await fxAccounts.promiseAccountsSignUpURI("snippets");
+    const url = await FxAccounts.config.promiseSignUpURI("snippets");
     // We want to replace the current tab.
     browser.loadURI(url);
   }
 
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
--- a/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
@@ -16,17 +16,17 @@ describe("SnippetsFeed", () => {
     sandbox = sinon.sandbox.create();
     overrider.set({
       ProfileAge: class ProfileAge {
         constructor() {
           this.created = Promise.resolve(0);
           this.reset = Promise.resolve(WEEK_IN_MS);
         }
       },
-      fxAccounts: {promiseAccountsSignUpURI: sandbox.stub().returns(Promise.resolve(signUpUrl))}
+      FxAccounts: {config: {promiseSignUpURI: sandbox.stub().returns(Promise.resolve(signUpUrl))}}
     });
   });
   afterEach(() => {
     clock.restore();
     overrider.restore();
     sandbox.restore();
   });
   it("should dispatch a SNIPPETS_DATA action with the right data on INIT", async () => {
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -54,26 +54,17 @@ var publicProperties = [
   "handleDeviceDisconnection",
   "handleEmailUpdated",
   "hasLocalSession",
   "invalidateCertificate",
   "loadAndPoll",
   "localtimeOffsetMsec",
   "notifyDevices",
   "now",
-  "promiseAccountsChangeProfileURI",
-  "promiseAccountsEmailURI",
-  "promiseAccountsForceSigninURI",
-  "promiseAccountsManageURI",
-  "promiseAccountsManageDevicesURI",
-  "promiseAccountsConnectDeviceURI",
-  "promiseAccountsSignUpURI",
-  "promiseAccountsSignInURI",
   "removeCachedOAuthToken",
-  "requiresHttps",
   "resendVerificationEmail",
   "resetCredentials",
   "sessionStatus",
   "setProfileCache",
   "setSignedInUser",
   "signOut",
   "updateDeviceRegistration",
   "deleteDeviceRegistration",
@@ -358,16 +349,17 @@ this.FxAccounts = function(mockInternal)
     });
   }
 
   // wait until after the mocks are setup before initializing.
   internal.initialize();
 
   return Object.freeze(external);
 };
+this.FxAccounts.config = FxAccountsConfig;
 
 /**
  * The internal API's constructor.
  */
 function FxAccountsInternal() {
   // Make a local copy of this constant so we can mock it in testing
   this.POLL_SESSION = POLL_SESSION;
 
@@ -1386,115 +1378,16 @@ FxAccountsInternal.prototype = {
     }
   },
 
   _rejectWhenVerified(currentState, error) {
     currentState.whenVerifiedDeferred.reject(error);
     delete currentState.whenVerifiedDeferred;
   },
 
-  requiresHttps() {
-    // Also used in FxAccountsOAuthGrantClient.jsm.
-    let allowHttp = Services.prefs.getBoolPref("identity.fxaccounts.allowHttp", false);
-    return allowHttp !== true;
-  },
-
-  async promiseAccountsSignUpURI(entrypoint) {
-    const url = new URL((await FxAccountsConfig.promiseAccountsSignUpURI()));
-    if (entrypoint) {
-      url.searchParams.append("entrypoint", entrypoint);
-    }
-    return url.href;
-  },
-
-  async promiseAccountsSignInURI(entrypoint) {
-    const url = new URL((await FxAccountsConfig.promiseAccountsSignInURI()));
-    if (entrypoint) {
-      url.searchParams.append("entrypoint", entrypoint);
-    }
-    return url.href;
-  },
-
-  async promiseAccountsEmailURI(email, entrypoint) {
-    const url = new URL((await FxAccountsConfig.promiseAccountsEmailURI()));
-    url.searchParams.append("email", email);
-    if (entrypoint) {
-      url.searchParams.append("entrypoint", entrypoint);
-    }
-    return url.href;
-  },
-
-  /**
-   * Pull an URL defined in the user preferences, add the current UID and email
-   * to the query string, add entrypoint and extra params to the query string if
-   * requested.
-   * @param {string} prefName The preference name from where to pull the URL to format.
-   * @param {string} [entrypoint] "entrypoint" searchParam value.
-   * @param {Object.<string, string>} [extraParams] Additionnal searchParam key and values.
-   * @returns {Promise.<string>} A promise that resolves to the formatted URL
-   */
-  async _formatPrefURL(prefName, entrypoint, extraParams) {
-    let url = new URL(Services.urlFormatter.formatURLPref(prefName));
-    if (this.requiresHttps() && url.protocol != "https:") {
-      throw new Error("Firefox Accounts server must use HTTPS");
-    }
-    let accountData = await this.getSignedInUser();
-    if (!accountData) {
-      return Promise.resolve(null);
-    }
-    url.searchParams.append("uid", accountData.uid);
-    url.searchParams.append("email", accountData.email);
-    if (entrypoint) {
-      url.searchParams.append("entrypoint", entrypoint);
-    }
-    if (extraParams) {
-      for (let [k, v] of Object.entries(extraParams)) {
-        url.searchParams.append(k, v);
-      }
-    }
-    return this.currentAccountState.resolve(url.href);
-  },
-
-  // Returns a promise that resolves with the URL to use to force a re-signin
-  // of the current account.
-  async promiseAccountsForceSigninURI(entrypoint) {
-    await FxAccountsConfig.ensureConfigured();
-    return this._formatPrefURL("identity.fxaccounts.remote.force_auth.uri", entrypoint);
-  },
-
-  // Returns a promise that resolves with the URL to use to change
-  // the current account's profile image.
-  // if settingToEdit is set, the profile page should hightlight that setting
-  // for the user to edit.
-  async promiseAccountsChangeProfileURI(entrypoint, settingToEdit = null) {
-    let extraParams;
-    if (settingToEdit) {
-      extraParams = { setting: settingToEdit };
-    }
-    return this._formatPrefURL("identity.fxaccounts.settings.uri", entrypoint, extraParams);
-  },
-
-  // Returns a promise that resolves with the URL to use to manage the current
-  // user's FxA acct.
-  async promiseAccountsManageURI(entrypoint) {
-    return this._formatPrefURL("identity.fxaccounts.settings.uri", entrypoint);
-  },
-
-  // Returns a promise that resolves with the URL to use to manage the devices in
-  // the current user's FxA acct.
-  async promiseAccountsManageDevicesURI(entrypoint) {
-    return this._formatPrefURL("identity.fxaccounts.settings.devices.uri", entrypoint);
-  },
-
-  // Returns a promise that resolves with the URL to use to connect a new
-  // device to the current user's FxA acct.
-  async promiseAccountsConnectDeviceURI(entrypoint) {
-    return this._formatPrefURL("identity.fxaccounts.remote.connectdevice.uri", entrypoint);
-  },
-
   /**
    * Get an OAuth token for the user
    *
    * @param options
    *        {
    *          scope: (string/array) the oauth scope(s) being requested. As a
    *                 convenience, you may pass a string if only one scope is
    *                 required, or an array of strings if multiple are needed.
@@ -1649,24 +1542,16 @@ FxAccountsInternal.prototype = {
 
   /**
    * Get the user's account and profile data if it is locally cached. If
    * not cached it will return null, but cause the profile data to be fetched
    * in the background, after which a ON_PROFILE_CHANGE_NOTIFICATION
    * observer notification will be sent, at which time this can be called
    * again to obtain the most recent profile info.
    *
-   * @param options
-   *        {
-   *          contentUrl: (string) Used by the FxAccountsWebChannel.
-   *            Defaults to pref identity.fxaccounts.settings.uri
-   *          profileServerUrl: (string) Used by the FxAccountsWebChannel.
-   *            Defaults to pref identity.fxaccounts.remote.profile.uri
-   *        }
-   *
    * @return Promise.<object | Error>
    *        The promise resolves to an accountData object with extra profile
    *        information such as profileImageUrl, or rejects with
    *        an error object ({error: ERROR, details: {}}) of the following:
    *          INVALID_PARAMETER
    *          NO_ACCOUNT
    *          UNVERIFIED_ACCOUNT
    *          NETWORK_ERROR
--- a/services/fxaccounts/FxAccountsConfig.jsm
+++ b/services/fxaccounts/FxAccountsConfig.jsm
@@ -10,54 +10,95 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "fxAccounts",
                                "resource://gre/modules/FxAccounts.jsm");
 
 ChromeUtils.defineModuleGetter(this, "EnsureFxAccountsWebChannel",
                                "resource://gre/modules/FxAccountsWebChannel.jsm");
 
+XPCOMUtils.defineLazyPreferenceGetter(this, "ROOT_URL",
+                                      "identity.fxaccounts.remote.root");
+XPCOMUtils.defineLazyPreferenceGetter(this, "CONTEXT_PARAM",
+                                      "identity.fxaccounts.contextParam");
+XPCOMUtils.defineLazyPreferenceGetter(this, "REQUIRES_HTTPS",
+                                      // Also used in FxAccountsOAuthGrantClient.jsm.
+                                      "identity.fxaccounts.allowHttp", false,
+                                      null, val => !val);
+
 const CONFIG_PREFS = [
+  "identity.fxaccounts.remote.root",
   "identity.fxaccounts.auth.uri",
   "identity.fxaccounts.remote.oauth.uri",
   "identity.fxaccounts.remote.profile.uri",
   "identity.sync.tokenserver.uri",
-  "identity.fxaccounts.remote.webchannel.uri",
-  "identity.fxaccounts.settings.uri",
-  "identity.fxaccounts.settings.devices.uri",
-  "identity.fxaccounts.remote.signup.uri",
-  "identity.fxaccounts.remote.signin.uri",
-  "identity.fxaccounts.remote.email.uri",
-  "identity.fxaccounts.remote.connectdevice.uri",
-  "identity.fxaccounts.remote.force_auth.uri",
 ];
 
 this.FxAccountsConfig = {
+  async promiseSignUpURI(entrypoint) {
+    return this._buildURL("signup", {entrypoint});
+  },
 
-  async _getPrefURL(prefName) {
-    await this.ensureConfigured();
-    let url = Services.urlFormatter.formatURLPref(prefName);
-    if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
-      throw new Error("Firefox Accounts server must use HTTPS");
-    }
-    return url;
+  async promiseSignInURI(entrypoint) {
+    return this._buildURL("signin", {entrypoint});
+  },
+
+  async promiseEmailURI(email, entrypoint) {
+    return this._buildURL("", {entrypoint, email});
+  },
+
+  async promiseForceSigninURI(entrypoint) {
+    return this._buildURL("force_auth", {entrypoint}, true);
+  },
+
+  async promiseManageURI(entrypoint) {
+    return this._buildURL("settings", {entrypoint}, true);
+  },
+
+  async promiseChangeAvatarURI(entrypoint) {
+    return this._buildURL("settings/avatar/change", {entrypoint}, true);
+  },
+
+  async promiseManageDevicesURI(entrypoint) {
+    return this._buildURL("settings/clients", {entrypoint}, true);
   },
 
-  // Returns a promise that resolves with the URI of the remote UI flows.
-  promiseAccountsSignUpURI() {
-    return this._getPrefURL("identity.fxaccounts.remote.signup.uri");
+  async promiseConnectDeviceURI(entrypoint) {
+    return this._buildURL("connect_another_device", {entrypoint}, true);
+  },
+
+  get defaultParams() {
+    return {service: "sync", context: CONTEXT_PARAM};
   },
 
-  // Returns a promise that resolves with the URI of the remote UI flows.
-  promiseAccountsSignInURI() {
-    return this._getPrefURL("identity.fxaccounts.remote.signin.uri");
-  },
-
-  promiseAccountsEmailURI() {
-    return this._getPrefURL("identity.fxaccounts.remote.email.uri");
+  /**
+   * @param path should be parsable by the URL constructor first parameter.
+   * @param {Object.<string, string>} [extraParams] Additionnal search params.
+   * @param {bool} [addCredentials] if true we add the current logged-in user
+   *                                uid and email to the search params.
+   */
+  async _buildURL(path, extraParams, addCredentials = false) {
+    await this.ensureConfigured();
+    const url = new URL(path, ROOT_URL);
+    if (REQUIRES_HTTPS && url.protocol != "https:") {
+      throw new Error("Firefox Accounts server must use HTTPS");
+    }
+    const params = {...this.defaultParams, ...extraParams};
+    for (let [k, v] of Object.entries(params)) {
+      url.searchParams.append(k, v);
+    }
+    if (addCredentials) {
+      const accountData = await this.getSignedInUser();
+      if (!accountData) {
+        return null;
+      }
+      url.searchParams.append("uid", accountData.uid);
+      url.searchParams.append("email", accountData.email);
+    }
+    return url.href;
   },
 
   resetConfigURLs() {
     let autoconfigURL = this.getAutoConfigURL();
     if (!autoconfigURL) {
       return;
     }
     // They have the autoconfig uri pref set, so we clear all the prefs that we
@@ -78,22 +119,58 @@ this.FxAccountsConfig = {
     let rootURL = Services.urlFormatter.formatURL(pref);
     if (rootURL.endsWith("/")) {
       rootURL.slice(0, -1);
     }
     return rootURL;
   },
 
   async ensureConfigured() {
-    let isSignedIn = !!(await fxAccounts.getSignedInUser());
+    await this.tryPrefsMigration();
+    let isSignedIn = !!(await this.getSignedInUser());
     if (!isSignedIn) {
       await this.fetchConfigURLs();
     }
   },
 
+  // In bug 1427674 we migrated a set of preferences with a shared origin
+  // to a single preference (identity.fxaccounts.remote.root).
+  // This whole function should be removed in version 65 or later once
+  // everyone had a chance to migrate.
+  async tryPrefsMigration() {
+    // If this pref is set, there is a very good chance the user is running
+    // a custom FxA content server.
+    if (!Services.prefs.prefHasUserValue("identity.fxaccounts.remote.signin.uri")) {
+      return;
+    }
+
+    if (Services.prefs.prefHasUserValue("identity.fxaccounts.autoconfig.uri")) {
+      await this.fetchConfigURLs();
+    } else {
+      // Best effort.
+      const signinURI = Services.prefs.getCharPref("identity.fxaccounts.remote.signin.uri");
+      Services.prefs.setCharPref("identity.fxaccounts.remote.root",
+        signinURI.slice(0, signinURI.lastIndexOf("/signin")) + "/");
+    }
+
+    const migratedPrefs = [
+      "identity.fxaccounts.remote.webchannel.uri",
+      "identity.fxaccounts.settings.uri",
+      "identity.fxaccounts.settings.devices.uri",
+      "identity.fxaccounts.remote.signup.uri",
+      "identity.fxaccounts.remote.signin.uri",
+      "identity.fxaccounts.remote.email.uri",
+      "identity.fxaccounts.remote.connectdevice.uri",
+      "identity.fxaccounts.remote.force_auth.uri",
+    ];
+    for (const pref of migratedPrefs) {
+      Services.prefs.clearUserPref(pref);
+    }
+  },
+
   // Read expected client configuration from the fxa auth server
   // (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration)
   // and replace all the relevant our prefs with the information found there.
   // This is only done before sign-in and sign-up, and even then only if the
   // `identity.fxaccounts.autoconfig.uri` preference is set.
   async fetchConfigURLs() {
     let rootURL = this.getAutoConfigURL();
     if (!rootURL) {
@@ -128,31 +205,24 @@ this.FxAccountsConfig = {
       let authServerBase = config.auth_server_base_url;
       if (!authServerBase.endsWith("/v1")) {
         authServerBase += "/v1";
       }
       Services.prefs.setCharPref("identity.fxaccounts.auth.uri", authServerBase);
       Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", config.oauth_server_base_url + "/v1");
       Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", config.profile_server_base_url + "/v1");
       Services.prefs.setCharPref("identity.sync.tokenserver.uri", config.sync_tokenserver_base_url + "/1.0/sync/1.5");
-      // Update the prefs that are based off of the autoconfig url
-
-      let contextParam = encodeURIComponent(
-        Services.prefs.getCharPref("identity.fxaccounts.contextParam"));
-
-      Services.prefs.setCharPref("identity.fxaccounts.remote.webchannel.uri", rootURL);
-      Services.prefs.setCharPref("identity.fxaccounts.settings.uri", rootURL + "/settings?service=sync&context=" + contextParam);
-      Services.prefs.setCharPref("identity.fxaccounts.settings.devices.uri", rootURL + "/settings/clients?service=sync&context=" + contextParam);
-      Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", rootURL + "/signup?service=sync&context=" + contextParam);
-      Services.prefs.setCharPref("identity.fxaccounts.remote.signin.uri", rootURL + "/signin?service=sync&context=" + contextParam);
-      Services.prefs.setCharPref("identity.fxaccounts.remote.email.uri", rootURL + "/?service=sync&context=" + contextParam + "&action=email");
-      Services.prefs.setCharPref("identity.fxaccounts.remote.connectdevice.uri", rootURL + "/connect_another_device?service=sync&context=" + contextParam);
-      Services.prefs.setCharPref("identity.fxaccounts.remote.force_auth.uri", rootURL + "/force_auth?service=sync&context=" + contextParam);
+      Services.prefs.setCharPref("identity.fxaccounts.remote.root", rootURL);
 
       // Ensure the webchannel is pointed at the correct uri
       EnsureFxAccountsWebChannel();
     } catch (e) {
       log.error("Failed to initialize configuration preferences from autoconfig object", e);
       throw e;
     }
   },
 
+  // For test purposes, returns a Promise.
+  getSignedInUser() {
+    return fxAccounts.getSignedInUser();
+  }
+
 };
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -517,17 +517,17 @@ this.FxAccountsWebChannelHelpers.prototy
 
 var singleton;
 // The entry-point for this module, which ensures only one of our channels is
 // ever created - we require this because the WebChannel is global in scope
 // (eg, it uses the observer service to tell interested parties of interesting
 // things) and allowing multiple channels would cause such notifications to be
 // sent multiple times.
 this.EnsureFxAccountsWebChannel = () => {
-  let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+  let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.root");
   if (singleton && singleton._contentUri !== contentUri) {
     singleton.tearDown();
     singleton = null;
   }
   if (!singleton) {
     try {
       if (contentUri) {
         // The FxAccountsWebChannel listens for events and updates
--- a/services/fxaccounts/tests/browser/browser_device_connected.js
+++ b/services/fxaccounts/tests/browser/browser_device_connected.js
@@ -1,30 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"]
                      .getService(Ci.nsIObserver);
 const accountsBundle = Services.strings.createBundle(
   "chrome://browser/locale/accounts.properties"
 );
 const DEVICES_URL = "http://localhost/devices";
 
 let expectedBody;
 
 add_task(async function setup() {
-  let fxAccounts = {
-    promiseAccountsManageDevicesURI() {
-      return Promise.resolve(DEVICES_URL);
-    }
-  };
-  gBrowserGlue.observe({wrappedJSObject: fxAccounts}, "browser-glue-test", "mock-fxaccounts");
+  const origManageDevicesURI = FxAccounts.config.promiseManageDevicesURI;
+  FxAccounts.config.promiseManageDevicesURI = () => Promise.resolve(DEVICES_URL);
   setupMockAlertsService();
+
+  registerCleanupFunction(function() {
+    FxAccounts.config.promiseManageDevicesURI = origManageDevicesURI;
+    delete window.FxAccounts;
+  });
 });
 
 async function testDeviceConnected(deviceName) {
   info("testDeviceConnected with deviceName=" + deviceName);
   gBrowser.selectedBrowser.loadURI("about:robots");
   await waitForDocLoadComplete();
 
   let waitForTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -23,27 +23,16 @@ initTestLogging("Trace");
 var log = Log.repository.getLogger("Services.FxAccounts.test");
 log.level = Log.Level.Debug;
 
 // See verbose logging from FxAccounts.jsm and jwcrypto.jsm.
 Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace");
 Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace;
 Services.prefs.setCharPref("services.crypto.jwcrypto.log.level", "Debug");
 
-// The oauth server is mocked, but set these prefs to pass param checks
-Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1");
-Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123");
-
-
-const PROFILE_SERVER_URL = "http://example.com/v1";
-const CONTENT_URL = "http://accounts.example.com/";
-
-Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", PROFILE_SERVER_URL);
-Services.prefs.setCharPref("identity.fxaccounts.settings.uri", CONTENT_URL);
-
 /*
  * The FxAccountsClient communicates with the remote Firefox
  * Accounts auth server.  Mock the server calls, with a little
  * lag time to simulate some latency.
  *
  * We add the _verified attribute to mock the change in verification
  * state on the FXA server.
  */
@@ -183,38 +172,16 @@ function MakeFxAccounts(internal = {}) {
     internal._signOutServer = () => Promise.resolve();
   }
   if (!internal._registerOrUpdateDevice) {
     internal._registerOrUpdateDevice = () => Promise.resolve();
   }
   return new FxAccounts(internal);
 }
 
-add_task(async function test_non_https_remote_server_uri_with_requireHttps_false() {
-  Services.prefs.setBoolPref(
-    "identity.fxaccounts.allowHttp",
-    true);
-  Services.prefs.setCharPref(
-    "identity.fxaccounts.remote.signup.uri",
-    "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
-  Assert.equal(await fxAccounts.promiseAccountsSignUpURI(),
-               "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
-
-  Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
-  Services.prefs.clearUserPref("identity.fxaccounts.allowHttp");
-});
-
-add_task(async function test_non_https_remote_server_uri() {
-  Services.prefs.setCharPref(
-    "identity.fxaccounts.remote.signup.uri",
-    "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
-  Assert.rejects(fxAccounts.promiseAccountsSignUpURI(), null, "Firefox Accounts server must use HTTPS");
-  Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
-});
-
 add_task(async function test_get_signed_in_user_initially_unset() {
   _("Check getSignedInUser initially and after signout reports no user");
   let account = MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_accounts_config.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
+
+add_task(async function test_non_https_remote_server_uri_with_requireHttps_false() {
+  Services.prefs.setBoolPref(
+    "identity.fxaccounts.allowHttp",
+    true);
+  Services.prefs.setCharPref(
+    "identity.fxaccounts.remote.root",
+    "http://example.com/");
+  Assert.equal(await FxAccounts.config.promiseSignUpURI("test"),
+               "http://example.com/signup?service=sync&context=null&entrypoint=test");
+
+  Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
+  Services.prefs.clearUserPref("identity.fxaccounts.allowHttp");
+});
+
+add_task(async function test_non_https_remote_server_uri() {
+  Services.prefs.setCharPref(
+    "identity.fxaccounts.remote.root",
+    "http://example.com/");
+  Assert.rejects(FxAccounts.config.promiseSignUpURI(), null, "Firefox Accounts server must use HTTPS");
+  Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
+});
+
+function createFakeOldPrefs() {
+  const baseURL = "https://example.com/myfxa/";
+  let createPref = (pref, extraPath) => {
+    Services.prefs.setCharPref(pref, `${baseURL}${extraPath}?service=sync&context=fx_desktop_v3`);
+  };
+  createPref("identity.fxaccounts.remote.signin.uri", "signin");
+  createPref("identity.fxaccounts.remote.signup.uri", "signup");
+  createPref("identity.fxaccounts.remote.email.uri", "email");
+  createPref("identity.fxaccounts.remote.connectdevice.uri", "connect_another_device");
+  createPref("identity.fxaccounts.remote.force_auth.uri", "force_auth");
+  createPref("identity.fxaccounts.settings.uri", "settings");
+  createPref("identity.fxaccounts.settings.devices.uri", "settings/clients");
+  Services.prefs.setCharPref("identity.fxaccounts.remote.webchannel.uri", baseURL);
+}
+
+function checkOldPrefsDeleted() {
+  const migratedPrefs = [
+    "identity.fxaccounts.remote.webchannel.uri",
+    "identity.fxaccounts.settings.uri",
+    "identity.fxaccounts.settings.devices.uri",
+    "identity.fxaccounts.remote.signup.uri",
+    "identity.fxaccounts.remote.signin.uri",
+    "identity.fxaccounts.remote.email.uri",
+    "identity.fxaccounts.remote.connectdevice.uri",
+    "identity.fxaccounts.remote.force_auth.uri",
+  ];
+  for (const pref of migratedPrefs) {
+    Assert.ok(!Services.prefs.prefHasUserValue(pref));
+  }
+}
+
+add_task(async function test_migration_autoconfig() {
+  createFakeOldPrefs();
+  Services.prefs.setCharPref("identity.fxaccounts.autoconfig.uri",
+                             "https://example.com/.well-known/fxa-client-configuration");
+  sinon.stub(FxAccounts.config, "fetchConfigURLs");
+  await FxAccounts.config.tryPrefsMigration();
+  Assert.ok(FxAccounts.config.fetchConfigURLs.called);
+  checkOldPrefsDeleted();
+  FxAccounts.config.fetchConfigURLs.restore();
+  Services.prefs.clearUserPref("identity.fxaccounts.autoconfig.uri");
+  Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
+});
+
+add_task(async function test_migration_manual() {
+  createFakeOldPrefs();
+  sinon.stub(FxAccounts.config, "fetchConfigURLs");
+  await FxAccounts.config.tryPrefsMigration();
+  Assert.equal(Services.prefs.getCharPref("identity.fxaccounts.remote.root"),
+               "https://example.com/myfxa/");
+  Assert.ok(!FxAccounts.config.fetchConfigURLs.called);
+  checkOldPrefsDeleted();
+  FxAccounts.config.fetchConfigURLs.restore();
+  Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
+});
--- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
@@ -15,21 +15,16 @@ var log = Log.repository.getLogger("Serv
 log.level = Log.Level.Debug;
 
 const BOGUS_PUBLICKEY = "BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc";
 const BOGUS_AUTHKEY = "GSsIiaD2Mr83iPqwFNK4rw";
 
 Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace");
 Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace;
 
-Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1");
-Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123");
-Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", "http://example.com/v1");
-Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "http://accounts.example.com/");
-
 const DEVICE_REGISTRATION_VERSION = 42;
 
 function MockStorageManager() {
 }
 
 MockStorageManager.prototype = {
   initialize(accountData) {
     this.accountData = accountData;
--- a/services/fxaccounts/tests/xpcshell/test_profile.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile.js
@@ -3,60 +3,16 @@
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
 ChromeUtils.import("resource://gre/modules/FxAccountsProfileClient.jsm");
 ChromeUtils.import("resource://gre/modules/FxAccountsProfile.jsm");
 ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 
-const URL_STRING = "https://example.com";
-Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "https://example.com/settings");
-
-const STATUS_SUCCESS = 200;
-
-/**
- * Mock request responder
- * @param {String} response
- *        Mocked raw response from the server
- * @returns {Function}
- */
-let mockResponse = function(response) {
-  let Request = function(requestUri) {
-    // Store the request uri so tests can inspect it
-    Request._requestUri = requestUri;
-    return {
-      setHeader() {},
-      head() {
-        this.response = response;
-        this.onComplete();
-      }
-    };
-  };
-
-  return Request;
-};
-
-/**
- * Mock request error responder
- * @param {Error} error
- *        Error object
- * @returns {Function}
- */
-let mockResponseError = function(error) {
-  return function() {
-    return {
-      setHeader() {},
-      head() {
-        this.onComplete(error);
-      }
-    };
-  };
-};
-
 let mockClient = function(fxa) {
   let options = {
     serverURL: "http://127.0.0.1:1111/v1",
     fxa,
   };
   return new FxAccountsProfileClient(options);
 };
 
--- a/services/fxaccounts/tests/xpcshell/xpcshell.ini
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
 skip-if = (toolkit == 'android' || appname == 'thunderbird')
 support-files =
   !/services/common/tests/unit/head_helpers.js
   !/services/common/tests/unit/head_http.js
 
 [test_accounts.js]
+[test_accounts_config.js]
 [test_accounts_device_registration.js]
 [test_client.js]
 [test_credentials.js]
 [test_loginmgr_storage.js]
 [test_oauth_grant_client.js]
 [test_oauth_grant_client_server.js]
 [test_oauth_tokens.js]
 [test_oauth_token_storage.js]
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -263,22 +263,18 @@ user_pref("toolkit.telemetry.firstShutdo
 user_pref("toolkit.telemetry.test.pref1", true);
 user_pref("toolkit.telemetry.test.pref2", false);
 
 // We don't want to hit the real Firefox Accounts server for tests.  We don't
 // actually need a functioning FxA server, so just set it to something that
 // resolves and accepts requests, even if they all fail.
 user_pref("identity.fxaccounts.auth.uri", "https://%(server)s/fxa-dummy/");
 
-// Ditto for all the other Firefox accounts URIs used for about:accounts et al.:
-user_pref("identity.fxaccounts.remote.signup.uri", "https://%(server)s/fxa-signup");
-user_pref("identity.fxaccounts.remote.force_auth.uri", "https://%(server)s/fxa-force-auth");
-user_pref("identity.fxaccounts.remote.signin.uri", "https://%(server)s/fxa-signin");
-user_pref("identity.fxaccounts.settings.uri", "https://%(server)s/fxa-settings");
-user_pref("identity.fxaccounts.remote.webchannel.uri", "https://%(server)s/");
+// Ditto for all the FxA content root URI.
+user_pref("identity.fxaccounts.remote.root", "https://%(server)s/");
 
 // We don't want browser tests to perform FxA device registration.
 user_pref("identity.fxaccounts.skipDeviceRegistration", true);
 
 // Increase the APZ content response timeout in tests to 1 minute.
 // This is to accommodate the fact that test environments tends to be slower
 // than production environments (with the b2g emulator being the slowest of them
 // all), resulting in the production timeout value sometimes being exceeded