Bug 1249520 - Add client support for a fxa-client-configuration endpoint r?markh draft
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Wed, 21 Sep 2016 15:07:28 -0400
changeset 417693 020f88759f9ec27b501b5314e10b95c4f4bd4c8c
parent 417692 14b0ea0c7b1cecaf522b813e47e9ffdee4c1eec3
child 532134 56543e1e50072202bf9a91000e17cd3bfebf83a9
push id30451
push userbmo:tchiovoloni@mozilla.com
push dateMon, 26 Sep 2016 15:40:18 +0000
reviewersmarkh
bugs1249520
milestone52.0a1
Bug 1249520 - Add client support for a fxa-client-configuration endpoint r?markh MozReview-Commit-ID: 4jTl1yIduKG
browser/app/profile/firefox.js
browser/base/content/aboutaccounts/aboutaccounts.css
browser/base/content/aboutaccounts/aboutaccounts.js
browser/base/content/aboutaccounts/aboutaccounts.xhtml
browser/base/content/test/general/browser_aboutAccounts.js
browser/locales/en-US/chrome/browser/aboutAccounts.dtd
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsClient.jsm
services/fxaccounts/FxAccountsConfig.jsm
services/fxaccounts/FxAccountsWebChannel.jsm
services/fxaccounts/moz.build
services/fxaccounts/tests/xpcshell/test_accounts.js
services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1282,16 +1282,20 @@ pref("identity.fxaccounts.remote.signup.
 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 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.
+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 remote URL of the FxA Profile Server
 pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
 
--- a/browser/base/content/aboutaccounts/aboutaccounts.css
+++ b/browser/base/content/aboutaccounts/aboutaccounts.css
@@ -4,17 +4,17 @@ html, body {
 
 #remote {
   width: 100%;
   height: 100%;
   border: 0;
   display: none;
 }
 
-#networkError, #manage, #intro, #stage {
+#networkError, #manage, #intro, #stage, #configError {
   display: none;
 }
 
 #oldsync {
   background: none;
   border: 0;
   color: #0095dd;
 }
--- a/browser/base/content/aboutaccounts/aboutaccounts.js
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -159,24 +159,24 @@ var wrapper = {
           }
         }
       }
 
       // Calling cancel() will raise some OnStateChange notifications by itself,
       // so avoid doing that more than once
       if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
         aRequest.cancel(Components.results.NS_BINDING_ABORTED);
-        setErrorPage();
+        setErrorPage("networkError");
       }
     },
 
     onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
       if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
         aRequest.cancel(Components.results.NS_BINDING_ABORTED);
-        setErrorPage();
+        setErrorPage("networkError");
       }
     },
 
     onProgressChange: function() {},
     onStatusChange: function() {},
     onSecurityChange: function() {},
   },
 
