Bug 1044530 - Remove invalid session and key fetch tokens from account storage. r=markh
authorKit Cambridge <kcambridge@mozilla.com>
Wed, 20 Jan 2016 18:12:22 -0800
changeset 292120 3e64199755c5692e10c13068cf96ad5210df3e35
parent 292119 133d8859d7b5e7d44d74c66aaa52c2da30cab3c4
child 292121 572875411dbaa092bf066a9ce561da6a2fc1c293
push id74762
push usercbook@mozilla.com
push dateThu, 07 Apr 2016 09:56:20 +0000
treeherdermozilla-inbound@772253c53374 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1044530
milestone48.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 1044530 - Remove invalid session and key fetch tokens from account storage. r=markh MozReview-Commit-ID: DOLlus0At8s
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsCommon.js
services/fxaccounts/tests/xpcshell/test_accounts.js
services/sync/modules-testing/utils.js
services/sync/modules/browserid_identity.js
services/sync/tests/unit/test_browserid_identity.js
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -412,16 +412,20 @@ FxAccountsInternal.prototype = {
   get localtimeOffsetMsec() {
     return this.fxAccountsClient.localtimeOffsetMsec;
   },
 
   /**
    * Ask the server whether the user's email has been verified
    */
   checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) {
+    if (!sessionToken) {
+      return Promise.reject(new Error(
+        "checkEmailStatus called without a session token"));
+    }
     return this.fxAccountsClient.recoveryEmailStatus(sessionToken, options);
   },
 
   /**
    * Once the user's email is verified, we can request the keys
    */
   fetchKeys: function fetchKeys(keyFetchToken) {
     log.debug("fetchKeys: " + !!keyFetchToken);
@@ -570,27 +574,29 @@ FxAccountsInternal.prototype = {
         // No signed-in user
         return null;
       }
       if (!this.isUserEmailVerified(data)) {
         // Signed-in user has not verified email
         return null;
       }
       if (!data.sessionToken) {
-        // can't get a signed certificate without a session token, but that
-        // should be impossible - make log noise about it.
-        log.error("getAssertion called without a session token!");
-        return null;
+        // can't get a signed certificate without a session token. This
+        // can happen if we request an assertion after clearing an invalid
+        // session token from storage.
+        throw this._error(ERROR_AUTH_ERROR, "getAssertion called without a session token");
       }
       return this.getKeypairAndCertificate(currentState).then(
         ({keyPair, certificate}) => {
           return this.getAssertionFromCert(data, keyPair, certificate, audience);
         }
       );
-    }).then(result => currentState.resolve(result));
+    }).catch(err =>
+      this._handleTokenError(err)
+    ).then(result => currentState.resolve(result));
   },
 
   getDeviceId() {
     return this.currentAccountState.getUserAccountData()
       .then(data => {
         if (data) {
           if (data.isDeviceStale || !data.deviceId) {
             // A previous device registration attempt failed or there is no
@@ -614,18 +620,23 @@ FxAccountsInternal.prototype = {
    */
   resendVerificationEmail: function resendVerificationEmail() {
     let currentState = this.currentAccountState;
     return this.getSignedInUser().then(data => {
       // If the caller is asking for verification to be re-sent, and there is
       // no signed-in user to begin with, this is probably best regarded as an
       // error.
       if (data) {
+        if (!data.sessionToken) {
+          return Promise.reject(new Error(
+            "resendVerificationEmail called without a session token"));
+        }
         this.pollEmailStatus(currentState, data.sessionToken, "start");
-        return this.fxAccountsClient.resendVerificationEmail(data.sessionToken);
+        return this.fxAccountsClient.resendVerificationEmail(
+          data.sessionToken).catch(err => this._handleTokenError(err));
       }
       throw new Error("Cannot resend verification email; no signed-in user");
     });
   },
 
   /*
    * Reset state such that any previous flow is canceled.
    */
@@ -705,17 +716,20 @@ FxAccountsInternal.prototype = {
       // to FxAccountsClient.signOut().
       if (!localOnly) {
         // Wrap this in a promise so *any* errors in signOut won't
         // block the local sign out. This is *not* returned.
         Promise.resolve().then(() => {
           // This can happen in the background and shouldn't block
           // the user from signing out. The server must tolerate
           // clients just disappearing, so this call should be best effort.
-          return this._signOutServer(sessionToken, deviceId);
+          if (sessionToken) {
+            return this._signOutServer(sessionToken, deviceId);
+          }
+          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(() => {
           // just for testing - notifications are cheap when no observers.
@@ -747,18 +761,17 @@ FxAccountsInternal.prototype = {
     // we must tell the server to either destroy the device or sign out
     // (if no device exists). We might need to revisit this when this
     // FxA code is used in a context that isn't Sync.
 
     const options = { service: "sync" };
 
     if (deviceId) {
       log.debug("destroying device and session");
-      return this.fxAccountsClient.signOutAndDestroyDevice(sessionToken, deviceId, options)
-        .then(() => this.currentAccountState.updateUserAccountData({ deviceId: null }));
+      return this.fxAccountsClient.signOutAndDestroyDevice(sessionToken, deviceId, options);
     }
 
     log.debug("destroying session");
     return this.fxAccountsClient.signOut(sessionToken, options);
   },
 
   /**
    * Fetch encryption keys for the signed-in-user from the FxA API server.
@@ -805,17 +818,19 @@ FxAccountsInternal.prototype = {
               currentState.whenKeysReadyDeferred.reject(err);
             }
           );
         } else {
           currentState.whenKeysReadyDeferred.reject('No keyFetchToken');
         }
       }
       return currentState.whenKeysReadyDeferred.promise;
-    }).then(result => currentState.resolve(result));
+    }).catch(err =>
+      this._handleTokenError(err)
+    ).then(result => currentState.resolve(result));
    },
 
   fetchAndUnwrapKeys: function(keyFetchToken) {
     if (logPII) {
       log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
     }
     let currentState = this.currentAccountState;
     return Task.spawn(function* task() {
@@ -1110,28 +1125,35 @@ FxAccountsInternal.prototype = {
         if (error && error.retryAfter) {
           // If the server told us to back off, back off the requested amount.
           timeoutMs = (error.retryAfter + 3) * 1000;
         }
         // The server will return 401 if a request parameter is erroneous or
         // if the session token expired. Let's continue polling otherwise.
         if (!error || !error.code || error.code != 401) {
           this.pollEmailStatusAgain(currentState, sessionToken, timeoutMs);
+        } else {
+          let error = new Error("Verification status check failed");
+          this._rejectWhenVerified(currentState, error);
         }
       });
   },
 
+  _rejectWhenVerified(currentState, error) {
+    currentState.whenVerifiedDeferred.reject(error);
+    delete currentState.whenVerifiedDeferred;
+  },
+
   // Poll email status using truncated exponential back-off.
   pollEmailStatusAgain: function (currentState, sessionToken, timeoutMs) {
     let ageMs = Date.now() - this.pollStartDate;
     if (ageMs >= this.POLL_SESSION) {
       if (currentState.whenVerifiedDeferred) {
         let error = new Error("User email verification timed out.");
-        currentState.whenVerifiedDeferred.reject(error);
-        delete currentState.whenVerifiedDeferred;
+        this._rejectWhenVerified(currentState, error);
       }
       log.debug("polling session exceeded, giving up");
       return;
     }
     if (timeoutMs === undefined) {
       let currentMinute = Math.ceil(ageMs / 60000);
       timeoutMs = currentMinute <= 2 ? this.VERIFICATION_POLL_TIMEOUT_INITIAL
                                      : this.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT;
@@ -1380,17 +1402,18 @@ FxAccountsInternal.prototype = {
    */
   _errorToErrorClass: function (aError) {
     if (aError.errno) {
       let error = SERVER_ERRNO_TO_ERROR[aError.errno];
       return this._error(ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN, aError);
     } else if (aError.message &&
         (aError.message === "INVALID_PARAMETER" ||
         aError.message === "NO_ACCOUNT" ||
-        aError.message === "UNVERIFIED_ACCOUNT")) {
+        aError.message === "UNVERIFIED_ACCOUNT" ||
+        aError.message === "AUTH_ERROR")) {
       return aError;
     }
     return this._error(ERROR_UNKNOWN, aError);
   },
 
   _error: function(aError, aDetails) {
     log.error("FxA rejecting with error ${aError}, details: ${aDetails}", {aError, aDetails});
     let reason = new Error(aError);
@@ -1453,16 +1476,21 @@ FxAccountsInternal.prototype = {
       //   1. It makes remote requests to the auth server.
       //   2. _getDeviceName does not work from xpcshell.
       //   3. The B2G tests fail when attempting to import services-sync/util.js.
       if (Services.prefs.getBoolPref("identity.fxaccounts.skipDeviceRegistration")) {
         return Promise.resolve();
       }
     } catch(ignore) {}
 
+    if (!signedInUser.sessionToken) {
+      return Promise.reject(new Error(
+        "_registerOrUpdateDevice called without a session token"));
+    }
+
     return this.fxaPushService.registerPushEndpoint().then(subscription => {
       const deviceName = this._getDeviceName();
       let deviceOptions = {};
 
       // if we were able to obtain a subscription
       if (subscription && subscription.endpoint) {
         deviceOptions.pushCallback = subscription.endpoint;
       }
@@ -1499,18 +1527,21 @@ FxAccountsInternal.prototype = {
           return this._recoverFromUnknownDevice();
         }
 
         if (error.errno === ERRNO_DEVICE_SESSION_CONFLICT) {
           return this._recoverFromDeviceSessionConflict(error, sessionToken);
         }
       }
 
-      return this._logErrorAndSetStaleDeviceFlag(error);
-    }).catch(() => {});
+      // `_handleTokenError` re-throws the error.
+      return this._handleTokenError(error);
+    }).catch(error =>
+      this._logErrorAndSetStaleDeviceFlag(error)
+    ).catch(() => {});
   },
 
   _recoverFromUnknownDevice() {
     // FxA did not recognise the device id. Handle it by clearing the device
     // id on the account data. At next sync or next sign-in, registration is
     // retried and should succeed.
     log.warn("unknown device id, clearing the local device data");
     return this.currentAccountState.updateUserAccountData({ deviceId: null })
@@ -1556,17 +1587,48 @@ FxAccountsInternal.prototype = {
     log.error("device registration failed", error);
     return this.currentAccountState.updateUserAccountData({
       isDeviceStale: true
     }).catch(secondError => {
       log.error(
         "failed to set stale device flag, device registration won't be retried",
         secondError);
     }).then(() => {});
-  }
+  },
+
+  _handleTokenError(err) {
+    if (!err || err.code != 401 || err.errno != ERRNO_INVALID_AUTH_TOKEN) {
+      throw err;
+    }
+    log.warn("recovering from invalid token error", err);
+    return this.accountStatus().then(exists => {
+      if (!exists) {
+        // Delete all local account data. Since the account no longer
+        // exists, we can skip the remote calls.
+        log.info("token invalidated because the account no longer exists");
+        return this.signOut(true);
+      }
+
+      // Delete all fields except those required for the user to
+      // reauthenticate.
+      log.info("clearing credentials to handle invalid token error");
+      let updateData = {};
+      let clearField = field => {
+        if (!FXA_PWDMGR_REAUTH_WHITELIST.has(field)) {
+          updateData[field] = null;
+        }
+      }
+      FXA_PWDMGR_PLAINTEXT_FIELDS.forEach(clearField);
+      FXA_PWDMGR_SECURE_FIELDS.forEach(clearField);
+      FXA_PWDMGR_MEMORY_FIELDS.forEach(clearField);
+
+      let currentState = this.currentAccountState;
+      return currentState.updateUserAccountData(updateData);
+    }).then(() => Promise.reject(err));
+  },
 };
 
 
 // A getter for the instance to export
 XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
   let a = new FxAccounts();
 
   // XXX Bug 947061 - We need a strategy for resuming email verification after
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -234,16 +234,21 @@ exports.FXA_PWDMGR_PLAINTEXT_FIELDS = ne
 // Fields we store in secure storage if it exists.
 exports.FXA_PWDMGR_SECURE_FIELDS = new Set(
   ["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]);
 
 // Fields we keep in memory and don't persist anywhere.
 exports.FXA_PWDMGR_MEMORY_FIELDS = new Set(
   ["cert", "keyPair"]);
 
+// A whitelist of fields that remain in storage when the user needs to
+// reauthenticate. All other fields will be removed.
+exports.FXA_PWDMGR_REAUTH_WHITELIST = new Set(
+  ["email", "uid", "profile", "deviceId", "isDeviceStale", "verified"]);
+
 // The pseudo-host we use in the login manager
 exports.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts";
 // The realm we use in the login manager.
 exports.FXA_PWDMGR_REALM = "Firefox Accounts credentials";
 
 // Error matching.
 exports.SERVER_ERRNO_TO_ERROR = {};
 
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -548,16 +548,81 @@ add_test(function test_getKeys() {
           do_check_eq(user.unwrapBKey, undefined);
           run_next_test();
         });
       });
     });
   });
 });
 
+add_task(function* test_getKeys_nonexistent_account() {
+  let fxa = new MockFxAccounts();
+  let bismarck = getTestUser("bismarck");
+
+  let client = fxa.internal.fxAccountsClient;
+  client.accountStatus = () => Promise.resolve(false);
+  client.accountKeys = () => {
+    return Promise.reject({
+      code: 401,
+      errno: ERRNO_INVALID_AUTH_TOKEN,
+    });
+  };
+
+  yield fxa.setSignedInUser(bismarck);
+
+  let promiseLogout = new Promise(resolve => {
+    makeObserver(ONLOGOUT_NOTIFICATION, function() {
+      log.debug("test_getKeys_nonexistent_account observed logout");
+      resolve();
+    });
+  });
+
+  try {
+    yield fxa.internal.getKeys();
+    do_check_true(false);
+  } catch (err) {
+    do_check_eq(err.code, 401);
+    do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN);
+  }
+
+  yield promiseLogout;
+
+  let user = yield fxa.internal.getUserAccountData();
+  do_check_eq(user, null);
+});
+
+// getKeys with invalid keyFetchToken should delete keyFetchToken from storage
+add_task(function* test_getKeys_invalid_token() {
+  let fxa = new MockFxAccounts();
+  let yusuf = getTestUser("yusuf");
+
+  let client = fxa.internal.fxAccountsClient;
+  client.accountStatus = () => Promise.resolve(true);
+  client.accountKeys = () => {
+    return Promise.reject({
+      code: 401,
+      errno: ERRNO_INVALID_AUTH_TOKEN,
+    });
+  };
+
+  yield fxa.setSignedInUser(yusuf);
+
+  try {
+    yield fxa.internal.getKeys();
+    do_check_true(false);
+  } catch (err) {
+    do_check_eq(err.code, 401);
+    do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN);
+  }
+
+  let user = yield fxa.internal.getUserAccountData();
+  do_check_eq(user.email, yusuf.email);
+  do_check_eq(user.keyFetchToken, null);
+});
+
 //  fetchAndUnwrapKeys with no keyFetchToken should trigger signOut
 add_test(function test_fetchAndUnwrapKeys_no_token() {
   let fxa = new MockFxAccounts();
   let user = getTestUser("lettuce.protheroe");
   delete user.keyFetchToken
 
   makeObserver(ONLOGOUT_NOTIFICATION, function() {
     log.debug("test_fetchAndUnwrapKeys_no_token observed logout");
@@ -612,16 +677,49 @@ add_test(function test_overlapping_signi
           log.debug("Bob verifying his email ...");
           fxa.internal.fxAccountsClient._verified = true;
         });
       });
     });
   });
 });
 
