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 323714 dfd5feaf7573e64040615a5d09953f6f517e9bdb
parent 323526 6764bc656c1d146962d53710d734c2ac87c2306f
child 325869 418e68d7127d96de89e35cf832ad20a7159d61e8
push id9780
push userkcambridge@mozilla.com
push dateThu, 21 Jan 2016 02:15:17 +0000
reviewersmarkh
bugs1044530
milestone46.0a1
Bug 1044530 - Remove invalid session and key fetch tokens from account storage. r?markh
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/tests/xpcshell/test_accounts.js
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -518,17 +518,17 @@ FxAccountsInternal.prototype = {
     return this._getAssertion(audience);
   },
 
   // getAssertion() is "public" so screws with our mock story. This
   // implementation method *can* be (and is) mocked by tests.
   _getAssertion: function _getAssertion(audience) {
     log.debug("enter getAssertion()");
     let currentState = this.currentAccountState;
-    return currentState.getUserAccountData().then(data => {
+    return this.withValidUserAccountData(data => {
       if (!data) {
         // No signed-in user
         return null;
       }
       if (!this.isUserEmailVerified(data)) {
         // Signed-in user has not verified email
         return null;
       }
@@ -538,16 +538,18 @@ FxAccountsInternal.prototype = {
         log.error("getAssertion called without a session token!");
         return null;
       }
       return this.getKeypairAndCertificate(currentState).then(
         ({keyPair, certificate}) => {
           return this.getAssertionFromCert(data, keyPair, certificate, audience);
         }
       );
+    }, {
+      clearIfInvalid: ["sessionToken", "keyPair", "cert"],
     }).then(result => currentState.resolve(result));
   },
 
   getDeviceId() {
     return this.currentAccountState.getUserAccountData()
       .then(data => {
         if (data) {
           if (data.isDeviceStale || !data.deviceId) {
@@ -567,25 +569,27 @@ FxAccountsInternal.prototype = {
   },
 
   /**
    * Resend the verification email fot the currently signed-in user.
    *
    */
   resendVerificationEmail: function resendVerificationEmail() {
     let currentState = this.currentAccountState;
-    return this.getSignedInUser().then(data => {
+    return this.withValidSignedInUser(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) {
         this.pollEmailStatus(currentState, data.sessionToken, "start");
         return this.fxAccountsClient.resendVerificationEmail(data.sessionToken);
       }
       throw new Error("Cannot resend verification email; no signed-in user");
+    }, {
+      clearIfInvalid: ["sessionToken"],
     });
   },
 
   /*
    * Reset state such that any previous flow is canceled.
    */
   abortExistingFlow: function abortExistingFlow() {
     if (this.currentTimer) {
@@ -623,16 +627,51 @@ FxAccountsInternal.prototype = {
     // let's just destroy them all in parallel...
     let promises = [];
     for (let [key, tokenInfo] in Iterator(tokenInfos || {})) {
       promises.push(this._destroyOAuthToken(tokenInfo));
     }
     return Promise.all(promises);
   },
 
+  withValidSignedInUser(func, options) {
+    return this.getSignedInUser().then(func).catch(err =>
+      this._handleAccountDataError(err, options));
+  },
+
+  withValidUserAccountData(func, options) {
+    return this.currentAccountState.getUserAccountData()
+      .then(func).catch(err => this._handleAccountDataError(err, options));
+  },
+
+  _handleAccountDataError(err, options = {}) {
+    if (err && err.code == 401 && err.errno == ERRNO_INVALID_AUTH_TOKEN) {
+      return this.accountStatus().then(exists => {
+        if (!exists) {
+          // Delete all local account data. Since the account no longer
+          // exists, we can skip the remote calls.
+          return this.signOut(true);
+        }
+        if (options.clearIfInvalid) {
+          // Remove the requested fields from storage.
+          let updateData = {};
+          for (let field of options.clearIfInvalid) {
+            updateData[field] = null;
+          }
+          let currentState = this.currentAccountState;
+          return currentState.updateUserAccountData(updateData).then(() => {
+            // ...And notify our UI observers.
+            this.notifyObservers(ONLOGIN_NOTIFICATION)
+          });
+        }
+      }).then(() => Promise.reject(err));
+    }
+    throw err;
+  },
+
   signOut: function signOut(localOnly) {
     let currentState = this.currentAccountState;
     let sessionToken;
     let tokensToRevoke;
     let deviceId;
     return currentState.getUserAccountData().then(data => {
       // Save the session token, tokens to revoke and the
       // device id for use in the call to signOut below.
@@ -718,17 +757,17 @@ FxAccountsInternal.prototype = {
    *          kA: An encryption key from the FxA server
    *          kB: An encryption key derived from the user's FxA password
    *          verified: email verification status
    *        }
    *        or null if no user is signed in
    */
   getKeys: function() {
     let currentState = this.currentAccountState;
-    return currentState.getUserAccountData().then((userData) => {
+    return this.withValidUserAccountData((userData) => {
       if (!userData) {
         throw new Error("Can't get keys; User is not signed in");
       }
       if (userData.kA && userData.kB) {
         return userData;
       }
       if (!currentState.whenKeysReadyDeferred) {
         currentState.whenKeysReadyDeferred = Promise.defer();
@@ -747,16 +786,18 @@ FxAccountsInternal.prototype = {
               currentState.whenKeysReadyDeferred.reject(err);
             }
           );
         } else {
           currentState.whenKeysReadyDeferred.reject('No keyFetchToken');
         }
       }
       return currentState.whenKeysReadyDeferred.promise;
+    }, {
+      clearIfInvalid: ["keyFetchToken", "kA", "kB"],
     }).then(result => currentState.resolve(result));
    },
 
   fetchAndUnwrapKeys: function(keyFetchToken) {
     if (logPII) {
       log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
     }
     let currentState = this.currentAccountState;
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -496,16 +496,90 @@ 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);
+
+  let promiseLoginRefresh = new Promise(resolve => {
+    makeObserver(ONLOGIN_NOTIFICATION, function() {
+      log.debug("test_getKeys_invalid_token observed login refresh");
+      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 promiseLoginRefresh;
+
+  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");
@@ -560,16 +634,58 @@ 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);
+
+  let promiseLoginRefresh = new Promise(resolve => {
+    makeObserver(ONLOGIN_NOTIFICATION, function() {
+      log.debug("test_getAssertion_invalid_token observed login refresh");
+      resolve();
+    });
+  });
+
+  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);
+  }
+
+  yield promiseLoginRefresh;
+
+  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 = {
@@ -726,16 +842,57 @@ 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");
+
+  let promiseLoginRefresh = new Promise(resolve => {
+    makeObserver(ONLOGIN_NOTIFICATION, function() {
+      log.debug("test_getAssertion_invalid_token observed login refresh");
+      resolve();
+    });
+  });
+
+  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);
+  }
+
+  yield promiseLoginRefresh;
+  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(() => {