Bug 1044530 - Remove invalid session and key fetch tokens from account storage. r?markh
--- 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(() => {