+add_task(function* test_getAssertion_invalid_token() {
+  let fxa = new MockFxAccounts();
+
+  let client = fxa.internal.fxAccountsClient;
+  client.accountStatus = () => Promise.resolve(true);
+
+  let creds = {
+    sessionToken: "sessionToken",
+    kA: expandHex("11"),
+    kB: expandHex("66"),
+    verified: true,
+    email: "sonia@example.com",
+  };
+  yield fxa.setSignedInUser(creds);
+
+  try {
+    let promiseAssertion = fxa.getAssertion("audience.example.com");
+    fxa.internal._d_signCertificate.reject({
+      code: 401,
+      errno: ERRNO_INVALID_AUTH_TOKEN,
+    });
+    yield promiseAssertion;
+    do_check_true(false, "getAssertion should reject invalid session token");
+  } catch (err) {
+    do_check_eq(err.code, 401);
+    do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN);
+  }
+
+  let user = yield fxa.internal.getUserAccountData();
+  do_check_eq(user.email, creds.email);
+  do_check_eq(user.sessionToken, null);
+});
+
 add_task(function* test_getAssertion() {
   let fxa = new MockFxAccounts();
 
   do_check_throws(function* () {
     yield fxa.getAssertion("nonaudience");
   });
 
   let creds = {
@@ -778,16 +876,49 @@ add_test(function test_accountStatus() {
             }
           )
         }
       );
     }
   );
 });
 
