Bug 1435929 - refactor browserid_identity.js to be less confusing and error prone. r=eoger,tcsc
authorMark Hammond <mhammond@skippinet.com.au>
Tue, 06 Feb 2018 14:05:45 +1100
changeset 457702 de66caaad6daa6ebeded76fff25f392f580fcb3b
parent 457701 01e78c9fcb5df65d4b37daa016ba7e9a2dd47b49
child 457703 c39af4d010d51ad06339162c7b9a5f7c2c50553f
push id8799
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 16:46:23 +0000
treeherdermozilla-beta@15334014dc67 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseoger, tcsc
bugs1435929
milestone60.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 1435929 - refactor browserid_identity.js to be less confusing and error prone. r=eoger,tcsc MozReview-Commit-ID: IJPQv4ZvJlp
services/sync/modules-testing/fxa_utils.js
services/sync/modules-testing/utils.js
services/sync/modules/UIState.jsm
services/sync/modules/browserid_identity.js
services/sync/modules/constants.js
services/sync/modules/service.js
services/sync/modules/stages/enginesync.js
services/sync/modules/status.js
services/sync/modules/util.js
services/sync/tests/unit/test_browserid_identity.js
services/sync/tests/unit/test_errorhandler_1.js
services/sync/tests/unit/test_fxa_node_reassignment.js
services/sync/tests/unit/test_fxa_service_cluster.js
services/sync/tests/unit/test_keys.js
services/sync/tests/unit/test_service_cluster.js
services/sync/tests/unit/test_service_verifyLogin.js
services/sync/tests/unit/test_syncscheduler.js
tools/lint/eslint/modules.json
--- a/services/sync/modules-testing/fxa_utils.js
+++ b/services/sync/modules-testing/fxa_utils.js
@@ -39,17 +39,16 @@ var initializeIdentityWithTokenServerRes
   MockTSC.prototype.newRESTRequest = function(url) {
     return new MockRESTRequest(url);
   };
   // Arrange for the same observerPrefix as browserid_identity uses.
   MockTSC.prototype.observerPrefix = "weave:service";
 
   // tie it all together.
   Weave.Status.__authManager = Weave.Service.identity = new BrowserIDManager();
-  Weave.Service._clusterManager = Weave.Service.identity.createClusterManager(Weave.Service);
   let browseridManager = Weave.Service.identity;
   // a sanity check
   if (!(browseridManager instanceof BrowserIDManager)) {
     throw new Error("sync isn't configured for browserid_identity");
   }
   let mockTSC = new MockTSC();
   configureFxAccountIdentity(browseridManager);
   browseridManager._tokenServerClient = mockTSC;
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -21,16 +21,17 @@ var EXPORTED_SYMBOLS = [
   "syncTestLogging",
 ];
 
 ChromeUtils.import("resource://services-sync/status.js");
 ChromeUtils.import("resource://services-common/utils.js");
 ChromeUtils.import("resource://services-crypto/utils.js");
 ChromeUtils.import("resource://services-sync/util.js");
 ChromeUtils.import("resource://services-sync/browserid_identity.js");
+ChromeUtils.import("resource://testing-common/Assert.jsm");
 ChromeUtils.import("resource://testing-common/services/common/logging.js");
 ChromeUtils.import("resource://testing-common/services/sync/fakeservices.js");
 ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
 ChromeUtils.import("resource://gre/modules/FxAccountsClient.jsm");
 ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 // and grab non-exported stuff via a backstage pass.
@@ -157,17 +158,17 @@ var makeFxAccountsInternalMock = functio
         throw new Error("Not expecting to have credentials passed");
       }
       let storageManager = new MockFxaStorageManager();
       storageManager.initialize(config.fxaccount.user);
       let accountState = new AccountState(storageManager);
       return accountState;
     },
     _getAssertion(audience) {
-      return Promise.resolve("assertion");
+      return Promise.resolve(config.fxaccount.user.assertion);
     },
   };
 };
 
 // Configure an instance of an FxAccount identity provider with the specified
 // config (or the default config if not specified).
 var configureFxAccountIdentity = function(authService,
                                           config = makeIdentityConfig(),
@@ -187,16 +188,18 @@ var configureFxAccountIdentity = functio
       return Promise.resolve(true);
     }
   };
   let mockFxAClient = new MockFxAccountsClient();
   fxa.internal._fxAccountsClient = mockFxAClient;
 
   let mockTSC = { // TokenServerClient
     async getTokenFromBrowserIDAssertion(uri, assertion) {
+      Assert.equal(uri, Services.prefs.getStringPref("identity.sync.tokenserver.uri"));
+      Assert.equal(assertion, config.fxaccount.user.assertion);
       config.fxaccount.token.uid = config.username;
       return config.fxaccount.token;
     },
   };
   authService._fxaService = fxa;
   authService._tokenServerClient = mockTSC;
   // Set the "account" of the browserId manager to be the "email" of the
   // logged in user of the mockFXA service.
@@ -215,20 +218,22 @@ var configureIdentity = async function(i
     if (!ep.endsWith("/")) {
       ep += "/";
     }
     ep += "1.1/" + config.username + "/";
     config.fxaccount.token.endpoint = ep;
   }
 
   configureFxAccountIdentity(ns.Service.identity, config);