@@ -289,28 +289,27 @@ var wrapper = {
         break;
       default:
         log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
         break;
     }
   },
 
   injectData: function (type, content) {
-    let authUrl;
-    try {
-      authUrl = fxAccounts.getAccountsSignUpURI();
-    } catch (e) {
-      error("Couldn't inject data: " + e.message);
-      return;
-    }
-    let data = {
-      type: type,
-      content: content
-    };
-    this.iframe.contentWindow.postMessage(data, authUrl);
+    return fxAccounts.promiseAccountsSignUpURI().then(authUrl => {
+      let data = {
+        type: type,
+        content: content
+      };
+      this.iframe.contentWindow.postMessage(data, authUrl);
+    })
+    .catch(e => {
+      console.log("Failed to inject data", e);
+      setErrorPage("configError");
+    });
   },
 };
 
 
 // Button onclick handlers
 function handleOldSync() {
   let chromeWin = window
     .QueryInterface(Ci.nsIInterfaceRequestor)
@@ -339,80 +338,87 @@ function openPrefs() {
   window.location = "about:preferences#sync";
 }
 
 function init() {
   fxAccounts.getSignedInUser().then(user => {
     // tests in particular might cause the window to start closing before
     // getSignedInUser has returned.
     if (window.closed) {
-      return;
+      return Promise.resolve();
     }
 
     updateDisplayedEmail(user);
 
     // Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
     // searchParams is empty.
     let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
     let action = urlParams.get(ACTION_URL_PARAM);
     urlParams.delete(ACTION_URL_PARAM);
 
     switch (action) {
     case "signin":
       if (user) {
         // asking to sign-in when already signed in just shows manage.
         show("stage", "manage");
       } else {
-        show("remote");
-        wrapper.init(fxAccounts.getAccountsSignInURI(), urlParams);
+        return fxAccounts.promiseAccountsSignInURI().then(url => {
+          show("remote");
+          wrapper.init(url, urlParams);
+        });
       }
       break;
     case "signup":
       if (user) {
         // asking to sign-up when already signed in just shows manage.
         show("stage", "manage");
       } else {
-        show("remote");
-        wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
+        return fxAccounts.promiseAccountsSignUpURI().then(url => {
+          show("remote");
+          wrapper.init(url, urlParams);
+        });
       }
       break;
     case "reauth":
       // ideally we would only show this when we know the user is in a
       // "must reauthenticate" state - but we don't.
       // As the email address will be included in the URL returned from
       // promiseAccountsForceSigninURI, just always show it.
-      fxAccounts.promiseAccountsForceSigninURI().then(url => {
+      return fxAccounts.promiseAccountsForceSigninURI().then(url => {
         show("remote");
         wrapper.init(url, urlParams);
       });
-      break;
     default:
       // No action specified.
       if (user) {
         show("stage", "manage");
       } else {
         // Attempt a migration if enabled or show the introductory page
         // otherwise.
-        migrateToDevEdition(urlParams).then(migrated => {
+        return migrateToDevEdition(urlParams).then(migrated => {
           if (!migrated) {
             show("stage", "intro");
             // load the remote frame in the background
-            wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
+            return fxAccounts.promiseAccountsSignUpURI().then(uri =>
+              wrapper.init(uri, urlParams));
           }
+          return Promise.resolve();
         });
       }
       break;
     }
+    return Promise.resolve();
   }).catch(err => {
-    error("Failed to get the signed in user: " + err);
+    console.log("Configuration or sign in error", err);
+    setErrorPage("configError");
   });
 }
 
-function setErrorPage() {
-  show("stage", "networkError");
+function setErrorPage(errorType) {
+  show("stage", errorType);
 }
 
 // Causes the "top-level" element with |id| to be shown - all other top-level
 // elements are hidden.  Optionally, ensures that only 1 "second-level" element
 // inside the top-level one is shown.
 function show(id, childId) {
   // top-level items are either <div> or <iframe>
   let allTop = document.querySelectorAll("body > div, iframe");
@@ -465,17 +471,22 @@ function migrateToDevEdition(urlParams) 
     return fxAccounts.promiseAccountsForceSigninURI().then(url => {
       show("remote");
       wrapper.init(url, urlParams);
     });
   }).then(null, error => {
     log("Failed to migrate FX Account: " + error);
     show("stage", "intro");
     // load the remote frame in the background
-    wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
+    fxAccounts.promiseAccountsSignUpURI().then(uri => {
+      wrapper.init(uri, urlParams)
+    }).catch(e => {
+      console.log("Failed to load signup page", e);
+      setErrorPage("configError");
+    });
   }).then(() => {
     // Reset the pref after migration.
     Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false);
     return true;
   }).then(null, err => {
     Cu.reportError("Failed to reset the migrateToDevEdition pref: " + err);
     return false;
   });
--- a/browser/base/content/aboutaccounts/aboutaccounts.xhtml
+++ b/browser/base/content/aboutaccounts/aboutaccounts.xhtml
@@ -82,16 +82,29 @@
             <div class="description">&aboutAccounts.noConnection.description;</div>
 
             <div class="button-row">
               <button id="buttonRetry" class="button" tabindex="3">&aboutAccounts.noConnection.retry;</button>
             </div>
         </section>
       </div>
 
+      <div id="configError">
+        <header>
+          <h1>&aboutAccounts.badConfig.title;</h1>
+        </header>
+
+        <section>
+            <div class="graphic graphic-sync-intro"> </div>
+
+            <div class="description">&aboutAccounts.badConfig.description;</div>
+
+        </section>
+      </div>
+
     </div>
 
     <iframe mozframetype="content" id="remote" />
 
     <script type="application/javascript;version=1.8"
       src="chrome://browser/content/utilityOverlay.js"/>
     <script type="text/javascript;version=1.8"
       src="chrome://browser/content/aboutaccounts/aboutaccounts.js" />
--- a/browser/base/content/test/general/browser_aboutAccounts.js
+++ b/browser/base/content/test/general/browser_aboutAccounts.js
@@ -313,17 +313,17 @@ var gTests = [
     let mm = tab.linkedBrowser.messageManager;
     mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
       url: "about:accounts",
       profilePath: mockDir.path,
     });
 
     let response = yield readyPromise;
     // We are expecting the iframe to be on the "signup" URL
-    let expected = fxAccounts.getAccountsSignUpURI();
+    let expected = yield fxAccounts.promiseAccountsSignUpURI();
     is(response.data.url, expected);
 
     // and expect no signed in user.
     let userData = yield fxAccounts.getSignedInUser();
     is(userData, null);
     // The migration pref should have still been switched off.
     is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
     yield OS.File.removeEmptyDir(mockDir.path);
--- a/browser/locales/en-US/chrome/browser/aboutAccounts.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutAccounts.dtd
@@ -7,8 +7,10 @@
 
 <!ENTITY aboutAccountsConfig.description "Sign in to sync your tabs, bookmarks, passwords &amp; more.">
 <!ENTITY aboutAccountsConfig.startButton.label "Get started">
 <!ENTITY aboutAccountsConfig.useOldSync.label "Using an older version of Sync?">
 <!ENTITY aboutAccountsConfig.syncPreferences.label "Sync preferences">
 <!ENTITY aboutAccounts.noConnection.title "No connection">
 <!ENTITY aboutAccounts.noConnection.description "You must be connected to the Internet to sign in.">
 <!ENTITY aboutAccounts.noConnection.retry "Try again">
+<!ENTITY aboutAccounts.badConfig.title "Bad configuration">
+<!ENTITY aboutAccounts.badConfig.description "Unable to determine your Firefox Account server configuration. Please try again later.">
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -5,27 +5,31 @@
 
 this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient",
   "resource://gre/modules/FxAccountsClient.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsConfig",
+  "resource://gre/modules/FxAccountsConfig.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
   "resource://gre/modules/identity/jwcrypto.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthGrantClient",
   "resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfile",
   "resource://gre/modules/FxAccountsProfile.jsm");
@@ -33,34 +37,35 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource://services-sync/util.js");
 
 // All properties exposed by the public FxAccounts API.
 var publicProperties = [
   "accountStatus",
   "checkVerificationStatus",
   "getAccountsClient",
-  "getAccountsSignInURI",
-  "getAccountsSignUpURI",
   "getAssertion",
   "getDeviceId",
   "getKeys",
   "getOAuthToken",
   "getSignedInUser",
   "getSignedInUserProfile",
   "handleDeviceDisconnection",
   "invalidateCertificate",
   "loadAndPoll",
   "localtimeOffsetMsec",
   "notifyDevices",
   "now",
   "promiseAccountsChangeProfileURI",
   "promiseAccountsForceSigninURI",
   "promiseAccountsManageURI",
+  "promiseAccountsSignUpURI",
+  "promiseAccountsSignInURI",
   "removeCachedOAuthToken",
+  "requiresHttps",
   "resendVerificationEmail",
   "resetCredentials",
   "sessionStatus",
   "setSignedInUser",
   "signOut",
   "updateDeviceRegistration",
   "updateUserAccountData",
   "whenVerified",
@@ -773,19 +778,24 @@ FxAccountsInternal.prototype = {
           log.warn("Missing session token; skipping remote sign out");
         }).catch(err => {
           log.error("Error during remote sign out of Firefox Accounts", err);
         }).then(() => {
           return this._destroyAllOAuthTokens(tokensToRevoke);
         }).catch(err => {
           log.error("Error during destruction of oauth tokens during signout", err);
         }).then(() => {
+          FxAccountsConfig.resetConfigURLs();
           // just for testing - notifications are cheap when no observers.
           this.notifyObservers("testhelper-fxa-signout-complete");
-        });
+        })
+      } else {
+        // We want to do this either way -- but if we're signing out remotely we
+        // need to wait until we destroy the oauth tokens if we want that to succeed.
+        FxAccountsConfig.resetConfigURLs();
       }
     }).then(() => {
       this.notifyObservers(ONLOGOUT_NOTIFICATION);
     });
   },
 
   /**
    * This function should be called in conjunction with a server-side
@@ -1223,76 +1233,67 @@ FxAccountsInternal.prototype = {
                                      : this.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT;
     }
     log.debug("polling with timeout = " + timeoutMs);
     this.currentTimer = setTimeout(() => {
       this.pollEmailStatus(currentState, sessionToken, "timer");
     }, timeoutMs);
   },
 
-  _requireHttps: function() {
+  requiresHttps: function() {
     let allowHttp = false;
     try {
       allowHttp = Services.prefs.getBoolPref("identity.fxaccounts.allowHttp");
     } catch(e) {
       // Pref doesn't exist
     }
     return allowHttp !== true;
   },
 
-  // Return the URI of the remote UI flows.
-  getAccountsSignUpURI: function() {
-    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri");
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
-      throw new Error("Firefox Accounts server must use HTTPS");
-    }
-    return url;
+  promiseAccountsSignUpURI() {
+    return FxAccountsConfig.promiseAccountsSignUpURI();
   },
 
-  // Return the URI of the remote UI flows.
-  getAccountsSignInURI: function() {
-    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri");
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
-      throw new Error("Firefox Accounts server must use HTTPS");
-    }
-    return url;
+  promiseAccountsSignInURI() {
+    return FxAccountsConfig.promiseAccountsSignInURI();
   },
 
   // Returns a promise that resolves with the URL to use to force a re-signin
   // of the current account.
-  promiseAccountsForceSigninURI: function() {
+  promiseAccountsForceSigninURI: Task.async(function *() {
+    yield FxAccountsConfig.ensureConfigured();
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri");
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
     let currentState = this.currentAccountState;
     // but we need to append the email address onto a query string.
     return this.getSignedInUser().then(accountData => {
       if (!accountData) {
         return null;
       }
       let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
       newQueryPortion += "email=" + encodeURIComponent(accountData.email);
       return url + newQueryPortion;
     }).then(result => currentState.resolve(result));
-  },
+  }),
 
   // 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.
   promiseAccountsChangeProfileURI: function(entrypoint, settingToEdit = null) {
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
 
     if (settingToEdit) {
       url += (url.indexOf("?") == -1 ? "?" : "&") +
              "setting=" + encodeURIComponent(settingToEdit);
     }
 
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
     let currentState = this.currentAccountState;
     // but we need to append the email address onto a query string.
     return this.getSignedInUser().then(accountData => {
       if (!accountData) {
         return null;
       }
@@ -1305,17 +1306,17 @@ FxAccountsInternal.prototype = {
       return url + newQueryPortion;
     }).then(result => currentState.resolve(result));
   },
 
   // Returns a promise that resolves with the URL to use to manage the current
   // user's FxA acct.
   promiseAccountsManageURI: function(entrypoint) {
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
     let currentState = this.currentAccountState;
     // but we need to append the uid and email address onto a query string
     // (if the server has no matching uid it will offer to sign in with the
     // email address)
     return this.getSignedInUser().then(accountData => {
       if (!accountData) {
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -11,22 +11,22 @@ Cu.import("resource://gre/modules/Promis
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-common/hawkclient.js");
 Cu.import("resource://services-common/hawkrequest.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 Cu.import("resource://gre/modules/Credentials.jsm");
 
-const HOST = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
+const HOST_PREF = "identity.fxaccounts.auth.uri";
 
 const SIGNIN = "/account/login";
 const SIGNUP = "/account/create";
 
-this.FxAccountsClient = function(host = HOST) {
+this.FxAccountsClient = function(host = Services.prefs.getCharPref(HOST_PREF)) {
   this.host = host;
 
   // The FxA auth server expects requests to certain endpoints to be authorized
   // using Hawk.
   this.hawk = new HawkClient(host);
   this.hawk.observerPrefix = "FxA:hawk";
 
   // Manage server backoff state. C.f.
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsConfig.jsm
@@ -0,0 +1,175 @@
+/* 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";
+this.EXPORTED_SYMBOLS = ["FxAccountsConfig"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+                                  "resource://gre/modules/FxAccounts.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
+                                  "resource://gre/modules/FxAccountsWebChannel.jsm");
+
+const CONFIG_PREFS = [
+  "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.remote.signup.uri",
+  "identity.fxaccounts.remote.signin.uri",
+  "identity.fxaccounts.remote.force_auth.uri",
+];
+
+this.FxAccountsConfig = {
+
+  // Returns a promise that resolves with the URI of the remote UI flows.
+  promiseAccountsSignUpURI: Task.async(function*() {
+    yield this.ensureConfigured();
+    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri");
+    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;
+  }),
+
+  // Returns a promise that resolves with the URI of the remote UI flows.
+  promiseAccountsSignInURI: Task.async(function*() {
+    yield this.ensureConfigured();
+    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri");
+    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;
+  }),
+
+  resetConfigURLs() {
+    let autoconfigURL = this.getAutoConfigURL();
+    if (!autoconfigURL) {
+      return;
+    }
+    // They have the autoconfig uri pref set, so we clear all the prefs that we
+    // will have initialized, which will leave them pointing at production.
+    for (let pref of CONFIG_PREFS) {
+      Services.prefs.clearUserPref(pref);
+    }
+    // Reset the webchannel.
+    EnsureFxAccountsWebChannel();
+    if (!Services.prefs.prefHasUserValue("webchannel.allowObject.urlWhitelist")) {
+      return;
+    }
+    let whitelistValue = Services.prefs.getCharPref("webchannel.allowObject.urlWhitelist");
+    if (whitelistValue.startsWith(autoconfigURL + " ")) {
+      whitelistValue = whitelistValue.slice(autoconfigURL.length + 1);
+      // Check and see if the value will be the default, and just clear the pref if it would
+      // to avoid it showing up as changed in about:config.
+      let defaultWhitelist;
+      try {
+        defaultWhitelist = Services.prefs.getDefaultBranch("webchannel.allowObject.").getCharPref("urlWhitelist");
+      } catch (e) {
+        // No default value ...
+      }
+
+      if (defaultWhitelist === whitelistValue) {
+        Services.prefs.clearUserPref("webchannel.allowObject.urlWhitelist");
+      } else {
+        Services.prefs.setCharPref("webchannel.allowObject.urlWhitelist", whitelistValue);
+      }
+    }
+  },
+
+  getAutoConfigURL() {
+    let pref;
+    try {
+      pref = Services.prefs.getCharPref("identity.fxaccounts.autoconfig.uri");
+    } catch (e) { /* no pref */ }
+    if (!pref) {
+      // no pref / empty pref means we don't bother here.
+      return "";
+    }
+    let rootURL = Services.urlFormatter.formatURL(pref);
+    if (rootURL.endsWith("/")) {
+      rootURL.slice(0, -1);
+    }
+    return rootURL;
+  },
+
+  ensureConfigured: Task.async(function*() {
+    let isSignedIn = !!(yield fxAccounts.getSignedInUser());
+    if (!isSignedIn) {
+      yield this.fetchConfigURLs();
+    }
+  }),
+
+  // 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.
+  fetchConfigURLs: Task.async(function*() {
+    let rootURL = this.getAutoConfigURL();
+    if (!rootURL) {
+      return;
+    }
+    let configURL = rootURL + "/.well-known/fxa-client-configuration";
+    let jsonStr = yield new Promise((resolve, reject) => {
+      let request = new RESTRequest(configURL);
+      request.setHeader("Accept", "application/json");
+      request.get(error => {
+        if (error) {
+          log.error(`Failed to get configuration object from "${configURL}"`, error);
+          return reject(error);
+        }
+        if (!request.response.success) {
+          log.error(`Received HTTP response code ${request.response.status} from configuration object request`);
+          if (request.response && request.response.body) {
+            log.debug("Got error response", request.response.body);
+          }
+          return reject(request.response.status);
+        }
+        resolve(request.response.body);
+      });
+    });
+
+    log.debug("Got successful configuration response", jsonStr);
+    try {
+      // Update the prefs directly specified by the config.
+      let config = JSON.parse(jsonStr)
+      Services.prefs.setCharPref("identity.fxaccounts.auth.uri", config.auth_server_base_url);
+      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.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.force_auth.uri", rootURL + "/force_auth?service=sync&context=" + contextParam);
+
+      let whitelistValue = Services.prefs.getCharPref("webchannel.allowObject.urlWhitelist");
+      if (!whitelistValue.includes(rootURL)) {
+        whitelistValue = `${rootURL} ${whitelistValue}`;
+        Services.prefs.setCharPref("webchannel.allowObject.urlWhitelist", whitelistValue);
+      }
+      // 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;
+    }
+  }),
+
+};
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -445,19 +445,23 @@ 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 = function() {
+  let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+  if (singleton && singleton._contentUri !== contentUri) {
+    singleton.tearDown();
+    singleton = null;
+  }
   if (!singleton) {
     try {
-      let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
       if (contentUri) {
         // The FxAccountsWebChannel listens for events and updates
         // the state machine accordingly.
         singleton = new this.FxAccountsWebChannel({
           content_uri: contentUri,
           channel_id: WEBCHANNEL_ID,
         });
       } else {
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -15,16 +15,17 @@ EXTRA_COMPONENTS += [
   'FxAccountsPush.js',
 ]
 
 EXTRA_JS_MODULES += [
   'Credentials.jsm',
   'FxAccounts.jsm',
   'FxAccountsClient.jsm',
   'FxAccountsCommon.js',
+  'FxAccountsConfig.jsm',
   'FxAccountsOAuthClient.jsm',
   'FxAccountsOAuthGrantClient.jsm',
   'FxAccountsProfile.jsm',
   'FxAccountsProfileClient.jsm',
   'FxAccountsPush.js',
   'FxAccountsStorage.jsm',
   'FxAccountsWebChannel.jsm',
 ]
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -187,42 +187,36 @@ function MakeFxAccounts(internal = {}) {
     internal._signOutServer = () => Promise.resolve();
   }
   if (!internal._registerOrUpdateDevice) {
     internal._registerOrUpdateDevice = () => Promise.resolve();
   }
   return new FxAccounts(internal);
 }
 
-add_test(function test_non_https_remote_server_uri_with_requireHttps_false() {
+add_task(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");
-  do_check_eq(fxAccounts.getAccountsSignUpURI(),
+  do_check_eq(yield 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");
-  run_next_test();
 });
 
-add_test(function test_non_https_remote_server_uri() {
+add_task(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");
-  do_check_throws_message(function () {
-    fxAccounts.getAccountsSignUpURI();
-  }, "Firefox Accounts server must use HTTPS");
-
+  rejects(fxAccounts.promiseAccountsSignUpURI(), null, "Firefox Accounts server must use HTTPS");
   Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
-
-  run_next_test();
 });
 
 add_task(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",
--- a/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
+++ b/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
@@ -7,16 +7,17 @@
 this.EXPORTED_SYMBOLS = [
   "Authentication",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/FxAccounts.jsm");
 Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://gre/modules/FxAccountsConfig.jsm");
 Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://tps/logger.jsm");
 
 
 /**
  * Helper object for Firefox Accounts authentication
  */
@@ -64,16 +65,19 @@ var Authentication = {
   signIn: function signIn(account) {
     let cb = Async.makeSpinningCallback();
 
     Logger.AssertTrue(account["username"], "Username has been found");
     Logger.AssertTrue(account["password"], "Password has been found");
 
     Logger.logInfo("Login user: " + account["username"]);
 
+    // Required here since we don't go through the real login page
+    Async.promiseSpinningly(FxAccountsConfig.ensureConfigured());
+
     let client = new FxAccountsClient();
     client.signIn(account["username"], account["password"], true).then(credentials => {
       return fxAccounts.setSignedInUser(credentials);
     }).then(() => {
       cb(null, true);
     }, error => {
       cb(error, false);
     });