Bug 1249619 - Handle FxA password reset/changed push notifications. r?markh
MozReview-Commit-ID: 4ZtUs80iPXp
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -16,17 +16,17 @@ var gFxAccounts = {
.wrappedJSObject;
},
get topics() {
// Do all this dance to lazy-load FxAccountsCommon.
delete this.topics;
return this.topics = [
"weave:service:ready",
- "weave:service:login:error",
+ "weave:service:login:change",
"weave:service:setup-complete",
"weave:ui:login:error",
"fxa-migration:state-changed",
this.FxAccountsCommon.ONLOGIN_NOTIFICATION,
this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
];
},
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -38,34 +38,36 @@ var publicProperties = [
"accountStatus",
"checkVerificationStatus",
"getAccountsClient",
"getAccountsSignInURI",
"getAccountsSignUpURI",
"getAssertion",
"getDeviceId",
"getKeys",
+ "getOAuthToken",
"getSignedInUser",
- "getOAuthToken",
"getSignedInUserProfile",
+ "handleDeviceDisconnection",
"invalidateCertificate",
"loadAndPoll",
"localtimeOffsetMsec",
"now",
+ "promiseAccountsChangeProfileURI",
"promiseAccountsForceSigninURI",
- "promiseAccountsChangeProfileURI",
"promiseAccountsManageURI",
"removeCachedOAuthToken",
"resendVerificationEmail",
+ "resetCredentials",
+ "sessionStatus",
"setSignedInUser",
"signOut",
+ "updateDeviceRegistration",
"updateUserAccountData",
- "updateDeviceRegistration",
- "handleDeviceDisconnection",
- "whenVerified"
+ "whenVerified",
];
// An AccountState object holds all state related to one specific account.
// Only one AccountState is ever "current" in the FxAccountsInternal object -
// whenever a user logs out or logs in, the current AccountState is discarded,
// making it impossible for the wrong state or state data to be accidentally
// used.
// In addition, it has some promise-related helpers to ensure that if an
@@ -789,16 +791,32 @@ FxAccountsInternal.prototype = {
return this.fxAccountsClient.signOutAndDestroyDevice(sessionToken, deviceId, options);
}
log.debug("destroying session");
return this.fxAccountsClient.signOut(sessionToken, options);
},
/**
+ * Check the status of the current session using cached credentials.
+ *
+ * @return Promise
+ * Resolves with a boolean indicating if the session is still valid
+ */
+ sessionStatus() {
+ return this.getSignedInUser().then(data => {
+ if (!data.sessionToken) {
+ return Promise.reject(new Error(
+ "sessionStatus called without a session token"));
+ }
+ return this.fxAccountsClient.sessionStatus(data.sessionToken);
+ });
+ },
+
+ /**
* Fetch encryption keys for the signed-in-user from the FxA API server.
*
* Not for user consumption. Exists to cause the keys to be fetch.
*
* Returns user data so that it can be chained with other methods.
*
* @return Promise
* The promise resolves to the credentials object of the signed-in user:
@@ -1501,16 +1519,38 @@ FxAccountsInternal.prototype = {
return this.signOut(true);
}
log.error(
"The device ID to disconnect doesn't match with the local device ID.\n"
+ "Local: " + localDeviceId + ", ID to disconnect: " + deviceId);
});
},
+ /**
+ * Delete all the cached persisted credentials we store for FxA.
+ *
+ * @return Promise resolves when the user data has been persisted
+ */
+ resetCredentials() {
+ // Delete all fields except those required for the user to
+ // reauthenticate.
+ 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);
+ },
+
// If you change what we send to the FxA servers during device registration,
// you'll have to bump the DEVICE_REGISTRATION_VERSION number to force older
// devices to re-register when Firefox updates
_registerOrUpdateDevice(signedInUser) {
try {
// Allow tests to skip device registration because:
// 1. It makes remote requests to the auth server.
// 2. _getDeviceName does not work from xpcshell.
@@ -1646,32 +1686,18 @@ FxAccountsInternal.prototype = {
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);
+ return this.resetCredentials();
}).then(() => Promise.reject(err));
},
};
// A getter for the instance to export
XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
let a = new FxAccounts();
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -179,16 +179,37 @@ this.FxAccountsClient.prototype = {
* }
*/
signIn: function signIn(email, password, getKeys=false) {
return this._createSession(SIGNIN, email, password, getKeys,
true /* retry */);
},
/**
+ * Check the status of a session given a session token
+ *
+ * @param sessionTokenHex
+ * The session token encoded in hex
+ * @return Promise
+ * Resolves with a boolean indicating if the session is still valid
+ */
+ sessionStatus: function (sessionTokenHex) {
+ return this._request("/session/status", "GET",
+ deriveHawkCredentials(sessionTokenHex, "sessionToken")).then(
+ () => Promise.resolve(true),
+ error => {
+ if (isInvalidTokenError(error)) {
+ return Promise.resolve(false);
+ }
+ throw error;
+ }
+ );
+ },
+
+ /**
* Destroy the current session with the Firefox Account API server
*
* @param sessionTokenHex
* The session token encoded in hex
* @return Promise
*/
signOut: function (sessionTokenHex, options = {}) {
let path = "/session/destroy";
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -85,20 +85,23 @@ exports.POLL_SESSION = 1000 * 60 *
// Observer notifications.
exports.ONLOGIN_NOTIFICATION = "fxaccounts:onlogin";
exports.ONVERIFIED_NOTIFICATION = "fxaccounts:onverified";
exports.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout";
// Internal to services/fxaccounts only
exports.ON_FXA_UPDATE_NOTIFICATION = "fxaccounts:update";
exports.ON_DEVICE_DISCONNECTED_NOTIFICATION = "fxaccounts:device_disconnected";
+exports.ON_PASSWORD_CHANGED_NOTIFICATION = "fxaccounts:password_changed";
+exports.ON_PASSWORD_RESET_NOTIFICATION = "fxaccounts:password_reset";
exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update";
exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange";
+exports.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION = "fxaccounts:statechange";
// UI Requests.
exports.UI_REQUEST_SIGN_IN_FLOW = "signInFlow";
exports.UI_REQUEST_REFRESH_AUTH = "refreshAuthentication";
// The OAuth client ID for Firefox Desktop
exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
--- a/services/fxaccounts/FxAccountsPush.js
+++ b/services/fxaccounts/FxAccountsPush.js
@@ -5,16 +5,17 @@
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/Task.jsm");
/**
* FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
*
* @param [options]
* Object, custom options that used for testing
* @constructor
*/
@@ -108,72 +109,100 @@ FxAccountsPushService.prototype = {
});
},
/**
* Standard observer interface to listen to push messages, changes and logout.
*
* @param subject
* @param topic
* @param data
+ * @returns {Promise}
*/
- observe(subject, topic, data) {
+ _observe(subject, topic, data) {
this.log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`);
switch (topic) {
case this.pushService.pushTopic:
if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
let message = subject.QueryInterface(Ci.nsIPushMessage);
- this._onPushMessage(message);
+ return this._onPushMessage(message);
}
break;
case this.pushService.subscriptionChangeTopic:
if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
- this._onPushSubscriptionChange();
+ return this._onPushSubscriptionChange();
}
break;
case ONLOGOUT_NOTIFICATION:
// user signed out, we need to stop polling the Push Server
- this.unsubscribe().catch(err => {
+ return this.unsubscribe().catch(err => {
this.log.error("Error during unsubscribe", err);
});
break;
default:
break;
}
},
/**
+ * Wrapper around _observe that catches errors
+ */
+ observe(subject, topic, data) {
+ Promise.resolve()
+ .then(() => this._observe(subject, topic, data))
+ .catch(err => this.log.error(err));
+ },
+ /**
* Fired when the Push server sends a notification.
*
* @private
+ * @returns {Promise}
*/
_onPushMessage(message) {
this.log.trace("FxAccountsPushService _onPushMessage");
if (!message.data) {
// Use the empty signal to check the verification state of the account right away
- this.fxAccounts.checkVerificationStatus();
- return;
+ return this.fxAccounts.checkVerificationStatus();
}
let payload = message.data.json();
switch (payload.command) {
case ON_DEVICE_DISCONNECTED_NOTIFICATION:
- this.fxAccounts.handleDeviceDisconnection(payload.data.id);
+ return this.fxAccounts.handleDeviceDisconnection(payload.data.id);
+ break;
+ case ON_PASSWORD_CHANGED_NOTIFICATION:
+ case ON_PASSWORD_RESET_NOTIFICATION:
+ return this._onPasswordChanged();
break;
default:
this.log.warn("FxA Push command unrecognized: " + payload.command);
}
},
/**
+ * Check the FxA session status after a password change/reset event.
+ * If the session is invalid, reset credentials and notify listeners of
+ * ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed
+ *
+ * @returns {Promise}
+ * @private
+ */
+ _onPasswordChanged: Task.async(function* () {
+ if (!(yield this.fxAccounts.sessionStatus())) {
+ yield this.fxAccounts.resetCredentials();
+ Services.obs.notifyObservers(null, ON_ACCOUNT_STATE_CHANGE_NOTIFICATION, null);
+ }
+ }),
+ /**
* Fired when the Push server drops a subscription, or the subscription identifier changes.
*
* https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages
*
+ * @returns {Promise}
* @private
*/
_onPushSubscriptionChange() {
this.log.trace("FxAccountsPushService _onPushSubscriptionChange");
- this.fxAccounts.updateDeviceRegistration();
+ return this.fxAccounts.updateDeviceRegistration();
},
/**
* Unsubscribe from the Push server
*
* Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
*
* @returns {Promise}
* @private
@@ -189,18 +218,18 @@ FxAccountsPushService.prototype = {
this.log.debug("FxAccountsPushService unsubscribed");
} else {
this.log.debug("FxAccountsPushService had no subscription to unsubscribe");
}
} else {
this.log.warn("FxAccountsPushService failed to unsubscribe", result);
}
return resolve(ok);
- })
- })
+ });
+ });
},
};
// Service registration below registers with FxAccountsComponents.manifest
const components = [FxAccountsPushService];
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
// The following registration below helps with testing this service.
--- a/services/fxaccounts/tests/xpcshell/test_push_service.js
+++ b/services/fxaccounts/tests/xpcshell/test_push_service.js
@@ -168,16 +168,62 @@ add_test(function observePushTopicDevice
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: customAccounts,
});
pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});
+add_test(function observePushTopicPasswordChanged() {
+ let msg = {
+ data: {
+ json: () => ({
+ command: ON_PASSWORD_CHANGED_NOTIFICATION
+ })
+ },
+ QueryInterface: function() {
+ return this;
+ }
+ };
+
+ let pushService = new FxAccountsPushService({
+ pushService: mockPushService,
+ });
+
+ pushService._onPasswordChanged = function () {
+ run_next_test();
+ }
+
+ pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
+});
+
+add_test(function observePushTopicPasswordReset() {
+ let msg = {
+ data: {
+ json: () => ({
+ command: ON_PASSWORD_RESET_NOTIFICATION
+ })
+ },
+ QueryInterface: function() {
+ return this;
+ }
+ };
+
+ let pushService = new FxAccountsPushService({
+ pushService: mockPushService
+ });
+
+ pushService._onPasswordChanged = function () {
+ run_next_test();
+ }
+
+ pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
+});
+
add_test(function observeSubscriptionChangeTopic() {
let customAccounts = Object.assign(mockFxAccounts, {
updateDeviceRegistration: function () {
// subscription change means updating the device registration
run_next_test();
}
});
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -40,16 +40,17 @@ XPCOMUtils.defineLazyGetter(this, 'log',
// FxAccountsCommon.js doesn't use a "namespace", so create one here.
var fxAccountsCommon = {};
Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
const OBSERVER_TOPICS = [
fxAccountsCommon.ONLOGIN_NOTIFICATION,
fxAccountsCommon.ONLOGOUT_NOTIFICATION,
+ fxAccountsCommon.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION,
];
const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
function deriveKeyBundle(kB) {
let out = CryptoUtils.hkdf(kB, undefined,
"identity.mozilla.com/picl/v1/oldsync", 2*32);
let bundle = new BulkKeyBundle();
@@ -302,16 +303,23 @@ this.BrowserIDManager.prototype = {
this.initializeWithCurrentIdentity(true);
break;
case fxAccountsCommon.ONLOGOUT_NOTIFICATION:
Weave.Service.startOver();
// 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
+ this.resetCredentials();
+ this._ensureValidToken().catch(err =>
+ this._log.error("Error while fetching a new token", err));
+ break;
}
},
/**
* Compute the sha256 of the message bytes. Return bytes.
*/
_sha256: function(message) {
let hasher = Cc["@mozilla.org/security/hash;1"]
@@ -668,22 +676,29 @@ this.BrowserIDManager.prototype = {
// 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: function() {
if (this.hasValidToken()) {
this._log.debug("_ensureValidToken already has one");
return Promise.resolve();
}
+ const notifyStateChanged =
+ () => Services.obs.notifyObservers(null, "weave:service:login:change", null);
// 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;
+ notifyStateChanged();
+ },
+ error => {
+ notifyStateChanged();
+ throw error
}
);
},
getResourceAuthenticator: function () {
return this._getAuthenticationHeader.bind(this);
},