-  await ns.Service.identity.initializeWithCurrentIdentity();
-  // The token is fetched in the background, whenReadyToAuthenticate is resolved
-  // when we are ready.
-  await ns.Service.identity.whenReadyToAuthenticate.promise;
+  // because we didn't send any FxA LOGIN notifications we must set the username.
+  ns.Service.identity.username = config.username;
+  // many of these tests assume all the auth stuff is setup and don't hit
+  // a path which causes that auth to magically happen - so do it now.
+  await ns.Service.identity._ensureValidToken();
+
   // and cheat to avoid requiring each test do an explicit login - give it
   // a cluster URL.
   if (config.fxaccount.token.endpoint) {
     ns.Service.clusterURL = config.fxaccount.token.endpoint;
   }
 };
 
 function syncTestLogging(level = "Trace") {
--- a/services/sync/modules/UIState.jsm
+++ b/services/sync/modules/UIState.jsm
@@ -23,16 +23,17 @@ ChromeUtils.defineModuleGetter(this, "We
 
 const TOPICS = [
   "weave:service:login:change",
   "weave:service:login:error",
   "weave:service:ready",
   "weave:service:sync:start",
   "weave:service:sync:finish",
   "weave:service:sync:error",
+  "weave:service:start-over:finish",
   "fxaccounts:onverified",
   "fxaccounts:onlogin", // Defined in FxAccountsCommon, pulling it is expensive.
   "fxaccounts:onlogout",
   "fxaccounts:profilechange",
 ];
 
 const ON_UPDATE = "sync-ui-state:update";
 
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -4,17 +4,16 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["BrowserIDManager", "AuthenticationError"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/Log.jsm");
-ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
 ChromeUtils.import("resource://services-common/async.js");
 ChromeUtils.import("resource://services-common/utils.js");
 ChromeUtils.import("resource://services-common/tokenserverclient.js");
 ChromeUtils.import("resource://services-crypto/utils.js");
 ChromeUtils.import("resource://services-sync/util.js");
 ChromeUtils.import("resource://services-sync/constants.js");
 
@@ -38,16 +37,17 @@ XPCOMUtils.defineLazyPreferenceGetter(th
                                       "services.sync.debug.ignoreCachedAuthCredentials");
 
 // FxAccountsCommon.js doesn't use a "namespace", so create one here.
 var fxAccountsCommon = {};
 ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
 
 const OBSERVER_TOPICS = [
   fxAccountsCommon.ONLOGIN_NOTIFICATION,
+  fxAccountsCommon.ONVERIFIED_NOTIFICATION,
   fxAccountsCommon.ONLOGOUT_NOTIFICATION,
   fxAccountsCommon.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION,
 ];
 
 // A telemetry helper that records how long a user was in a "bad" state.
 // It is recorded in the *main* ping, *not* the Sync ping.
 // These bad states may persist across browser restarts, and may never change
 // (eg, users may *never* validate)
@@ -153,246 +153,133 @@ AuthenticationError.prototype = {
 };
 
 function BrowserIDManager() {
   // NOTE: _fxaService and _tokenServerClient are replaced with mocks by
   // the test suite.
   this._fxaService = fxAccounts;
   this._tokenServerClient = new TokenServerClient();
   this._tokenServerClient.observerPrefix = "weave:service";
-  // will be a promise that resolves when we are ready to authenticate
-  this.whenReadyToAuthenticate = null;
   this._log = log;
   XPCOMUtils.defineLazyPreferenceGetter(this, "_username", "services.sync.username");
+
+  this.asyncObserver = Async.asyncObserver(this, log);
+  for (let topic of OBSERVER_TOPICS) {
+    Services.obs.addObserver(this.asyncObserver, topic);
+  }
 }
 
 this.BrowserIDManager.prototype = {
   _fxaService: null,
   _tokenServerClient: null,
   // https://docs.services.mozilla.com/token/apis.html
   _token: null,
   _signedInUser: null, // the signedinuser we got from FxAccounts.
 
-  // null if no error, otherwise a LOGIN_FAILED_* value that indicates why
-  // we failed to authenticate (but note it might not be an actual
-  // authentication problem, just a transient network error or similar)
-  _authFailureReason: null,
-
-  // it takes some time to fetch a sync key bundle, so until this flag is set,
-  // we don't consider the lack of a keybundle as a failure state.
-  _shouldHaveSyncKeyBundle: false,
-
   hashedUID() {
     if (!this._hashedUID) {
       throw new Error("hashedUID: Don't seem to have previously seen a token");
     }
     return this._hashedUID;
   },
 
   // Return a hashed version of a deviceID, suitable for telemetry.
   hashedDeviceID(deviceID) {
     let uid = this.hashedUID();
     // Combine the raw device id with the metrics uid to create a stable
     // unique identifier that can't be mapped back to the user's FxA
     // identity without knowing the metrics HMAC key.
     return Utils.sha256(deviceID + uid);
   },
 
-  deviceID() {
-    return this._signedInUser && this._signedInUser.deviceId;
-  },
-
-  initialize() {
-    for (let topic of OBSERVER_TOPICS) {
-      Services.obs.addObserver(this, topic);
-    }
-  },
-
-  /**
-   * Ensure the user is logged in.  Returns a promise that resolves when
-   * the user is logged in, or is rejected if the login attempt has failed.
-   */
-  ensureLoggedIn() {
-    if (!this._shouldHaveSyncKeyBundle && this.whenReadyToAuthenticate) {
-      // We are already in the process of logging in.
-      return this.whenReadyToAuthenticate.promise;
-    }
-
-    // If we are already happy then there is nothing more to do.
-    if (this._syncKeyBundle) {
-      return Promise.resolve();
-    }
-
-    // Similarly, if we have a previous failure that implies an explicit
-    // re-entering of credentials by the user is necessary we don't take any
-    // further action - an observer will fire when the user does that.
-    if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
-      return Promise.reject(new Error("User needs to re-authenticate"));
-    }
-
-    // So - we've a previous auth problem and aren't currently attempting to
-    // log in - so fire that off.
-    this.initializeWithCurrentIdentity();
-    return this.whenReadyToAuthenticate.promise;
-  },
-
   finalize() {
     // After this is called, we can expect Service.identity != this.
     for (let topic of OBSERVER_TOPICS) {
-      Services.obs.removeObserver(this, topic);
+      Services.obs.removeObserver(this.asyncObserver, topic);
     }
     this.resetCredentials();
     this._signedInUser = null;
   },
 
-  async initializeWithCurrentIdentity(isInitialSync = false) {
-    // While this function returns a promise that resolves once we've started
-    // the auth process, that process is complete when
-    // this.whenReadyToAuthenticate.promise resolves.
-    this._log.trace("initializeWithCurrentIdentity");
-
-    // Reset the world before we do anything async.
-    this.whenReadyToAuthenticate = PromiseUtils.defer();
-    this.whenReadyToAuthenticate.promise.catch(err => {
-      this._log.error("Could not authenticate", err);
-    });
-
-    // initializeWithCurrentIdentity() can be called after the
-    // identity module was first initialized, e.g., after the
-    // user completes a force authentication, so we should make
-    // sure all credentials are reset before proceeding.
-    this.resetCredentials();
-    this._authFailureReason = null;
-
-    try {
-      let accountData = await this._fxaService.getSignedInUser();
-      if (!accountData) {
-        this._log.info("initializeWithCurrentIdentity has no user logged in");
-        // and we are as ready as we can ever be for auth.
-        this._shouldHaveSyncKeyBundle = true;
-        this.whenReadyToAuthenticate.reject("no user is logged in");
-        return;
-      }
-
-      this.username = accountData.email;
-      this._updateSignedInUser(accountData);
-      // The user must be verified before we can do anything at all; we kick
-      // this and the rest of initialization off in the background (ie, we
-      // don't return the promise)
-      CommonUtils.nextTick(async () => {
-        try {
-          this._log.info("Waiting for user to be verified.");
-          if (!accountData.verified) {
-            telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.NOTVERIFIED);
-          }
-          accountData = await this._fxaService.whenVerified(accountData);
-          this._updateSignedInUser(accountData);
-
-          this._log.info("Starting fetch for key bundle.");
-          let token = await this._fetchTokenForUser();
-          this._token = token;
-          if (token) {
-            // We may not have a token if the master-password is locked - but we
-            // still treat this as "success" so we don't prompt for re-authentication.
-            this._hashedUID = token.hashed_fxa_uid; // see _ensureValidToken for why we do this...
-          }
-          this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
-          this.whenReadyToAuthenticate.resolve();
-          this._log.info("Background fetch for key bundle done");
-          Weave.Status.login = LOGIN_SUCCEEDED;
-          if (isInitialSync) {
-            this._log.info("Doing initial sync actions");
-            Svc.Prefs.set("firstSync", "resetClient");
-            Services.obs.notifyObservers(null, "weave:service:setup-complete");
-            CommonUtils.nextTick(Weave.Service.sync, Weave.Service);
-          }
-        } catch (authErr) {
-          // report what failed...
-          this._log.error("Background fetch for key bundle failed", authErr);
-          this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
-          this.whenReadyToAuthenticate.reject(authErr);
-        }
-        // and we are done - the fetch continues on in the background...
-      });
-    } catch (err) {
-      this._log.error("Processing logged in account", err);
-    }
-  },
-
   _updateSignedInUser(userData) {
     // This object should only ever be used for a single user.  It is an
     // error to update the data if the user changes (but updates are still
     // necessary, as each call may add more attributes to the user).
     // We start with no user, so an initial update is always ok.
-    if (this._signedInUser && this._signedInUser.email != userData.email) {
+    if (this._signedInUser && this._signedInUser.uid != userData.uid) {
       throw new Error("Attempting to update to a different user.");
     }
     this._signedInUser = userData;
   },
 
   logout() {
     // This will be called when sync fails (or when the account is being
     // unlinked etc).  It may have failed because we got a 401 from a sync
     // server, so we nuke the token.  Next time sync runs and wants an
     // authentication header, we will notice the lack of the token and fetch a
     // new one.
     this._token = null;
   },
 
-  observe(subject, topic, data) {
+  async observe(subject, topic, data) {
     this._log.debug("observed " + topic);
     switch (topic) {
     case fxAccountsCommon.ONLOGIN_NOTIFICATION: {
+      this._log.info("A user has logged in");
       // If our existing Sync state is that we needed to reauth, clear that
       // state now - it will get reset back if a problem persists.
       if (Weave.Status.login == LOGIN_FAILED_LOGIN_REJECTED) {
         Weave.Status.login = LOGIN_SUCCEEDED;
       }
-      // This should only happen if we've been initialized without a current
-      // user - otherwise we'd have seen the LOGOUT notification and been
-      // thrown away.
-      // The exception is when we've initialized with a user that needs to
-      // reauth with the server - in that case we will also get here, but
-      // should have the same identity, and so we pass `false` into
-      // initializeWithCurrentIdentity so that we won't do a full sync for our
-      // first sync if we can avoid it.
-      // initializeWithCurrentIdentity will throw and log if these constraints
-      // aren't met (indirectly, via _updateSignedInUser()), so just go ahead
-      // and do the init.
-      let firstLogin = !this.username;
-      this.initializeWithCurrentIdentity(firstLogin);
+      this.resetCredentials();
+      let accountData = await this._fxaService.getSignedInUser();
+      this._updateSignedInUser(accountData);
+
+      if (!accountData.verified) {
+        // wait for a verified notification before we kick sync off.
+        this._log.info("The user is not verified");
+        break;
+      }
+    }
+    // We've been configured with an already verified user, so fall-through.
+    case fxAccountsCommon.ONVERIFIED_NOTIFICATION: {
+      this._log.info("The user became verified");
 
-      if (!firstLogin) {
-        // We still want to trigger these even if it isn't our first login.
-        // Note that the promise returned by `initializeWithCurrentIdentity`
-        // is resolved at the start of authentication, but we don't want to fire
-        // this event or start the next sync until after authentication is done
-        // (which is signaled by `this.whenReadyToAuthenticate.promise` resolving).
-        this.whenReadyToAuthenticate.promise.then(() => {
-          Services.obs.notifyObservers(null, "weave:service:setup-complete");
-          return Async.promiseYield();
-        }).then(() => {
-          return Weave.Service.sync();
-        }).catch(e => {
-          this._log.warn("Failed to trigger setup complete notification", e);
-        });
+      // Set the username now - that will cause Sync to know it is configured
+      let accountData = await this._fxaService.getSignedInUser();
+      this.username = accountData.email;
+
+      // And actually sync. If we've never synced before, we force a full sync.
+      // If we have, then we are probably just reauthenticating so it's a normal sync.
+      // We can use any pref that must be set if we've synced before.
+      let isFirstSync = !Svc.Prefs.get("client.syncID", null);
+      if (isFirstSync) {
+        this._log.info("Doing initial sync actions");
+        Svc.Prefs.set("firstSync", "resetClient");
       }
-    } break;
+      Services.obs.notifyObservers(null, "weave:service:setup-complete");
+      // There's no need to wait for sync to complete and it would deadlock
+      // our AsyncObserver.
+      Weave.Service.sync({why: "login"});
+      break;
+    }
 
     case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
-      Async.promiseSpinningly(Weave.Service.startOver());
+      Weave.Service.startOver().then(() => {
+        this._log.trace("startOver completed");
+      }).catch(err => {
+        this._log.warn("Failed to reset sync", err);
+      });
       // startOver will cause this instance to be thrown away, so there's
       // nothing else to do.
       break;
 
     case fxAccountsCommon.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION:
-      // throw away token and fetch a new one
+      // throw away token forcing us to fetch a new one later.
       this.resetCredentials();
-      this._ensureValidToken().catch(err =>
-        this._log.error("Error while fetching a new token", err));
       break;
     }
   },
 
   /**
    * Provide override point for testing token expiration.
    */
   _now() {
@@ -434,125 +321,100 @@ this.BrowserIDManager.prototype = {
     this._log.info("Username changed. Removing stored credentials.");
     this.resetCredentials();
   },
 
   /**
    * Resets/Drops all credentials we hold for the current user.
    */
   resetCredentials() {
-    this.resetSyncKeyBundle();
+    this._syncKeyBundle = null;
     this._token = null;
     this._hashedUID = null;
     // The cluster URL comes from the token, so resetting it to empty will
     // force Sync to not accidentally use a value from an earlier token.
     Weave.Service.clusterURL = null;
   },
 
   /**
-   * Resets/Drops the sync key bundle we hold for the current user.
-   */
-  resetSyncKeyBundle() {
-    this._syncKeyBundle = null;
-    this._shouldHaveSyncKeyBundle = false;
-  },
-
-  /**
    * Pre-fetches any information that might help with migration away from this
    * identity.  Called after every sync and is really just an optimization that
    * allows us to avoid a network request for when we actually need the
    * migration info.
    */
   prefetchMigrationSentinel(service) {
     // nothing to do here until we decide to migrate away from FxA.
   },
 
   /**
-    * Return credentials hosts for this identity only.
-    */
-  _getSyncCredentialsHosts() {
-    return Utils.getSyncCredentialsHostsFxA();
-  },
-
-  /**
    * Deletes Sync credentials from the password manager.
    */
   deleteSyncCredentials() {
-    for (let host of this._getSyncCredentialsHosts()) {
+    for (let host of Utils.getSyncCredentialsHosts()) {
       let logins = Services.logins.findLogins({}, host, "", "");
       for (let login of logins) {
         Services.logins.removeLogin(login);
       }
     }
   },
 
   /**
-   * The current state of the auth credentials.
-   *
-   * This essentially validates that enough credentials are available to use
-   * Sync. It doesn't check we have all the keys we need as the master-password
-   * may have been locked when we tried to get them - we rely on
-   * unlockAndVerifyAuthState to check that for us.
-   */
-  get currentAuthState() {
-    if (this._authFailureReason) {
-      this._log.info("currentAuthState returning " + this._authFailureReason +
-                     " due to previous failure");
-      return this._authFailureReason;
-    }
-
-    // TODO: need to revisit this. Currently this isn't ready to go until
-    // both the username and syncKeyBundle are both configured and having no
-    // username seems to make things fail fast so that's good.
-    if (!this.username) {
-      return LOGIN_FAILED_NO_USERNAME;
-    }
-
-    return STATUS_OK;
-  },
-
-  /**
    * Verify the current auth state, unlocking the master-password if necessary.
    *
    * Returns a promise that resolves with the current auth state after
    * attempting to unlock.
    */
   async unlockAndVerifyAuthState() {
+    let data = await this._fxaService.getSignedInUser();
+    if (!data) {
+      log.debug("unlockAndVerifyAuthState has no user");
+      return LOGIN_FAILED_NO_USERNAME;
+    }
+    if (!data.verified) {
+      // Treat not verified as if the user needs to re-auth, so the browser
+      // UI reflects the state.
+      log.debug("unlockAndVerifyAuthState has an unverified user");
+      telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.NOTVERIFIED);
+      return LOGIN_FAILED_LOGIN_REJECTED;
+    }
+    this._updateSignedInUser(data);
     if ((await this._fxaService.canGetKeys())) {
       log.debug("unlockAndVerifyAuthState already has (or can fetch) sync keys");
+      telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.SUCCESS);
       return STATUS_OK;
     }
     // so no keys - ensure MP unlocked.
     if (!Utils.ensureMPUnlocked()) {
       // user declined to unlock, so we don't know if they are stored there.
       log.debug("unlockAndVerifyAuthState: user declined to unlock master-password");
       return MASTER_PASSWORD_LOCKED;
     }
     // now we are unlocked we must re-fetch the user data as we may now have
     // the details that were previously locked away.
-    const accountData = await this._fxaService.getSignedInUser();
-    this._updateSignedInUser(accountData);
+    this._updateSignedInUser(await this._fxaService.getSignedInUser());
     // If we still can't get keys it probably means the user authenticated
     // without unlocking the MP or cleared the saved logins, so we've now
     // lost them - the user will need to reauth before continuing.
     let result;
     if ((await this._fxaService.canGetKeys())) {
+      telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.SUCCESS);
       result = STATUS_OK;
     } else {
+      telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.REJECTED);
       result = LOGIN_FAILED_LOGIN_REJECTED;
     }
     log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
     return result;
   },
 
   /**
    * Do we have a non-null, not yet expired token for the user currently
    * signed in?
    */
-  hasValidToken() {
+  _hasValidToken() {
     // If pref is set to ignore cached authentication credentials for debugging,
     // then return false to force the fetching of a new token.
     if (IGNORE_CACHED_AUTH_CREDENTIALS) {
       return false;
     }
     if (!this._token) {
       return false;
     }
@@ -573,154 +435,153 @@ this.BrowserIDManager.prototype = {
     }
     while (url.endsWith("/")) { // trailing slashes cause problems...
       url = url.slice(0, -1);
     }
     return url;
   },
 
   // Refresh the sync token for our user. Returns a promise that resolves
-  // with a token (which may be null in one sad edge-case), or rejects with an
-  // error.
+  // with a token, or rejects with an error.
   async _fetchTokenForUser() {
-    // tokenServerURI is mis-named - convention is uri means nsISomething...
-    let tokenServerURI = this._tokenServerUrl;
-    let log = this._log;
-    let client = this._tokenServerClient;
-    let fxa = this._fxaService;
-    let userData = this._signedInUser;
+    // gotta be verified to fetch a token.
+    if (!this._signedInUser.verified) {
+      throw new Error("User is not verified");
+    }
 
     // We need keys for things to work.  If we don't have them, just
     // return null for the token - sync calling unlockAndVerifyAuthState()
     // before actually syncing will setup the error states if necessary.
     if (!(await this._fxaService.canGetKeys())) {
-      log.info("Unable to fetch keys (master-password locked?), so aborting token fetch");
-      return Promise.resolve(null);
+      this._log.info("Unable to fetch keys (master-password locked?), so aborting token fetch");
+      throw new Error("Can't fetch a token as we can't get keys");
     }
 
-    let maybeFetchKeys = () => {
-      log.info("Getting keys");
-      return this._fxaService.getKeys().then(
-        newUserData => {
-          userData = newUserData;
-          this._updateSignedInUser(userData); // throws if the user changed.
-        }
-      );
-    };
+    // Do the assertion/certificate/token dance, with a retry.
+    let getToken = async () => {
+      this._log.info("Getting an assertion from", this._tokenServerUrl);
+      const audience = Services.io.newURI(this._tokenServerUrl).prePath;
+      const assertion = await this._fxaService.getAssertion(audience);
 
-    let getToken = async (assertion) => {
-      log.debug("Getting a token");
-      let headers = {"X-Client-State": userData.kXCS};
-      // Exceptions will be handled by the caller.
-      const token = await client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, headers);
-      log.debug("Successfully got a sync token");
+      this._log.debug("Getting a token");
+      const headers = {"X-Client-State": this._signedInUser.kXCS};
+      const token = await this._tokenServerClient.getTokenFromBrowserIDAssertion(
+                            this._tokenServerUrl, assertion, headers);
+      this._log.trace("Successfully got a token");
       return token;
     };
 
-    let getAssertion = () => {
-      log.info("Getting an assertion from", tokenServerURI);
-      let audience = Services.io.newURI(tokenServerURI).prePath;
-      return fxa.getAssertion(audience);
-    };
+    let token;
+    try {
+      try {
+        this._log.info("Getting keys");
+        this._updateSignedInUser(await this._fxaService.getKeys()); // throws if the user changed.
 
-    // wait until the account email is verified and we know that
-    // getAssertion() will return a real assertion (not null).
-    return fxa.whenVerified(this._signedInUser)
-      .then(() => maybeFetchKeys())
-      .then(() => getAssertion())
-      .then(assertion => getToken(assertion))
-      .catch(err => {
+        token = await getToken();
+      } catch (err) {
         // If we get a 401 fetching the token it may be that our certificate
         // needs to be regenerated.
         if (!err.response || err.response.status !== 401) {
-          return Promise.reject(err);
-        }
-        log.warn("Token server returned 401, refreshing certificate and retrying token fetch");
-        return fxa.invalidateCertificate()
-          .then(() => getAssertion())
-          .then(assertion => getToken(assertion));
-      })
-      .then(token => {
-        // TODO: Make it be only 80% of the duration, so refresh the token
-        // before it actually expires. This is to avoid sync storage errors
-        // otherwise, we get a nasty notification bar briefly. Bug 966568.
-        token.expiration = this._now() + (token.duration * 1000) * 0.80;
-        if (!this._syncKeyBundle) {
-          this._syncKeyBundle = BulkKeyBundle.fromHexKey(userData.kSync);
+          throw err;
         }
-        telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.SUCCESS);
-        return token;
-      })
-      .catch(err => {
-        // TODO: unify these errors - we need to handle errors thrown by
-        // both tokenserverclient and hawkclient.
-        // A tokenserver error thrown based on a bad response.
-        if (err.response && err.response.status === 401) {
-          err = new AuthenticationError(err, "tokenserver");
-        // A hawkclient error.
-        } else if (err.code && err.code === 401) {
-          err = new AuthenticationError(err, "hawkclient");
-        // An FxAccounts.jsm error.
-        } else if (err.message == fxAccountsCommon.ERROR_AUTH_ERROR) {
-          err = new AuthenticationError(err, "fxaccounts");
-        }
+        this._log.warn("Token server returned 401, refreshing certificate and retrying token fetch");
+        await this._fxaService.invalidateCertificate();
+        token = await getToken();
+      }
+      // TODO: Make it be only 80% of the duration, so refresh the token
+      // before it actually expires. This is to avoid sync storage errors
+      // otherwise, we may briefly enter a "needs reauthentication" state.
+      // (XXX - the above may no longer be true - someone should check ;)
+      token.expiration = this._now() + (token.duration * 1000) * 0.80;
+      if (!this._syncKeyBundle) {
+        this._syncKeyBundle = BulkKeyBundle.fromHexKey(this._signedInUser.kSync);
+      }
+      telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.SUCCESS);
+      Weave.Status.login = LOGIN_SUCCEEDED;
+      this._token = token;
+      return token;
+    } catch (caughtErr) {
+      let err = caughtErr; // The error we will rethrow.
+      // TODO: unify these errors - we need to handle errors thrown by
+      // both tokenserverclient and hawkclient.
+      // A tokenserver error thrown based on a bad response.
+      if (err.response && err.response.status === 401) {
+        err = new AuthenticationError(err, "tokenserver");
+      // A hawkclient error.
+      } else if (err.code && err.code === 401) {
+        err = new AuthenticationError(err, "hawkclient");
+      // An FxAccounts.jsm error.
+      } else if (err.message == fxAccountsCommon.ERROR_AUTH_ERROR) {
+        err = new AuthenticationError(err, "fxaccounts");
+      }
 
-        // TODO: write tests to make sure that different auth error cases are handled here
-        // properly: auth error getting assertion, auth error getting token (invalid generation
-        // and client-state error)
-        if (err instanceof AuthenticationError) {
-          this._log.error("Authentication error in _fetchTokenForUser", err);
-          // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
-          this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
-          telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.REJECTED);
-        } else {
-          this._log.error("Non-authentication error in _fetchTokenForUser", err);
-          // for now assume it is just a transient network related problem
-          // (although sadly, it might also be a regular unhandled exception)
-          this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
-        }
-        // this._authFailureReason being set to be non-null in the above if clause
-        // ensures we are in the correct currentAuthState, and
-        // this._shouldHaveSyncKeyBundle being true ensures everything that cares knows
-        // that there is no authentication dance still under way.
-        this._shouldHaveSyncKeyBundle = true;
-        Weave.Status.login = this._authFailureReason;
-        throw err;
-      });
+      // TODO: write tests to make sure that different auth error cases are handled here
+      // properly: auth error getting assertion, auth error getting token (invalid generation
+      // and client-state error)
+      if (err instanceof AuthenticationError) {
+        this._log.error("Authentication error in _fetchTokenForUser", err);
+        // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
+        Weave.Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+        telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.REJECTED);
+      } else {
+        this._log.error("Non-authentication error in _fetchTokenForUser", err);
+        // for now assume it is just a transient network related problem
+        // (although sadly, it might also be a regular unhandled exception)
+        Weave.Status.login = LOGIN_FAILED_NETWORK_ERROR;
+      }
+      throw err;
+    }
   },
 