+add_task(function* test_resend_email_invalid_token() {
+  let fxa = new MockFxAccounts();
+  let sophia = getTestUser("sophia");
+  do_check_neq(sophia.sessionToken, null);
+
+  let client = fxa.internal.fxAccountsClient;
+  client.resendVerificationEmail = () => {
+    return Promise.reject({
+      code: 401,
+      errno: ERRNO_INVALID_AUTH_TOKEN,
+    });
+  };
+  client.accountStatus = () => Promise.resolve(true);
+
+  yield fxa.setSignedInUser(sophia);
+  let user = yield fxa.internal.getUserAccountData();
+  do_check_eq(user.email, sophia.email);
+  do_check_eq(user.verified, false);
+  log.debug("Sophia wants verification email resent");
+
+  try {
+    yield fxa.resendVerificationEmail();
+    do_check_true(false, "resendVerificationEmail should reject invalid session token");
+  } catch (err) {
+    do_check_eq(err.code, 401);
+    do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN);
+  }
+
+  user = yield fxa.internal.getUserAccountData();
+  do_check_eq(user.email, sophia.email);
+  do_check_eq(user.sessionToken, null);
+});
+
 add_test(function test_resend_email() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
 
   let initialState = fxa.internal.currentAccountState;
 
   // Alice is the user signing in; her email is unverified.
   fxa.setSignedInUser(alice).then(() => {
@@ -913,32 +1044,37 @@ add_task(function* test_sign_out_without
     });
   });
 
   yield fxa.signOut();
 
   yield promise;
 });
 
