Bug 1249520 - Add client support for a fxa-client-configuration endpoint r=markh
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Wed, 21 Sep 2016 15:07:28 -0400
changeset 315324 d08f86205057efa876a903e1a7d6ad1115138b46
parent 315267 7b050ca8ec6474c6d7131d0ebd352e8624992016
child 315325 b9e51f08329dd77b9d4e1fc93058c75343c6bcfe
push id30748
push usercbook@mozilla.com
push dateWed, 28 Sep 2016 13:53:19 +0000
treeherdermozilla-central@8c84b7618840 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1249520
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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);
     });