-  // Returns a promise that is resolved when we have a valid token for the
-  // current user stored in this._token.  When resolved, this._token is valid.
-  _ensureValidToken() {
-    if (this.hasValidToken()) {
-      this._log.debug("_ensureValidToken already has one");
-      return Promise.resolve();
+  // Returns a promise that is resolved with a valid token for the current
+  // user, or rejects if one can't be obtained.
+  // NOTE: This does all the authentication for Sync - it both sets the
+  // key bundle (ie, decryption keys) and does the token fetch. These 2
+  // concepts could be decoupled, but there doesn't seem any value in that
+  // currently.
+  async _ensureValidToken(forceNewToken = false) {
+    if (!this._signedInUser) {
+      throw new Error("no user is logged in");
+    }
+    if (!this._signedInUser.verified) {
+      throw new Error("user is not verified");
     }
-    const notifyStateChanged =
-      () => Services.obs.notifyObservers(null, "weave:service:login:change");
+
+    await this.asyncObserver.promiseObserversComplete();
+
+    if (!forceNewToken && this._hasValidToken()) {
+      this._log.trace("_ensureValidToken already has one");
+      return this._token;
+    }
+
+    // We are going to grab a new token - re-use the same promise if we are
+    // already fetching one.
+    if (!this._ensureValidTokenPromise) {
+      this._ensureValidTokenPromise = this.__ensureValidToken().finally(() => {
+        this._ensureValidTokenPromise = null;
+      });
+    }
+    return this._ensureValidTokenPromise;
+  },
+
+  async __ensureValidToken() {
     // reset this._token as a safety net to reduce the possibility of us
     // repeatedly attempting to use an invalid token if _fetchTokenForUser throws.
     this._token = null;
-    return this._fetchTokenForUser().then(
-      token => {
-        this._token = token;
-        // we store the hashed UID from the token so that if we see a transient
-        // error fetching a new token we still know the "most recent" hashed
-        // UID for telemetry.
-        if (token) {
-          // We may not have a token if the master-password is locked.
-          this._hashedUID = token.hashed_fxa_uid;
-        }
-        notifyStateChanged();
-      },
-      error => {
-        notifyStateChanged();
-        throw error;
-      }
-    );
+    try {
+      let token = await this._fetchTokenForUser();
+      this._token = token;
+      // we store the hashed UID from the token so that if we see a transient
+      // error fetching a new token we still know the "most recent" hashed
+      // UID for telemetry.
+      this._hashedUID = token.hashed_fxa_uid;
+      return token;
+    } finally {
+      Services.obs.notifyObservers(null, "weave:service:login:change");
+    }
   },
 
   getResourceAuthenticator() {
     return this._getAuthenticationHeader.bind(this);
   },
 
   /**
    * @return a Hawk HTTP Authorization Header, lightly wrapped, for the .uri
@@ -753,105 +614,67 @@ this.BrowserIDManager.prototype = {
       localtimeOffsetMsec: this._localtimeOffsetMsec,
       credentials,
     };
 
     let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method, options);
     return {headers: {authorization: headerValue.field}};
   },
 
-  createClusterManager(service) {
-    return new BrowserIDClusterManager(service);
-  },
-
-  // Tell Sync what the login status should be if it saw a 401 fetching
-  // info/collections as part of login verification (typically immediately
-  // after login.)
-  // In our case, it almost certainly means a transient error fetching a token
-  // (and hitting this will cause us to logout, which will correctly handle an
-  // authoritative login issue.)
-  loginStatusFromVerification404() {
-    return LOGIN_FAILED_NETWORK_ERROR;
-  },
-};
-
-/* An implementation of the ClusterManager for this identity
- */
-
-function BrowserIDClusterManager(service) {
-  this._log = log;
-  this.service = service;
-}
-
-BrowserIDClusterManager.prototype = {
-  get identity() {
-    return this.service.identity;
-  },
-
   /**
    * Determine the cluster for the current user and update state.
+   * Returns true if a new cluster URL was found and it is different from
+   * the existing cluster URL, false otherwise.
    */
   async setCluster() {
     // Make sure we didn't get some unexpected response for the cluster.
     let cluster = await this._findCluster();
     this._log.debug("Cluster value = " + cluster);
     if (cluster == null) {
       return false;
     }
 
     // Convert from the funky "String object with additional properties" that
     // resource.js returns to a plain-old string.
     cluster = cluster.toString();
     // Don't update stuff if we already have the right cluster
-    if (cluster == this.service.clusterURL) {
+    if (cluster == Weave.Service.clusterURL) {
       return false;
     }
 
     this._log.debug("Setting cluster to " + cluster);
-    this.service.clusterURL = cluster;
+    Weave.Service.clusterURL = cluster;
 
     return true;
   },
 
   async _findCluster() {
     try {
       // Ensure we are ready to authenticate and have a valid token.
-      await this.identity.whenReadyToAuthenticate.promise;
       // We need to handle node reassignment here.  If we are being asked
       // for a clusterURL while the service already has a clusterURL, then
       // it's likely a 401 was received using the existing token - in which
       // case we just discard the existing token and fetch a new one.
-      if (this.service.clusterURL) {
-        log.debug("_findCluster has a pre-existing clusterURL, so discarding the current token");
-        this.identity._token = null;
+      let forceNewToken = false;
+      if (Weave.Service.clusterURL) {
+        this._log.debug("_findCluster has a pre-existing clusterURL, so fetching a new token token");
+        forceNewToken = true;
       }
-      await this.identity._ensureValidToken();
-
-      // The only reason (in theory ;) that we can end up with a null token
-      // is when this._fxaService.canGetKeys() returned false.  In turn, this
-      // should only happen if the master-password is locked or the credentials
-      // storage is screwed, and in those cases we shouldn't have started
-      // syncing so shouldn't get here anyway.
-      // But better safe than sorry! To keep things clearer, throw an explicit
-      // exception - the message will appear in the logs and the error will be
-      // treated as transient.
-      if (!this.identity._token) {
-        throw new Error("Can't get a cluster URL as we can't fetch keys.");
-      }
-      let endpoint = this.identity._token.endpoint;
+      let token = await this._ensureValidToken(forceNewToken);
+      let endpoint = token.endpoint;
       // For Sync 1.5 storage endpoints, we use the base endpoint verbatim.
       // However, it should end in "/" because we will extend it with
       // well known path components. So we add a "/" if it's missing.
       if (!endpoint.endsWith("/")) {
         endpoint += "/";
       }
-      log.debug("_findCluster returning " + endpoint);
+      this._log.debug("_findCluster returning " + endpoint);
       return endpoint;
     } catch (err) {
-      log.info("Failed to fetch the cluster URL", err);
+      this._log.info("Failed to fetch the cluster URL", err);
       // service.js's verifyLogin() method will attempt to fetch a cluster
       // URL when it sees a 401.  If it gets null, it treats it as a "real"
       // auth error and sets Status.login to LOGIN_FAILED_LOGIN_REJECTED, which
       // in turn causes a notification bar to appear informing the user they
       // need to re-authenticate.
       // On the other hand, if fetching the cluster URL fails with an exception,
       // verifyLogin() assumes it is a transient error, and thus doesn't show
       // the notification bar under the assumption the issue will resolve
@@ -860,18 +683,9 @@ BrowserIDClusterManager.prototype = {
       // * On a real 401, we must return null.
       // * On any other problem we must let an exception bubble up.
       if (err instanceof AuthenticationError) {
         return null;
       }
       throw err;
     }
   },
-
-  getUserBaseURL() {
-    // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy
-    // Sync appends path components onto an empty path, and in FxA Sync the
-    // token server constructs this for us in an opaque manner. Since the
-    // cluster manager already sets the clusterURL on Service and also has
-    // access to the current identity, we added this functionality here.
-    return this.service.clusterURL;
-  }
 };
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -14,22 +14,16 @@ WEAVE_VERSION: "1.62.0",
 SYNC_API_VERSION:                      "1.5",
 
 // Version of the data format this client supports. The data format describes
 // how records are packaged; this is separate from the Server API version and
 // the per-engine cleartext formats.
 STORAGE_VERSION:                       5,
 PREFS_BRANCH:                          "services.sync.",
 
-// Host "key" to access Weave Identity in the password manager
-PWDMGR_HOST:                           "chrome://weave",
-PWDMGR_PASSWORD_REALM:                 "Mozilla Services Password",
-PWDMGR_PASSPHRASE_REALM:               "Mozilla Services Encryption Passphrase",
-PWDMGR_KEYBUNDLE_REALM:                "Mozilla Services Key Bundles",
-
 // Put in [] because those aren't allowed in a collection name.
 DEFAULT_KEYBUNDLE_NAME:                "[default]",
 
 // Key dimensions.
 SYNC_KEY_ENCODED_LENGTH:               26,
 SYNC_KEY_DECODED_LENGTH:               16,
 
 NO_SYNC_NODE_INTERVAL:                 10 * 60 * 1000, // 10 minutes
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -81,17 +81,17 @@ Sync11Service.prototype = {
   _lock: Utils.lock,
   _locked: false,
   _loggedIn: false,
 
   infoURL: null,
   storageURL: null,
   metaURL: null,
   cryptoKeyURL: null,
-  // The cluster URL comes via the ClusterManager object, which in the FxA
+  // The cluster URL comes via the identity object, which in the FxA
   // world is ebbedded in the token returned from the token server.
   _clusterURL: null,
 
   get clusterURL() {
     return this._clusterURL || "";
   },
   set clusterURL(value) {
     if (value != null && typeof value != "string") {
@@ -124,20 +124,18 @@ Sync11Service.prototype = {
         this._log.info("Cannot start sync: already syncing?");
       }
     }
 
     return Utils.catch.call(this, func, lockExceptions);
   },
 
   get userBaseURL() {
-    if (!this._clusterManager) {
-      return null;
-    }
-    return this._clusterManager.getUserBaseURL();
+    // The user URL is the cluster URL.
+    return this.clusterURL;
   },
 
   _updateCachedURLs: function _updateCachedURLs() {
     // Nothing to cache yet if we don't have the building blocks
     if (!this.clusterURL) {
       // Also reset all other URLs used by Sync to ensure we aren't accidentally
       // using one cached earlier - if there's no cluster URL any cached ones
       // are invalid.
@@ -292,17 +290,16 @@ Sync11Service.prototype = {
     this.scheduler = new SyncScheduler(this);
     this.errorHandler = new ErrorHandler(this);
 
     this._log = Log.repository.getLogger("Sync.Service");
     this._log.manageLevelFromPref("services.sync.log.logger.service.main");
 
     this._log.info("Loading Weave " + WEAVE_VERSION);
 
-    this._clusterManager = this.identity.createClusterManager(this);
     this.recordManager = new RecordManager(this);
 
     this.enabled = true;
 
     await this._registerEngines();
 
     let ua = Cc["@mozilla.org/network/protocol;1?name=http"].
       getService(Ci.nsIHttpProtocolHandler).userAgent;
@@ -651,40 +648,33 @@ Sync11Service.prototype = {
     // there's overhead involved that is hard to calculate on the client, so we
     // use 512k to be safe (at the recommendation of the server team). Note
     // that if the server reports a lower limit (via info/configuration), we
     // respect that limit instead. See also bug 1403052.
     return Math.min(512 * 1024, this.getMaxRecordPayloadSize());
   },
 
   async verifyLogin(allow40XRecovery = true) {
-    if (!this.identity.username) {
-      this._log.warn("No username in verifyLogin.");
-      this.status.login = LOGIN_FAILED_NO_USERNAME;
-      return false;
-    }
-
     // Attaching auth credentials to a request requires access to
     // passwords, which means that Resource.get can throw MP-related
     // exceptions!
     // So we ask the identity to verify the login state after unlocking the
     // master password (ie, this call is expected to prompt for MP unlock
     // if necessary) while we still have control.
-    let unlockedState = await this.identity.unlockAndVerifyAuthState();
-    this._log.debug("Fetching unlocked auth state returned " + unlockedState);
-    if (unlockedState != STATUS_OK) {
-      this.status.login = unlockedState;
+    this.status.login = await this.identity.unlockAndVerifyAuthState();
+    this._log.debug("Fetching unlocked auth state returned " + this.status.login);
+    if (this.status.login != STATUS_OK) {
       return false;
     }
 
     try {
       // Make sure we have a cluster to verify against.
       // This is a little weird, if we don't get a node we pretend
       // to succeed, since that probably means we just don't have storage.
-      if (this.clusterURL == "" && !(await this._clusterManager.setCluster())) {
+      if (this.clusterURL == "" && !(await this.identity.setCluster())) {
         this.status.sync = NO_SYNC_NODE_FOUND;
         return true;
       }
 
       // Fetch collection info on every startup.
       let test = await this.resource(this.infoURL).get();
 
       switch (test.status) {
@@ -713,26 +703,23 @@ Sync11Service.prototype = {
           return false;
 
         case 401:
           this._log.warn("401: login failed.");
           // Fall through to the 404 case.
 
         case 404:
           // Check that we're verifying with the correct cluster
-          if (allow40XRecovery && (await this._clusterManager.setCluster())) {
+          if (allow40XRecovery && (await this.identity.setCluster())) {
             return await this.verifyLogin(false);
           }
 
           // We must have the right cluster, but the server doesn't expect us.
-          // The implications of this depend on the identity being used - for
-          // the legacy identity, it's an authoritatively "incorrect password",
-          // (ie, LOGIN_FAILED_LOGIN_REJECTED) but for FxA it probably means
-          // "transient error fetching auth token".
-          this.status.login = this.identity.loginStatusFromVerification404();
+          // For FxA this almost certainly means "transient error fetching token".
+          this.status.login = LOGIN_FAILED_NETWORK_ERROR;
           return false;
 
         default:
           // Server didn't respond with something that we expected
           this.status.login = LOGIN_FAILED_SERVER_ERROR;
           this.errorHandler.checkServerError(test);
           return false;
       }
@@ -821,18 +808,18 @@ Sync11Service.prototype = {
     } else {
       this._log.debug("Skipping client data removal: no cluster URL.");
     }
 
     // We want let UI consumers of the following notification know as soon as
     // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
     // by emptying the passphrase (we still need the password).
     this._log.info("Service.startOver dropping sync key and logging out.");
-    this.identity.resetSyncKeyBundle();
-    this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
+    this.identity.resetCredentials();
+    this.status.login = LOGIN_FAILED_NO_USERNAME;
     this.logout();
     Svc.Obs.notify("weave:service:start-over");
 
     // Reset all engines and clear keys.
     await this.resetClient();
     this.collectionKeys.clear();
     this.status.resetBackoff();
 
@@ -845,17 +832,16 @@ Sync11Service.prototype = {
     Svc.Prefs.set("lastversion", WEAVE_VERSION);
 
     this.identity.deleteSyncCredentials();
 
     try {
       this.identity.finalize();
       this.status.__authManager = null;
       this.identity = Status._authManager;
-      this._clusterManager = this.identity.createClusterManager(this);
       Svc.Obs.notify("weave:service:start-over:finish");
     } catch (err) {
       this._log.error("startOver failed to re-initialize the identity manager", err);
       // Still send the observer notification so the current state is
       // reflected in the UI.
       Svc.Obs.notify("weave:service:start-over:finish");
     }
   },
@@ -863,32 +849,24 @@ Sync11Service.prototype = {
   async login() {
     async function onNotify() {
       this._loggedIn = false;
       if (this.scheduler.offline) {
         this.status.login = LOGIN_FAILED_NETWORK_ERROR;
         throw new Error("Application is offline, login should not be called");
       }
 
-      this._log.info("Logging in the user.");
-      // Just let any errors bubble up - they've more context than we do!
-      try {
-        await this.identity.ensureLoggedIn();
-      } finally {
-        this._checkSetup(); // _checkSetup has a side effect of setting the right state.
-      }
-
-      this._updateCachedURLs();
-
       this._log.info("User logged in successfully - verifying login.");
       if (!(await this.verifyLogin())) {
         // verifyLogin sets the failure states here.
         throw new Error(`Login failed: ${this.status.login}`);
       }
 
+      this._updateCachedURLs();
+
       this._loggedIn = true;
 
       return true;
     }
 
     let notifier = this._notify("login", "", onNotify.bind(this));
     return this._catch(this._lock("service.js: login", notifier))();
   },
--- a/services/sync/modules/stages/enginesync.js
+++ b/services/sync/modules/stages/enginesync.js
@@ -44,17 +44,17 @@ EngineSynchronizer.prototype = {
 
       // this is a purposeful abort rather than a failure, so don't set
       // any status bits
       reason = "Can't sync: " + reason;
       throw new Error(reason);
     }
 
     // If we don't have a node, get one. If that fails, retry in 10 minutes.
-    if (!this.service.clusterURL && !(await this.service._clusterManager.setCluster())) {
+    if (!this.service.clusterURL && !(await this.service.identity.setCluster())) {
       this.service.status.sync = NO_SYNC_NODE_FOUND;
       this._log.info("No cluster URL found. Cannot sync.");
       return;
     }
 
     // Ping the server with a special info request once a day.
     let infoURL = this.service.infoURL;
     let now = Math.floor(Date.now() / 1000);
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -14,17 +14,16 @@ var Status = {
   __authManager: null,
   ready: false,
 
   get _authManager() {
     if (this.__authManager) {
       return this.__authManager;
     }
     this.__authManager = new BrowserIDManager();
-    this.__authManager.initialize();
     return this.__authManager;
   },
 
   get service() {
     return this._service;
   },
 
   set service(code) {
@@ -87,23 +86,22 @@ var Status = {
   toString: function toString() {
     return "<Status" +
            ": login: " + Status.login +
            ", service: " + Status.service +
            ", sync: " + Status.sync + ">";
   },
 
   checkSetup: function checkSetup() {
-    let result = this._authManager.currentAuthState;
-    if (result == STATUS_OK) {
-      Status.service = result;
-      return result;
+    if (!this._authManager.username) {
+      Status.login = LOGIN_FAILED_NO_USERNAME;
+      Status.service = CLIENT_NOT_CONFIGURED;
+    } else if (Status.login == STATUS_OK) {
+      Status.service = STATUS_OK;
     }
-
-    Status.login = result;
     return Status.service;
   },
 
   resetBackoff: function resetBackoff() {
     this.enforceBackoff = false;
     this.backoffInterval = 0;
     this.minimumNextSync = 0;
   },
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -604,35 +604,16 @@ var Utils = {
   /**
    * Return a set of hostnames (including the protocol) which may have
    * credentials for sync itself stored in the login manager.
    *
    * In general, these hosts will not have their passwords synced, will be
    * reset when we drop sync credentials, etc.
    */
   getSyncCredentialsHosts() {
-    let result = new Set(this.getSyncCredentialsHostsLegacy());
-    for (let host of this.getSyncCredentialsHostsFxA()) {
-      result.add(host);
-    }
-    return result;
-  },
-
-  /*
-   * Get the "legacy" identity hosts.
-   */
-  getSyncCredentialsHostsLegacy() {
-    // the legacy sync host
-    return new Set([PWDMGR_HOST]);
-  },
-
-  /*
-   * Get the FxA identity hosts.
-   */
-  getSyncCredentialsHostsFxA() {
     let result = new Set();
     // the FxA host
     result.add(FxAccountsCommon.FXA_PWDMGR_HOST);
     // We used to include the FxA hosts (hence the Set() result) but we now
     // don't give them special treatment (hence the Set() with exactly 1 item)
     return result;
   },
 
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -59,33 +59,32 @@ function MockFxAccounts() {
     return Promise.resolve(this.cert.cert);
   };
   return fxa;
 }
 
 add_test(function test_initial_state() {
     _("Verify initial state");
     Assert.ok(!globalBrowseridManager._token);
-    Assert.ok(!globalBrowseridManager.hasValidToken());
+    Assert.ok(!globalBrowseridManager._hasValidToken());
     run_next_test();
   }
 );
 
-add_task(async function test_initialializeWithCurrentIdentity() {
-    _("Verify start after initializeWithCurrentIdentity");
-    globalBrowseridManager.initializeWithCurrentIdentity();
-    await globalBrowseridManager.whenReadyToAuthenticate.promise;
+add_task(async function test_initialialize() {
+    _("Verify start after fetching token");
+    await globalBrowseridManager._ensureValidToken();
     Assert.ok(!!globalBrowseridManager._token);
-    Assert.ok(globalBrowseridManager.hasValidToken());
+    Assert.ok(globalBrowseridManager._hasValidToken());
     Assert.deepEqual(getLoginTelemetryScalar(), {SUCCESS: 1});
   }
 );
 
 add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
-    _("Verify sync unpair after initializeWithCurrentIdentity with auth error + account deleted");
+    _("Verify sync state with auth error + account deleted");
 
     var identityConfig = makeIdentityConfig();
     var browseridManager = new BrowserIDManager();
 
     // Use the real `_getAssertion` method that calls
     // `mockFxAClient.signCertificate`.
     let fxaInternal = makeFxAccountsInternalMock(identityConfig);
     delete fxaInternal._getAssertion;
@@ -112,43 +111,26 @@ add_task(async function test_initialiali
         accountStatusCalled = true;
         return Promise.resolve(false);
       }
     };
 
     let mockFxAClient = new AuthErrorMockFxAClient();
     browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
 
-    await browseridManager.initializeWithCurrentIdentity();
-    await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
-                     "should reject due to an auth error");
+    await Assert.rejects(browseridManager._ensureValidToken(),
+                         "should reject due to an auth error");
 
     Assert.ok(signCertificateCalled);
     Assert.ok(accountStatusCalled);
     Assert.ok(!browseridManager._token);
-    Assert.ok(!browseridManager.hasValidToken());
+    Assert.ok(!browseridManager._hasValidToken());
     Assert.deepEqual(getLoginTelemetryScalar(), {REJECTED: 1});
 });
 
-add_task(async function test_initialializeWithNoKeys() {
-    _("Verify start after initializeWithCurrentIdentity without kSync, kXCS, kExtSync, kExtKbHash or keyFetchToken");
-    let identityConfig = makeIdentityConfig();
-    delete identityConfig.fxaccount.user.kSync;
-    delete identityConfig.fxaccount.user.kXCS;
-    delete identityConfig.fxaccount.user.kExtSync;
-    delete identityConfig.fxaccount.user.kExtKbHash;
-    // there's no keyFetchToken by default, so the initialize should fail.
-    configureFxAccountIdentity(globalBrowseridManager, identityConfig);
-
-    await globalBrowseridManager.initializeWithCurrentIdentity();
-    await globalBrowseridManager.whenReadyToAuthenticate.promise;
-    Assert.equal(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys");
-    Assert.equal(globalBrowseridManager._token, null, "we don't have a token");
-});
-
 add_task(async function test_getResourceAuthenticator() {
     _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
     configureFxAccountIdentity(globalBrowseridManager);
     let authenticator = globalBrowseridManager.getResourceAuthenticator();
     Assert.ok(!!authenticator);
     let req = {uri: CommonUtils.makeURI(
       "https://example.net/somewhere/over/the/rainbow"),
                method: "GET"};
@@ -261,44 +243,29 @@ add_task(async function test_RESTResourc
   // window.
   Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS);
   Assert.ok(
       (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
 });
 
 add_task(async function test_ensureLoggedIn() {
   configureFxAccountIdentity(globalBrowseridManager);
-  await globalBrowseridManager.initializeWithCurrentIdentity();
-  await globalBrowseridManager.whenReadyToAuthenticate.promise;
+  await globalBrowseridManager._ensureValidToken();
   Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
-  await globalBrowseridManager.ensureLoggedIn();
-  Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked");
-  Assert.ok(globalBrowseridManager._shouldHaveSyncKeyBundle,
-            "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
+  Assert.ok(globalBrowseridManager._token);
 
   // arrange for no logged in user.
   let fxa = globalBrowseridManager._fxaService;
   let signedInUser = fxa.internal.currentAccountState.storageManager.accountData;
   fxa.internal.currentAccountState.storageManager.accountData = null;
-  globalBrowseridManager.initializeWithCurrentIdentity();
-  Assert.ok(!globalBrowseridManager._shouldHaveSyncKeyBundle,
-            "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
-  Status.login = LOGIN_FAILED_NO_USERNAME;
-  await Assert.rejects(globalBrowseridManager.ensureLoggedIn(), "expecting rejection due to no user");
-  Assert.ok(globalBrowseridManager._shouldHaveSyncKeyBundle,
-            "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
+  await Assert.rejects(globalBrowseridManager._ensureValidToken(true), "expecting rejection due to no user");
   // Restore the logged in user to what it was.
   fxa.internal.currentAccountState.storageManager.accountData = signedInUser;
   Status.login = LOGIN_FAILED_LOGIN_REJECTED;
-  await Assert.rejects(globalBrowseridManager.ensureLoggedIn(),
-                       "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
-  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED,
-               "status should remain LOGIN_FAILED_LOGIN_REJECTED");
-  Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  await globalBrowseridManager.ensureLoggedIn();
+  await globalBrowseridManager._ensureValidToken();
   Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
 });
 
 add_task(async function test_tokenExpiration() {
     _("BrowserIDManager notices token expiration:");
     let bimExp = new BrowserIDManager();
     configureFxAccountIdentity(bimExp, globalIdentityConfig);
 
@@ -314,49 +281,47 @@ add_task(async function test_tokenExpira
     Object.defineProperty(bimExp, "_now", {
       value: function customNow() {
         return (Date.now() + 3000001);
       },
       writable: true,
     });
     Assert.ok(bimExp._token.expiration < bimExp._now());
     _("... means BrowserIDManager knows to re-fetch it on the next call.");
-    Assert.ok(!bimExp.hasValidToken());
+    Assert.ok(!bimExp._hasValidToken());
   }
 );
 
 add_task(async function test_getTokenErrors() {
   _("BrowserIDManager correctly handles various failures to get a token.");
 
   _("Arrange for a 401 - Sync should reflect an auth error.");
   initializeIdentityWithTokenServerResponse({
     status: 401,
     headers: {"content-type": "application/json"},
     body: JSON.stringify({}),
   });
   let browseridManager = Service.identity;
 
-  await browseridManager.initializeWithCurrentIdentity();
-  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+  await Assert.rejects(browseridManager._ensureValidToken(),
                        "should reject due to 401");
   Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
 
   // XXX - other interesting responses to return?
 
   // And for good measure, some totally "unexpected" errors - we generally
   // assume these problems are going to magically go away at some point.
   _("Arrange for an empty body with a 200 response - should reflect a network error.");
   initializeIdentityWithTokenServerResponse({
     status: 200,
     headers: [],
     body: "",
   });
   browseridManager = Service.identity;
-  await browseridManager.initializeWithCurrentIdentity();
-  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+  await Assert.rejects(browseridManager._ensureValidToken(),
                        "should reject due to non-JSON response");
   Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
 });
 
 add_task(async function test_refreshCertificateOn401() {
   _("BrowserIDManager refreshes the FXA certificate after a 401.");
   var identityConfig = makeIdentityConfig();
   var browseridManager = new BrowserIDManager();
@@ -404,24 +369,23 @@ add_task(async function test_refreshCert
         uid:          "uid",
         duration:     300,
       })
     };
   });
 
   browseridManager._tokenServerClient = mockTSC;
 
-  await browseridManager.initializeWithCurrentIdentity();
-  await browseridManager.whenReadyToAuthenticate.promise;
+  await browseridManager._ensureValidToken();
 
   Assert.equal(getCertCount, 2);
   Assert.ok(didReturn401);
   Assert.ok(didReturn200);
   Assert.ok(browseridManager._token);
-  Assert.ok(browseridManager.hasValidToken());
+  Assert.ok(browseridManager._hasValidToken());
 });
 
 
 
 add_task(async function test_getTokenErrorWithRetry() {
   _("tokenserver sends an observer notification on various backoff headers.");
 
   // Set Sync's backoffInterval to zero - after we simulated the backoff header
@@ -431,18 +395,17 @@ add_task(async function test_getTokenErr
   initializeIdentityWithTokenServerResponse({
     status: 503,
     headers: {"content-type": "application/json",
               "retry-after": "100"},
     body: JSON.stringify({}),
   });
   let browseridManager = Service.identity;
 
-  await browseridManager.initializeWithCurrentIdentity();
-  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+  await Assert.rejects(browseridManager._ensureValidToken(),
                        "should reject due to 503");
 
   // The observer should have fired - check it got the value in the response.
   Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
   // Sync will have the value in ms with some slop - so check it is at least that.
   Assert.ok(Status.backoffInterval >= 100000);
 
   _("Arrange for a 200 with an X-Backoff header.");
@@ -450,18 +413,17 @@ add_task(async function test_getTokenErr
   initializeIdentityWithTokenServerResponse({
     status: 503,
     headers: {"content-type": "application/json",
               "x-backoff": "200"},
     body: JSON.stringify({}),
   });
   browseridManager = Service.identity;
 
-  await browseridManager.initializeWithCurrentIdentity();
-  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+  await Assert.rejects(browseridManager._ensureValidToken(),
                        "should reject due to no token in response");
 
   // The observer should have fired - check it got the value in the response.
   Assert.ok(Status.backoffInterval >= 200000);
 });
 
 add_task(async function test_getKeysErrorWithBackoff() {
   _("Auth server (via hawk) sends an observer notification on backoff headers.");
@@ -485,17 +447,17 @@ add_task(async function test_getKeysErro
       status: 503,
       headers: {"content-type": "application/json",
                 "x-backoff": "100"},
       body: "{}",
     };
   });
 
   let browseridManager = Service.identity;
-  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+  await Assert.rejects(browseridManager._ensureValidToken(),
                        "should reject due to 503");
 
   // The observer should have fired - check it got the value in the response.
   Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
   // Sync will have the value in ms with some slop - so check it is at least that.
   Assert.ok(Status.backoffInterval >= 100000);
 });
 
@@ -521,17 +483,17 @@ add_task(async function test_getKeysErro
       status: 503,
       headers: {"content-type": "application/json",
                 "retry-after": "100"},
       body: "{}",
     };
   });
 
   let browseridManager = Service.identity;
-  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
+  await Assert.rejects(browseridManager._ensureValidToken(),
                        "should reject due to 503");
 
   // The observer should have fired - check it got the value in the response.
   Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
   // Sync will have the value in ms with some slop - so check it is at least that.
   Assert.ok(Status.backoffInterval >= 100000);
 });
 
@@ -653,26 +615,18 @@ add_task(async function test_getKeysMiss
       validUntil: fxa.internal.now() + CERT_LIFETIME,
       cert: "certificate",
     };
     return Promise.resolve(this.cert.cert);
   };
 
   browseridManager._fxaService = fxa;
 
-  await browseridManager.initializeWithCurrentIdentity();
-
-  let ex;
-  try {
-    await browseridManager.whenReadyToAuthenticate.promise;
-  } catch (e) {
-    ex = e;
-  }
-
-  Assert.equal(ex.message, "user data missing: kSync, kXCS, kExtSync, kExtKbHash");
+  await Assert.rejects(browseridManager._ensureValidToken(),
+                       /user data missing: kSync, kXCS, kExtSync, kExtKbHash/);
 });
 
 add_task(async function test_signedInUserMissing() {
   _("BrowserIDManager detects getSignedInUser returning incomplete account data");
 
   let browseridManager = new BrowserIDManager();
   // Delete stored keys and the key fetch token.
   delete globalIdentityConfig.fxaccount.user.kSync;
@@ -696,16 +650,17 @@ add_task(async function test_signedInUse
       }
       let storageManager = new MockFxaStorageManager();
       storageManager.initialize(globalIdentityConfig.fxaccount.user);
       return new AccountState(storageManager);
     },
   });
 
   browseridManager._fxaService = fxa;
+  browseridManager._signedInUser = await fxa.getSignedInUser();
 
   let status = await browseridManager.unlockAndVerifyAuthState();
   Assert.equal(status, LOGIN_FAILED_LOGIN_REJECTED);
 });
 
 // End of tests
 // Utility functions follow
 
@@ -769,19 +724,18 @@ async function initializeIdentityWithHAW
       let storageManager = new MockFxaStorageManager();
       storageManager.initialize(config.fxaccount.user);
       return new AccountState(storageManager);
     },
   };
   let fxa = new FxAccounts(internal);
 
   globalBrowseridManager._fxaService = fxa;
-  globalBrowseridManager._signedInUser = null;
-  await globalBrowseridManager.initializeWithCurrentIdentity();
-  await Assert.rejects(globalBrowseridManager.whenReadyToAuthenticate.promise,
+  globalBrowseridManager._signedInUser = await fxa.getSignedInUser();
+  await Assert.rejects(globalBrowseridManager._ensureValidToken(true),
                        "expecting rejection due to hawk error");
 }
 
 
 function getTimestamp(hawkAuthHeader) {
   return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
 }
 
--- a/services/sync/tests/unit/test_errorhandler_1.js
+++ b/services/sync/tests/unit/test_errorhandler_1.js
@@ -360,17 +360,17 @@ add_task(function test_shouldReportLogin
 
 add_task(async function test_login_syncAndReportErrors_non_network_error() {
   enableValidationPrefs();
 
   // Test non-network errors are reported
   // when calling syncAndReportErrors
   let server = await EHTestsCommon.sync_httpd_setup();
   await EHTestsCommon.setUp(server);
-  Service.identity.resetSyncKeyBundle();
+  Service.identity._syncKeyBundle = null;
 
   let promiseObserved = promiseOneObserver("weave:ui:login:error");
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   errorHandler.syncAndReportErrors();
   await promiseObserved;
   Assert.equal(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
 
@@ -413,17 +413,17 @@ add_task(async function test_sync_syncAn
 
 add_task(async function test_login_syncAndReportErrors_prolonged_non_network_error() {
   enableValidationPrefs();
 
   // Test prolonged, non-network errors are
   // reported when calling syncAndReportErrors.
   let server = await EHTestsCommon.sync_httpd_setup();
   await EHTestsCommon.setUp(server);
-  Service.identity.resetSyncKeyBundle();
+  Service.identity._syncKeyBundle = null;
 
   let promiseObserved = promiseOneObserver("weave:ui:login:error");
 
   setLastSync(PROLONGED_ERROR_DURATION);
   errorHandler.syncAndReportErrors();
   await promiseObserved;
   Assert.equal(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
 
@@ -538,17 +538,17 @@ add_task(async function test_sync_syncAn
 });
 
 add_task(async function test_login_prolonged_non_network_error() {
   enableValidationPrefs();
 
   // Test prolonged, non-network errors are reported
   let server = await EHTestsCommon.sync_httpd_setup();
   await EHTestsCommon.setUp(server);
-  Service.identity.resetSyncKeyBundle();
+  Service.identity._syncKeyBundle = null;
 
   let promiseObserved = promiseOneObserver("weave:ui:login:error");
 
   setLastSync(PROLONGED_ERROR_DURATION);
   await Service.sync();
   await promiseObserved;
   Assert.equal(Status.sync, PROLONGED_SYNC_FAILURE);
   Assert.ok(errorHandler.didReportProlongedError);
@@ -625,17 +625,17 @@ add_task(async function test_sync_prolon
 });
 
 add_task(async function test_login_non_network_error() {
   enableValidationPrefs();
 
   // Test non-network errors are reported
   let server = await EHTestsCommon.sync_httpd_setup();
   await EHTestsCommon.setUp(server);
-  Service.identity.resetSyncKeyBundle();
+  Service.identity._syncKeyBundle = null;
 
   let promiseObserved = promiseOneObserver("weave:ui:login:error");
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   await Service.sync();
   await promiseObserved;
   Assert.equal(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
   Assert.ok(!errorHandler.didReportProlongedError);
--- a/services/sync/tests/unit/test_fxa_node_reassignment.js
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -20,17 +20,16 @@ ChromeUtils.import("resource://gre/modul
 
 add_task(async function setup() {
   // Disables all built-in engines. Important for avoiding errors thrown by the
   // add-ons engine.
   await Service.engineManager.clear();
 
   // Setup the FxA identity manager and cluster manager.
   Status.__authManager = Service.identity = new BrowserIDManager();
-  Service._clusterManager = Service.identity.createClusterManager(Service);
 
   // None of the failures in this file should result in a UI error.
   function onUIError() {
     do_throw("Errors should not be presented in the UI.");
   }
   Svc.Obs.add("weave:ui:login:error", onUIError);
   Svc.Obs.add("weave:ui:sync:error", onUIError);
 });
@@ -51,18 +50,19 @@ function prepareServer(cbAfterTokenFetch
   syncTestLogging();
   let config = makeIdentityConfig({username: "johndoe"});
   // A server callback to ensure we don't accidentally hit the wrong endpoint
   // after a node reassignment.
   let callback = {
     __proto__: SyncServerCallback,
     onRequest(req, resp) {
       let full = `${req.scheme}://${req.host}:${req.port}${req.path}`;
-      Assert.ok(full.startsWith(config.fxaccount.token.endpoint),
-                `request made to ${full}`);
+      let expected = config.fxaccount.token.endpoint;
+      Assert.ok(full.startsWith(expected),
+                `request made to ${full}, expected ${expected}`);
     }
   };
   let server = new SyncServer(callback);
   server.registerUser("johndoe");
   server.start();
 
   // Set the token endpoint for the initial token request that's done implicitly
   // via configureIdentity.
@@ -76,16 +76,17 @@ function prepareServer(cbAfterTokenFetch
           // Build a new URL with trailing zeros for the SYNC_VERSION part - this
           // will still be seen as equivalent by the test server, but different
           // by sync itself.
           numReassigns += 1;
           let trailingZeros = new Array(numReassigns + 1).join("0");
           let token = config.fxaccount.token;
           token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
           token.uid = config.username;
+          _(`test server saw token fetch - endpoint now ${token.endpoint}`);
           numTokenRequests += 1;
           res(token);
           if (cbAfterTokenFetch) {
             cbAfterTokenFetch();
           }
         });
       },
     };
--- a/services/sync/tests/unit/test_fxa_service_cluster.js
+++ b/services/sync/tests/unit/test_fxa_service_cluster.js
@@ -10,34 +10,26 @@ add_task(async function test_findCluster
 
   _("_findCluster() throws on 500 errors.");
   initializeIdentityWithTokenServerResponse({
     status: 500,
     headers: [],
     body: "",
   });
 
-  await Service.identity.initializeWithCurrentIdentity();
-  await Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
-                       "should reject due to 500");
-
-  await Assert.rejects(Service._clusterManager._findCluster());
+  await Assert.rejects(Service.identity._findCluster());
 
   _("_findCluster() returns null on authentication errors.");
   initializeIdentityWithTokenServerResponse({
     status: 401,
     headers: {"content-type": "application/json"},
     body: "{}",
   });
 
-  await Service.identity.initializeWithCurrentIdentity();
-  await Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
-                       "should reject due to 401");
-
-  let cluster = await Service._clusterManager._findCluster();
+  let cluster = await Service.identity._findCluster();
   Assert.strictEqual(cluster, null);
 
   _("_findCluster() works with correct tokenserver response.");
   let endpoint = "http://example.com/something";
   initializeIdentityWithTokenServerResponse({
     status: 200,
     headers: {"content-type": "application/json"},
     body:
@@ -45,16 +37,14 @@ add_task(async function test_findCluster
         api_endpoint: endpoint,
         duration: 300,
         id: "id",
         key: "key",
         uid: "uid",
       })
   });
 
-  await Service.identity.initializeWithCurrentIdentity();
-  await Service.identity.whenReadyToAuthenticate.promise;
-  cluster = await Service._clusterManager._findCluster();
+  cluster = await Service.identity._findCluster();
   // The cluster manager ensures a trailing "/"
   Assert.strictEqual(cluster, endpoint + "/");
 
   Svc.Prefs.resetBranch("");
 });
--- a/services/sync/tests/unit/test_keys.js
+++ b/services/sync/tests/unit/test_keys.js
@@ -116,22 +116,19 @@ add_test(function test_repeated_hmac() {
 
   run_next_test();
 });
 
 add_task(async function test_ensureLoggedIn() {
   let log = Log.repository.getLogger("Test");
   Log.repository.rootLogger.addAppender(new Log.DumpAppender());
 
-  let identityConfig = makeIdentityConfig();
-  let browseridManager = new BrowserIDManager();
-  configureFxAccountIdentity(browseridManager, identityConfig);
-  await browseridManager.ensureLoggedIn();
+  await configureIdentity();
 
-  let keyBundle = browseridManager.syncKeyBundle;
+  let keyBundle = Weave.Service.identity.syncKeyBundle;
 
   /*
    * Build a test version of storage/crypto/keys.
    * Encrypt it with the sync key.
    * Pass it into the CollectionKeyManager.
    */
 
   log.info("Building storage keys...");
--- a/services/sync/tests/unit/test_service_cluster.js
+++ b/services/sync/tests/unit/test_service_cluster.js
@@ -11,47 +11,46 @@ add_task(async function test_findCluster
   try {
     let whenReadyToAuthenticate = PromiseUtils.defer();
     Service.identity.whenReadyToAuthenticate = whenReadyToAuthenticate;
     whenReadyToAuthenticate.resolve(true);
 
     Service.identity._ensureValidToken = () => Promise.reject(new Error("Connection refused"));
 
     _("_findCluster() throws on network errors (e.g. connection refused).");
-    await Assert.rejects(Service._clusterManager._findCluster());
+    await Assert.rejects(Service.identity._findCluster());
 
-    Service.identity._ensureValidToken = () => Promise.resolve(true);
-    Service.identity._token = { endpoint: "http://weave.user.node" };
+    Service.identity._ensureValidToken = () => Promise.resolve({ endpoint: "http://weave.user.node" });
 
     _("_findCluster() returns the user's cluster node");
-    let cluster = await Service._clusterManager._findCluster();
+    let cluster = await Service.identity._findCluster();
     Assert.equal(cluster, "http://weave.user.node/");
 
   } finally {
     Svc.Prefs.resetBranch("");
   }
 });
 
 add_task(async function test_setCluster() {
   syncTestLogging();
   _("Test Service._setCluster()");
   try {
     _("Check initial state.");
     Assert.equal(Service.clusterURL, "");
 
-    Service._clusterManager._findCluster = () => "http://weave.user.node/";
+    Service.identity._findCluster = () => "http://weave.user.node/";
 
     _("Set the cluster URL.");
-    Assert.ok((await Service._clusterManager.setCluster()));
+    Assert.ok((await Service.identity.setCluster()));
     Assert.equal(Service.clusterURL, "http://weave.user.node/");
 
     _("Setting it again won't make a difference if it's the same one.");
-    Assert.ok(!(await Service._clusterManager.setCluster()));
+    Assert.ok(!(await Service.identity.setCluster()));
     Assert.equal(Service.clusterURL, "http://weave.user.node/");
 
     _("A 'null' response won't make a difference either.");
-    Service._clusterManager._findCluster = () => null;
-    Assert.ok(!(await Service._clusterManager.setCluster()));
+    Service.identity._findCluster = () => null;
+    Assert.ok(!(await Service.identity.setCluster()));
     Assert.equal(Service.clusterURL, "http://weave.user.node/");
   } finally {
     Svc.Prefs.resetBranch("");
   }
 });
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -79,17 +79,17 @@ add_task(async function test_verifyLogin
     Assert.ok(Service.status.enforceBackoff);
     Assert.equal(backoffInterval, 42);
     Assert.equal(Service.status.service, LOGIN_FAILED);
     Assert.equal(Service.status.login, SERVER_MAINTENANCE);
 
     _("Ensure a network error when finding the cluster sets the right Status bits.");
     Service.status.resetSync();
     Service.clusterURL = "";
-    Service._clusterManager._findCluster = () => "http://localhost:12345/";
+    Service.identity._findCluster = () => "http://localhost:12345/";
     Assert.equal(false, (await Service.verifyLogin()));
     Assert.equal(Service.status.service, LOGIN_FAILED);
     Assert.equal(Service.status.login, LOGIN_FAILED_NETWORK_ERROR);
 
     _("Ensure a network error when getting the collection info sets the right Status bits.");
     Service.status.resetSync();
     Service.clusterURL = "http://localhost:12345/";
     Assert.equal(false, (await Service.verifyLogin()));
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -526,16 +526,19 @@ add_task(async function test_autoconnect
   Utils.ensureMPUnlocked = () => {
     _("Faking Master Password entry cancelation.");
     return false;
   };
   let origFxA = Service.identity._fxaService;
   Service.identity._fxaService = new FxAccounts({
     canGetKeys() {
       return false;
+    },
+    getSignedInUser() {
+      return origFxA.getSignedInUser();
     }
   });
 
   // A locked master password will still trigger a sync, but then we'll hit
   // MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL.
   let promiseObserved = promiseOneObserver("weave:service:login:error");
 
   scheduler.delayedAutoConnect(0);
@@ -711,27 +714,27 @@ add_task(async function test_back_deboun
 add_task(async function test_no_sync_node() {
   enableValidationPrefs();
 
   // Test when Status.sync == NO_SYNC_NODE_FOUND
   // it is not overwritten on sync:finish
   let server = sync_httpd_setup();
   await setUp(server);
 
-  let oldfc = Service._clusterManager._findCluster;
-  Service._clusterManager._findCluster = () => null;
+  let oldfc = Service.identity._findCluster;
+  Service.identity._findCluster = () => null;
   Service.clusterURL = "";
   try {
     await Service.sync();
     Assert.equal(Status.sync, NO_SYNC_NODE_FOUND);
     Assert.equal(scheduler.syncTimer.delay, NO_SYNC_NODE_INTERVAL);
 
     await cleanUpAndGo(server);
   } finally {
-    Service._clusterManager._findCluster = oldfc;
+    Service.identity._findCluster = oldfc;
   }
 });
 
 add_task(async function test_sync_failed_partial_500s() {
   enableValidationPrefs();
 
   _("Test a 5xx status calls handleSyncError.");
   scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF;
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -24,17 +24,17 @@
   "BootstrapMonitor.jsm": ["monitor"],
   "browser-loader.js": ["BrowserLoader"],
   "browserid_identity.js": ["BrowserIDManager", "AuthenticationError"],
   "CertUtils.jsm": ["BadCertHandler", "checkCert", "readCertPrefs", "validateCert"],
   "clients.js": ["ClientEngine", "ClientsRec"],
   "collection_repair.js": ["getRepairRequestor", "getAllRepairRequestors", "CollectionRepairRequestor", "getRepairResponder", "CollectionRepairResponder"],
   "collection_validator.js": ["CollectionValidator", "CollectionProblemData"],
   "Console.jsm": ["console", "ConsoleAPI"],
-  "constants.js": ["WEAVE_VERSION", "SYNC_API_VERSION", "STORAGE_VERSION", "PREFS_BRANCH", "PWDMGR_HOST", "PWDMGR_PASSWORD_REALM", "PWDMGR_PASSPHRASE_REALM", "PWDMGR_KEYBUNDLE_REALM", "DEFAULT_KEYBUNDLE_NAME", "SYNC_KEY_ENCODED_LENGTH", "SYNC_KEY_DECODED_LENGTH", "NO_SYNC_NODE_INTERVAL", "MAX_ERROR_COUNT_BEFORE_BACKOFF", "MINIMUM_BACKOFF_INTERVAL", "MAXIMUM_BACKOFF_INTERVAL", "HMAC_EVENT_INTERVAL", "MASTER_PASSWORD_LOCKED_RETRY_INTERVAL", "DEFAULT_GUID_FETCH_BATCH_SIZE", "DEFAULT_DOWNLOAD_BATCH_SIZE", "SINGLE_USER_THRESHOLD", "MULTI_DEVICE_THRESHOLD", "SCORE_INCREMENT_SMALL", "SCORE_INCREMENT_MEDIUM", "SCORE_INCREMENT_XLARGE", "SCORE_UPDATE_DELAY", "IDLE_OBSERVER_BACK_DELAY", "URI_LENGTH_MAX", "MAX_HISTORY_UPLOAD", "MAX_HISTORY_DOWNLOAD", "STATUS_OK", "SYNC_FAILED", "LOGIN_FAILED", "SYNC_FAILED_PARTIAL", "CLIENT_NOT_CONFIGURED", "STATUS_DISABLED", "MASTER_PASSWORD_LOCKED", "LOGIN_SUCCEEDED", "SYNC_SUCCEEDED", "ENGINE_SUCCEEDED", "LOGIN_FAILED_NO_USERNAME", "LOGIN_FAILED_NO_PASSPHRASE", "LOGIN_FAILED_NETWORK_ERROR", "LOGIN_FAILED_SERVER_ERROR", "LOGIN_FAILED_INVALID_PASSPHRASE", "LOGIN_FAILED_LOGIN_REJECTED", "METARECORD_DOWNLOAD_FAIL", "VERSION_OUT_OF_DATE", "CREDENTIALS_CHANGED", "ABORT_SYNC_COMMAND", "NO_SYNC_NODE_FOUND", "OVER_QUOTA", "PROLONGED_SYNC_FAILURE", "SERVER_MAINTENANCE", "RESPONSE_OVER_QUOTA", "ENGINE_UPLOAD_FAIL", "ENGINE_DOWNLOAD_FAIL", "ENGINE_UNKNOWN_FAIL", "ENGINE_APPLY_FAIL", "ENGINE_BATCH_INTERRUPTED", "kSyncMasterPasswordLocked", "kSyncWeaveDisabled", "kSyncNetworkOffline", "kSyncBackoffNotMet", "kFirstSyncChoiceNotMade", "kSyncNotConfigured", "kFirefoxShuttingDown", "DEVICE_TYPE_DESKTOP", "DEVICE_TYPE_MOBILE", "SQLITE_MAX_VARIABLE_NUMBER"],
+  "constants.js": ["WEAVE_VERSION", "SYNC_API_VERSION", "STORAGE_VERSION", "PREFS_BRANCH", "DEFAULT_KEYBUNDLE_NAME", "SYNC_KEY_ENCODED_LENGTH", "SYNC_KEY_DECODED_LENGTH", "NO_SYNC_NODE_INTERVAL", "MAX_ERROR_COUNT_BEFORE_BACKOFF", "MINIMUM_BACKOFF_INTERVAL", "MAXIMUM_BACKOFF_INTERVAL", "HMAC_EVENT_INTERVAL", "MASTER_PASSWORD_LOCKED_RETRY_INTERVAL", "DEFAULT_GUID_FETCH_BATCH_SIZE", "DEFAULT_DOWNLOAD_BATCH_SIZE", "SINGLE_USER_THRESHOLD", "MULTI_DEVICE_THRESHOLD", "SCORE_INCREMENT_SMALL", "SCORE_INCREMENT_MEDIUM", "SCORE_INCREMENT_XLARGE", "SCORE_UPDATE_DELAY", "IDLE_OBSERVER_BACK_DELAY", "URI_LENGTH_MAX", "MAX_HISTORY_UPLOAD", "MAX_HISTORY_DOWNLOAD", "STATUS_OK", "SYNC_FAILED", "LOGIN_FAILED", "SYNC_FAILED_PARTIAL", "CLIENT_NOT_CONFIGURED", "STATUS_DISABLED", "MASTER_PASSWORD_LOCKED", "LOGIN_SUCCEEDED", "SYNC_SUCCEEDED", "ENGINE_SUCCEEDED", "LOGIN_FAILED_NO_USERNAME", "LOGIN_FAILED_NO_PASSPHRASE", "LOGIN_FAILED_NETWORK_ERROR", "LOGIN_FAILED_SERVER_ERROR", "LOGIN_FAILED_INVALID_PASSPHRASE", "LOGIN_FAILED_LOGIN_REJECTED", "METARECORD_DOWNLOAD_FAIL", "VERSION_OUT_OF_DATE", "CREDENTIALS_CHANGED", "ABORT_SYNC_COMMAND", "NO_SYNC_NODE_FOUND", "OVER_QUOTA", "PROLONGED_SYNC_FAILURE", "SERVER_MAINTENANCE", "RESPONSE_OVER_QUOTA", "ENGINE_UPLOAD_FAIL", "ENGINE_DOWNLOAD_FAIL", "ENGINE_UNKNOWN_FAIL", "ENGINE_APPLY_FAIL", "ENGINE_BATCH_INTERRUPTED", "kSyncMasterPasswordLocked", "kSyncWeaveDisabled", "kSyncNetworkOffline", "kSyncBackoffNotMet", "kFirstSyncChoiceNotMade", "kSyncNotConfigured", "kFirefoxShuttingDown", "DEVICE_TYPE_DESKTOP", "DEVICE_TYPE_MOBILE", "SQLITE_MAX_VARIABLE_NUMBER"],
   "Constants.jsm": ["Roles", "Events", "Relations", "Filters", "States", "Prefilters"],
   "ContactDB.jsm": ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME", "REVISION_STORE", "DB_VERSION"],
   "content-server.jsm": ["init"],
   "content.jsm": ["registerContentFrame"],
   "ContentCrashHandlers.jsm": ["TabCrashHandler", "PluginCrashReporter", "UnsubmittedCrashHandler"],
   "ContentObservers.js": [],
   "ContentPrefUtils.jsm": ["ContentPref", "cbHandleResult", "cbHandleError", "cbHandleCompletion", "safeCallback", "_methodsCallableFromChild"],
   "cookies.js": ["Cookies"],