-add_test(function test_sign_out_with_remote_error() {
+add_task(function* test_sign_out_with_remote_error() {
   let fxa = new MockFxAccounts();
   let client = fxa.internal.fxAccountsClient;
   let remoteSignOutCalled = false;
   // Force remote sign out to trigger an error
-  client.signOut = function() { remoteSignOutCalled = true; throw "Remote sign out error"; };
-  makeObserver(ONLOGOUT_NOTIFICATION, function() {
-    log.debug("test_sign_out_with_remote_error observed onlogout");
-    // user should be undefined after sign out
-    fxa.internal.getUserAccountData().then(user => {
-      do_check_eq(user, null);
-      do_check_true(remoteSignOutCalled);
-      run_next_test();
+  client.signOutAndDestroyDevice = function() { remoteSignOutCalled = true; throw "Remote sign out error"; };
+  let promiseLogout = new Promise(resolve => {
+    makeObserver(ONLOGOUT_NOTIFICATION, function() {
+      log.debug("test_sign_out_with_remote_error observed onlogout");
+      resolve();
     });
   });
-  fxa.signOut();
+
+  let jane = getTestUser("jane");
+  yield fxa.setSignedInUser(jane);
+  yield fxa.signOut();
+  yield promiseLogout;
+
+  let user = yield fxa.internal.getUserAccountData();
+  do_check_eq(user, null);
+  do_check_true(remoteSignOutCalled);
 });
 
 add_test(function test_getOAuthToken() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
   let getTokenFromAssertionCalled = false;
 
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -6,16 +6,17 @@
 
 this.EXPORTED_SYMBOLS = [
   "btoa", // It comes from a module import.
   "encryptPayload",
   "isConfiguredWithLegacyIdentity",
   "ensureLegacyIdentityManager",
   "setBasicCredentials",
   "makeIdentityConfig",
+  "makeFxAccountsInternalMock",
   "configureFxAccountIdentity",
   "configureIdentity",
   "SyncTestingInfrastructure",
   "waitForZeroTimer",
   "Promise", // from a module import
   "add_identity_test",
   "MockFxaStorageManager",
   "AccountState", // from a module import
@@ -174,43 +175,45 @@ this.makeIdentityConfig = function(overr
     if (overrides.fxaccount) {
       // TODO: allow just some attributes to be specified
       result.fxaccount = overrides.fxaccount;
     }
   }
   return result;
 }
 
-// Configure an instance of an FxAccount identity provider with the specified
-// config (or the default config if not specified).
-this.configureFxAccountIdentity = function(authService,
-                                           config = makeIdentityConfig()) {
-  // until we get better test infrastructure for bid_identity, we set the
-  // signedin user's "email" to the username, simply as many tests rely on this.
-  config.fxaccount.user.email = config.username;
-
-  let fxa;
-  let MockInternal = {
+this.makeFxAccountsInternalMock = function(config) {
+  return {
     newAccountState(credentials) {
       // We only expect this to be called with null indicating the (mock)
       // storage should be read.
       if (credentials) {
         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");
     },
+  };
+};
 
-  };
-  fxa = new FxAccounts(MockInternal);
+// Configure an instance of an FxAccount identity provider with the specified
+// config (or the default config if not specified).
+this.configureFxAccountIdentity = function(authService,
+                                           config = makeIdentityConfig(),
+                                           fxaInternal = makeFxAccountsInternalMock(config)) {
+  // until we get better test infrastructure for bid_identity, we set the
+  // signedin user's "email" to the username, simply as many tests rely on this.
+  config.fxaccount.user.email = config.username;
+
+  let fxa = new FxAccounts(fxaInternal);
 
   let MockFxAccountsClient = function() {
     FxAccountsClient.apply(this);
   };
   MockFxAccountsClient.prototype = {
     __proto__: FxAccountsClient.prototype,
     accountStatus() {
       return Promise.resolve(true);
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -240,32 +240,21 @@ this.BrowserIDManager.prototype = {
         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", null);
           Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
         }
-      }).catch(err => {
-        let authErr = err; // note that we must reject with this error and not a
-                           // subsequent one
+      }).catch(authErr => {
         // report what failed...
         this._log.error("Background fetch for key bundle failed", authErr);
-        // check if the account still exists
-        this._fxaService.accountStatus().then(exists => {
-          if (!exists) {
-            return fxAccounts.signOut(true);
-          }
-        }).catch(err => {
-          this._log.error("Error while trying to determine FXA existence", err);
-        }).then(() => {
-          this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
-          this.whenReadyToAuthenticate.reject(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: function(userData) {
@@ -621,16 +610,19 @@ this.BrowserIDManager.prototype = {
         // 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);
         // A hawkclient error.
         } else if (err.code && err.code === 401) {
           err = new AuthenticationError(err);
+        // An FxAccounts.jsm error.
+        } else if (err.message == fxAccountsCommon.ERROR_AUTH_ERROR) {
+          err = new AuthenticationError(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.
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -84,52 +84,61 @@ add_task(function* test_initialializeWit
     do_check_true(browseridManager.hasValidToken());
     do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
   }
 );
 
 add_task(function* test_initialializeWithAuthErrorAndDeletedAccount() {
     _("Verify sync unpair after initializeWithCurrentIdentity 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;
+
+    configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
     browseridManager._fxaService.internal.initialize();
 
-    let fetchTokenForUserCalled = false;
+    let signCertificateCalled = false;
     let accountStatusCalled = false;
 
     let MockFxAccountsClient = function() {
       FxAccountsClient.apply(this);
     };
     MockFxAccountsClient.prototype = {
       __proto__: FxAccountsClient.prototype,
+      signCertificate() {
+        signCertificateCalled = true;
+        return Promise.reject({
+          code: 401,
+          errno: ERRNO_INVALID_AUTH_TOKEN,
+        });
+      },
       accountStatus() {
         accountStatusCalled = true;
         return Promise.resolve(false);
       }
     };
 
     let mockFxAClient = new MockFxAccountsClient();
     browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
 
-    let oldFetchTokenForUser = browseridManager._fetchTokenForUser;
-    browseridManager._fetchTokenForUser = function() {
-      fetchTokenForUserCalled = true;
-      return Promise.reject(false);
-    }
-
     yield browseridManager.initializeWithCurrentIdentity();
     yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                      "should reject due to an auth error");
 
-    do_check_true(fetchTokenForUserCalled);
+    do_check_true(signCertificateCalled);
     do_check_true(accountStatusCalled);
     do_check_false(browseridManager.account);
     do_check_false(browseridManager._token);
     do_check_false(browseridManager.hasValidToken());
     do_check_false(browseridManager.account);
-    browseridManager._fetchTokenForUser = oldFetchTokenForUser;
 });
 
 add_task(function* test_initialializeWithNoKeys() {
     _("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken");
     let identityConfig = makeIdentityConfig();
     delete identityConfig.fxaccount.user.kA;
     delete identityConfig.fxaccount.user.kB;
     // there's no keyFetchToken by default, so the initialize should fail.