Bug 1044530 - Remove invalid session and key fetch tokens from account storage. r=markh draft
authorKit Cambridge <kcambridge@mozilla.com>
Wed, 20 Jan 2016 18:12:22 -0800
changeset 348204 baf9b7ddf71c146995d579d0b6b350f6e315282e
parent 348040 68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8
child 517810 da03d18050c026c88cbfa8ef8ab9ac5f19191c04
push id14781
push userkcambridge@mozilla.com
push dateWed, 06 Apr 2016 23:37:27 +0000
reviewersmarkh
bugs1044530
milestone48.0a1
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.