Permalink
Browse files

Bug 1227527 - Implement basic FxA device registration. r=markh

  • Loading branch information...
1 parent d0fd610 commit 8a5646364ea53c37c96566faba7bd9e8d3735c53 @philbooth philbooth committed Jan 13, 2016
View
@@ -1064,6 +1064,9 @@ pref("services.mobileid.server.uri", "https://msisdn.services.mozilla.com");
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
+// Disable Firefox Accounts device registration until bug 1238895 is fixed.
+pref("identity.fxaccounts.skipDeviceRegistration", true);
+
// Enable mapped array buffer.
#ifndef XP_WIN
pref("dom.mapped_arraybuffer.enabled", true);
@@ -24,6 +24,7 @@ const ORIGINAL_SENDCUSTOM = SystemAppProxy._sendCustomEvent;
do_register_cleanup(function() {
Services.prefs.setCharPref("identity.fxaccounts.auth.uri", ORIGINAL_AUTH_URI);
SystemAppProxy._sendCustomEvent = ORIGINAL_SENDCUSTOM;
+ Services.prefs.clearUserPref("identity.fxaccounts.skipDeviceRegistration");
});
// Make profile available so that fxaccounts can store user data
@@ -39,6 +40,9 @@ function run_test() {
}
add_task(function test_overall() {
+ // FxA device registration throws from this context
+ Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true);
+
do_check_neq(FxAccountsMgmtService, null);
});
@@ -105,6 +109,9 @@ add_test(function test_invalidEmailCase_signIn() {
// Point the FxAccountsClient's hawk rest request client to the mock server
Services.prefs.setCharPref("identity.fxaccounts.auth.uri", server.baseURI);
+ // FxA device registration throws from this context
+ Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true);
+
// Receive a mozFxAccountsChromeEvent message
function onMessage(subject, topic, data) {
let message = subject.wrappedJSObject;
@@ -164,6 +171,9 @@ add_test(function test_invalidEmailCase_signIn() {
add_test(function testHandleGetAssertionError_defaultCase() {
do_test_pending();
+ // FxA device registration throws from this context
+ Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true);
+
FxAccountsManager.getAssertion(null).then(
success => {
// getAssertion should throw with invalid audience
@@ -30,13 +30,17 @@ XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthGrantClient",
XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfile",
"resource://gre/modules/FxAccountsProfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+ "resource://services-sync/util.js");
+
// All properties exposed by the public FxAccounts API.
var publicProperties = [
"accountStatus",
"getAccountsClient",
"getAccountsSignInURI",
"getAccountsSignUpURI",
"getAssertion",
+ "getDeviceId",
"getKeys",
"getSignedInUser",
"getOAuthToken",
@@ -51,6 +55,7 @@ var publicProperties = [
"resendVerificationEmail",
"setSignedInUser",
"signOut",
+ "updateDeviceRegistration",
"whenVerified"
];
@@ -490,7 +495,9 @@ FxAccountsInternal.prototype = {
// We're telling the caller that this is durable now (although is that
// really something we should commit to? Why not let the write happen in
// the background? Already does for updateAccountData ;)
- return currentAccountState.promiseInitialized.then(() => {
+ return currentAccountState.promiseInitialized.then(() =>
+ this.updateDeviceRegistration()
+ ).then(() => {
Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1);
this.notifyObservers(ONLOGIN_NOTIFICATION);
if (!this.isUserEmailVerified(credentials)) {
@@ -539,6 +546,26 @@ FxAccountsInternal.prototype = {
}).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
+ // device id. Either way, we should register the device with FxA
+ // before returning the id to the caller.
+ return this._registerOrUpdateDevice(data);
+ }
+
+ // Return the device id that we already registered with the server.
+ return data.deviceId;
+ }
+
+ // Without a signed-in user, there can be no device id.
+ return null;
+ });
+ },
+
/**
* Resend the verification email fot the currently signed-in user.
*
@@ -605,10 +632,15 @@ FxAccountsInternal.prototype = {
let currentState = this.currentAccountState;
let sessionToken;
let tokensToRevoke;
+ let deviceId;
return currentState.getUserAccountData().then(data => {
- // Save the session token for use in the call to signOut below.
- sessionToken = data && data.sessionToken;
- tokensToRevoke = data && data.oauthTokens;
+ // Save the session token, tokens to revoke and the
+ // device id for use in the call to signOut below.
+ if (data) {
+ sessionToken = data.sessionToken;
+ tokensToRevoke = data.oauthTokens;
+ deviceId = data.deviceId;
+ }
return this._signOutLocal();
}).then(() => {
// FxAccountsManager calls here, then does its own call
@@ -620,7 +652,7 @@ FxAccountsInternal.prototype = {
// 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);
+ return this._signOutServer(sessionToken, deviceId);
}).catch(err => {
log.error("Error during remote sign out of Firefox Accounts", err);
}).then(() => {
@@ -652,11 +684,22 @@ FxAccountsInternal.prototype = {
});
},
- _signOutServer: function signOutServer(sessionToken) {
- // For now we assume the service being logged out from is Sync - we might
- // need to revisit this when this FxA code is used in a context that
- // isn't Sync.
- return this.fxAccountsClient.signOut(sessionToken, {service: "sync"});
+ _signOutServer(sessionToken, deviceId) {
+ // For now we assume the service being logged out from is Sync, so
+ // 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 }));
+ }
+
+ log.debug("destroying session");
+ return this.fxAccountsClient.signOut(sessionToken, options);
},
/**
@@ -1334,6 +1377,127 @@ FxAccountsInternal.prototype = {
}
).catch(err => Promise.reject(this._errorToErrorClass(err)));
},
+
+ // Attempt to update the auth server with whatever device details are stored
+ // in the account data. Returns a promise that always resolves, never rejects.
+ // If the promise resolves to a value, that value is the device id.
+ updateDeviceRegistration() {
+ return this.getSignedInUser().then(signedInUser => {
+ if (signedInUser) {
+ return this._registerOrUpdateDevice(signedInUser);
+ }
+ }).catch(error => this._logErrorAndSetStaleDeviceFlag(error));
+ },
+
+ _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.
+ // 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) {}
+
+ return Promise.resolve().then(() => {
+ const deviceName = this._getDeviceName();
+
+ if (signedInUser.deviceId) {
+ log.debug("updating existing device details");
+ return this.fxAccountsClient.updateDevice(
+ signedInUser.sessionToken, signedInUser.deviceId, deviceName);
+ }
+
+ log.debug("registering new device details");
+ return this.fxAccountsClient.registerDevice(
+ signedInUser.sessionToken, deviceName, this._getDeviceType());
+ }).then(device =>
+ this.currentAccountState.updateUserAccountData({
+ deviceId: device.id,
+ isDeviceStale: null
+ }).then(() => device.id)
+ ).catch(error => this._handleDeviceError(error, signedInUser.sessionToken));
+ },
+
+ _getDeviceName() {
+ return Utils.getDeviceName();
+ },
+
+ _getDeviceType() {
+ return Utils.getDeviceType();
+ },
+
+ _handleDeviceError(error, sessionToken) {
+ return Promise.resolve().then(() => {
+ if (error.code === 400) {
+ if (error.errno === ERRNO_UNKNOWN_DEVICE) {
+ return this._recoverFromUnknownDevice();
+ }
+
+ if (error.errno === ERRNO_DEVICE_SESSION_CONFLICT) {
+ return this._recoverFromDeviceSessionConflict(error, sessionToken);
+ }
+ }
+
+ return 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 })
+ .catch(error => this._logErrorAndSetStaleDeviceFlag(error));
+ },
+
+ _recoverFromDeviceSessionConflict(error, sessionToken) {
+ // FxA has already associated this session with a different device id.
+ // Perhaps we were beaten in a race to register. Handle the conflict:
+ // 1. Fetch the list of devices for the current user from FxA.
+ // 2. Look for ourselves in the list.
+ // 3. If we find a match, set the correct device id and the stale device
+ // flag on the account data and return the correct device id. At next
+ // sync or next sign-in, registration is retried and should succeed.
+ // 4. If we don't find a match, log the original error.
+ log.warn("device session conflict, attempting to ascertain the correct device id");
+ return this.fxAccountsClient.getDeviceList(sessionToken)
+ .then(devices => {
+ const matchingDevices = devices.filter(device => device.isCurrentDevice);
+ const length = matchingDevices.length;
+ if (length === 1) {
+ const deviceId = matchingDevices[0].id
+ return this.currentAccountState.updateUserAccountData({
+ deviceId,
+ isDeviceStale: true
+ }).then(() => deviceId);
+ }
+ if (length > 1) {
+ log.error("insane server state, " + length + " devices for this session");
+ }
+ return this._logErrorAndSetStaleDeviceFlag(error);
+ }).catch(secondError => {
+ log.error("failed to recover from device-session conflict", secondError);
+ this._logErrorAndSetStaleDeviceFlag(error)
+ });
+ },
+
+ _logErrorAndSetStaleDeviceFlag(error) {
+ // Device registration should never cause other operations to fail.
+ // If we've reached this point, just log the error and set the stale
+ // device flag on the account data. At next sync or next sign-in,
+ // registration will be retried.
+ 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(() => {});
+ }
};
Oops, something went wrong.

0 comments on commit 8a56463

Please sign in to comment.