Bug 1047133 - Persist FxA OAuth token between browser sessions. r=MattN
authorJared Wein <jwein@mozilla.com>
Wed, 08 Oct 2014 20:49:47 -0400
changeset 209668 b2afd3b9c1b4c722769e1741bdd1d7b9f45e1a60
parent 209667 700ef0cb9a6ca789d660dca71429fb011206a4a2
child 209669 047060a5b1dc0a266e39ba3a9694e5e1417a60e8
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersMattN
bugs1047133
milestone35.0a1
Bug 1047133 - Persist FxA OAuth token between browser sessions. r=MattN
browser/app/profile/firefox.js
browser/components/loop/MozLoopService.jsm
browser/components/loop/test/mochitest/browser_fxa_login.js
browser/components/loop/test/mochitest/browser_toolbarbutton.js
browser/components/loop/test/mochitest/head.js
browser/components/loop/test/xpcshell/test_loopservice_busy.js
browser/components/loop/test/xpcshell/test_loopservice_initialize.js
browser/components/loop/test/xpcshell/test_loopservice_registration.js
browser/components/loop/test/xpcshell/test_loopservice_restart.js
browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
browser/components/loop/test/xpcshell/xpcshell.ini
browser/components/nsBrowserGlue.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1619,16 +1619,18 @@ pref("loop.debug.sdk", false);
 #ifdef DEBUG
 pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*");
 #else
 pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net");
 #endif
 pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
 pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
 pref("loop.rooms.enabled", false);
+pref("loop.fxa_oauth.tokendata", "");
+pref("loop.fxa_oauth.profile", "");
 
 // serverURL to be assigned by services team
 pref("services.push.serverURL", "wss://push.services.mozilla.com/");
 
 pref("social.sidebar.unload_timeout_ms", 10000);
 
 pref("dom.identity.enabled", false);
 
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -72,29 +72,37 @@ XPCOMUtils.defineLazyGetter(this, "log",
   let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
   let consoleOptions = {
     maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
     prefix: "Loop",
   };
   return new ConsoleAPI(consoleOptions);
 });
 
+function setJSONPref(aName, aValue) {
+  let value = !!aValue ? JSON.stringify(aValue) : "";
+  Services.prefs.setCharPref(aName, value);
+}
+
+function getJSONPref(aName) {
+  let value = Services.prefs.getCharPref(aName);
+  return !!value ? JSON.parse(value) : null;
+}
+
 // The current deferred for the registration process. This is set if in progress
 // or the registration was successful. This is null if a registration attempt was
 // unsuccessful.
 let gRegisteredDeferred = null;
 let gPushHandler = null;
 let gHawkClient = null;
-let gLocalizedStrings =  null;
+let gLocalizedStrings = null;
 let gInitializeTimer = null;
 let gFxAEnabled = true;
 let gFxAOAuthClientPromise = null;
 let gFxAOAuthClient = null;
-let gFxAOAuthTokenData = null;
-let gFxAOAuthProfile = null;
 let gErrors = new Map();
 
  /**
  * Attempts to open a websocket.
  *
  * A new websocket interface is used each time. If an onStop callback
  * was received, calling asyncOpen() on the same interface will
  * trigger a "alreay open socket" exception even though the channel
@@ -302,16 +310,48 @@ let MozLoopServiceInternal = {
   /**
    * Returns true if the expiry time is in the future.
    */
   urlExpiryTimeIsInFuture: function() {
     return this.expiryTimeSeconds * 1000 > Date.now();
   },
 
   /**
+   * Retrieves MozLoopService Firefox Accounts OAuth token.
+   *
+   * @return {Object} OAuth token
+   */
+  get fxAOAuthTokenData() {
+    return getJSONPref("loop.fxa_oauth.tokendata");
+  },
+
+  /**
+   * Sets MozLoopService Firefox Accounts OAuth token.
+   * If the tokenData is being cleared, will also clear the
+   * profile since the profile is dependent on the token data.
+   *
+   * @param {Object} aTokenData OAuth token
+   */
+  set fxAOAuthTokenData(aTokenData) {
+    setJSONPref("loop.fxa_oauth.tokendata", aTokenData);
+    if (!aTokenData) {
+      this.fxAOAuthProfile = null;
+    }
+  },
+
+  /**
+   * Sets MozLoopService Firefox Accounts Profile data.
+   *
+   * @param {Object} aProfileData Profile data
+   */
+  set fxAOAuthProfile(aProfileData) {
+    setJSONPref("loop.fxa_oauth.profile", aProfileData);
+  },
+
+  /**
    * Retrieves MozLoopService "do not disturb" pref value.
    *
    * @return {Boolean} aFlag
    */
   get doNotDisturb() {
     return Services.prefs.getBoolPref("loop.do_not_disturb");
   },
 
@@ -415,19 +455,18 @@ let MozLoopServiceInternal = {
     }
 
     gRegisteredDeferred = Promise.defer();
     // We grab the promise early in case .initialize or its results sets
     // it back to null on error.
     let result = gRegisteredDeferred.promise;
 
     gPushHandler = mockPushHandler || MozLoopPushHandler;
-
     gPushHandler.initialize(this.onPushRegistered.bind(this),
-      this.onHandleNotification.bind(this));
+                            this.onHandleNotification.bind(this));
 
     return result;
   },
 
   /**
    * Performs a hawk based request to the loop server.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
@@ -570,22 +609,25 @@ let MozLoopServiceInternal = {
       return;
     }
 
     this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => {
       // storeSessionToken could have rejected and nulled the promise if the token was malformed.
       if (!gRegisteredDeferred) {
         return;
       }
-      gRegisteredDeferred.resolve();
+      gRegisteredDeferred.resolve("registered to guest status");
       // No need to clear the promise here, everything was good, so we don't need
       // to re-register.
-    }, (error) => {
+    }, error => {
       log.error("Failed to register with Loop server: ", error);
-      gRegisteredDeferred.reject(error.errno);
+      // registerWithLoopServer may have already made this null.
+      if (gRegisteredDeferred) {
+        gRegisteredDeferred.reject(error);
+      }
       gRegisteredDeferred = null;
     });
   },
 
   /**
    * Registers with the Loop server either as a guest or a FxA user.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
@@ -611,16 +653,18 @@ let MozLoopServiceInternal = {
           // Authorization failed, invalid token, we need to try again with a new token.
           if (retry) {
             return this.registerWithLoopServer(sessionType, pushUrl, false);
           }
         }
 
         log.error("Failed to register with the loop server. Error: ", error);
         this.setError("registration", error);
+        gRegisteredDeferred.reject(error);
+        gRegisteredDeferred = null;
         throw error;
       }
     );
   },
 
   /**
    * Unregisters from the Loop server either as a guest or a FxA user.
    *
@@ -1064,66 +1108,101 @@ let MozLoopServiceInternal = {
       deferred.resolve(result);
     } else {
       deferred.reject("Invalid token data");
     }
   },
 };
 Object.freeze(MozLoopServiceInternal);
 
-let gInitializeTimerFunc = () => {
-  // Kick off the push notification service into registering after a timeout
-  // this ensures we're not doing too much straight after the browser's finished
+let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSocket) => {
+  // Kick off the push notification service into registering after a timeout.
+  // This ensures we're not doing too much straight after the browser's finished
   // starting up.
   gInitializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-  gInitializeTimer.initWithCallback(() => {
-    MozLoopService.register();
+  gInitializeTimer.initWithCallback(Task.async(function* initializationCallback() {
+    yield MozLoopService.register(mockPushHandler, mockWebSocket).then(Task.async(function*() {
+      if (!MozLoopServiceInternal.fxAOAuthTokenData) {
+        log.debug("MozLoopService: Initialized without an already logged-in account");
+        deferredInitialization.resolve("initialized to guest status");
+        return;
+      }
+
+      log.debug("MozLoopService: Initializing with already logged-in account");
+      let registeredPromise =
+            MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA,
+                                                          gPushHandler.pushUrl);
+      registeredPromise.then(() => {
+        deferredInitialization.resolve("initialized to logged-in status");
+      }, error => {
+        log.debug("MozLoopService: error logging in using cached auth token");
+        MozLoopServiceInternal.setError("login", error);
+        deferredInitialization.reject("error logging in using cached auth token");
+      });
+    }), error => {
+      log.debug("MozLoopService: Failure of initial registration", error);
+      deferredInitialization.reject(error);
+    });
     gInitializeTimer = null;
-  },
+  }),
   MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
 };
 
 /**
  * Public API
  */
 this.MozLoopService = {
   _DNSService: gDNSService,
 
   set initializeTimerFunc(value) {
     gInitializeTimerFunc = value;
   },
 
   /**
    * Initialized the loop service, and starts registration with the
    * push and loop servers.
+   *
+   * @return {Promise}
    */
-  initialize: function() {
-
+  initialize: Task.async(function*(mockPushHandler, mockWebSocket) {
     // Do this here, rather than immediately after definition, so that we can
     // stub out API functions for unit testing
     Object.freeze(this);
 
     // Don't do anything if loop is not enabled.
     if (!Services.prefs.getBoolPref("loop.enabled") ||
         Services.prefs.getBoolPref("loop.throttled")) {
-      return;
+      return Promise.reject("loop is not enabled");
     }
 
     if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
       gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
       if (!gFxAEnabled) {
-        this.logOutFromFxA();
+        yield this.logOutFromFxA();
       }
     }
 
-    // If expiresTime is in the future then kick-off registration.
-    if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
-      gInitializeTimerFunc();
+    // If expiresTime is not in the future and the user hasn't
+    // previously authenticated then skip registration.
+    if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
+        !MozLoopServiceInternal.fxAOAuthTokenData) {
+      return Promise.resolve("registration not needed");
     }
-  },
+
+    let deferredInitialization = Promise.defer();
+    gInitializeTimerFunc(deferredInitialization, mockPushHandler, mockWebSocket);
+
+    return deferredInitialization.promise.catch(error => {
+      if (typeof(error) == "object") {
+        // This never gets cleared since there is no UI to recover. Only restarting will work.
+        MozLoopServiceInternal.setError("initialization", error);
+      }
+      throw error;
+    });
+  }),
 
   /**
    * If we're operating the service in "soft start" mode, and this browser
    * isn't already activated, check whether it's time for it to become active.
    * If so, activate the loop service.
    *
    * @param {Object} buttonNode DOM node representing the Loop button -- if we
    *                            change from inactive to active, we need this
@@ -1243,17 +1322,17 @@ this.MozLoopService = {
   },
 
   /**
    * Used to note a call url expiry time. If the time is later than the current
    * latest expiry time, then the stored expiry time is increased. For times
    * sooner, this function is a no-op; this ensures we always have the latest
    * expiry time for a url.
    *
-   * This is used to deterimine whether or not we should be registering with the
+   * This is used to determine whether or not we should be registering with the
    * push server on start.
    *
    * @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
    *                                    of the url.
    */
   noteCallUrlExpiry: function(expiryTimeSeconds) {
     MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds;
   },
@@ -1300,18 +1379,26 @@ this.MozLoopService = {
   set doNotDisturb(aFlag) {
     MozLoopServiceInternal.doNotDisturb = aFlag;
   },
 
   get fxAEnabled() {
     return gFxAEnabled;
   },
 
+  /**
+   * Gets the user profile, but only if there is
+   * tokenData present. Without tokenData, the
+   * profile is meaningless.
+   *
+   * @return {Object}
+   */
   get userProfile() {
-    return gFxAOAuthProfile;
+    return getJSONPref("loop.fxa_oauth.tokendata") &&
+           getJSONPref("loop.fxa_oauth.profile");
   },
 
   get errors() {
     return MozLoopServiceInternal.errors;
   },
 
   get log() {
     return log;
@@ -1430,55 +1517,55 @@ this.MozLoopService = {
   /**
    * Start the FxA login flow using the OAuth client and params from the Loop server.
    *
    * The caller should be prepared to handle rejections related to network, server or login errors.
    *
    * @return {Promise} that resolves when the FxA login flow is complete.
    */
   logInToFxA: function() {
-    log.debug("logInToFxA with gFxAOAuthTokenData:", !!gFxAOAuthTokenData);
-    if (gFxAOAuthTokenData) {
-      return Promise.resolve(gFxAOAuthTokenData);
+    log.debug("logInToFxA with fxAOAuthTokenData:", !!MozLoopServiceInternal.fxAOAuthTokenData);
+    if (MozLoopServiceInternal.fxAOAuthTokenData) {
+      return Promise.resolve(MozLoopServiceInternal.fxAOAuthTokenData);
     }
 
     return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
     }).then(tokenData => {
-      gFxAOAuthTokenData = tokenData;
+      MozLoopServiceInternal.fxAOAuthTokenData = tokenData;
       return tokenData;
     }).then(tokenData => {
       return gRegisteredDeferred.promise.then(Task.async(function*() {
         if (gPushHandler.pushUrl) {
           yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl);
         } else {
           throw new Error("No pushUrl for FxA registration");
         }
         MozLoopServiceInternal.clearError("login");
         MozLoopServiceInternal.clearError("profile");
-        return gFxAOAuthTokenData;
+        return MozLoopServiceInternal.fxAOAuthTokenData;
       }));
     }).then(tokenData => {
       let client = new FxAccountsProfileClient({
         serverURL: gFxAOAuthClient.parameters.profile_uri,
         token: tokenData.access_token
       });
       client.fetchProfile().then(result => {
-        gFxAOAuthProfile = result;
+        MozLoopServiceInternal.fxAOAuthProfile = result;
         MozLoopServiceInternal.notifyStatusChanged("login");
       }, error => {
         log.error("Failed to retrieve profile", error);
         this.setError("profile", error);
-        gFxAOAuthProfile = null;
+        MozLoopServiceInternal.fxAOAuthProfile = null;
         MozLoopServiceInternal.notifyStatusChanged();
       });
       return tokenData;
     }).catch(error => {
-      gFxAOAuthTokenData = null;
-      gFxAOAuthProfile = null;
+      MozLoopServiceInternal.fxAOAuthTokenData = null;
+      MozLoopServiceInternal.fxAOAuthProfile = null;
       throw error;
     }).catch((error) => {
       MozLoopServiceInternal.setError("login", error);
       // Re-throw for testing
       throw error;
     });
   },
 
@@ -1493,18 +1580,18 @@ this.MozLoopService = {
     log.debug("logOutFromFxA");
     if (gPushHandler && gPushHandler.pushUrl) {
       yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA,
                                                             gPushHandler.pushUrl);
     } else {
       MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
     }
 
-    gFxAOAuthTokenData = null;
-    gFxAOAuthProfile = null;
+    MozLoopServiceInternal.fxAOAuthTokenData = null;
+    MozLoopServiceInternal.fxAOAuthProfile = null;
 
     // Reset the client since the initial promiseFxAOAuthParameters() call is
     // what creates a new session.
     gFxAOAuthClient = null;
     gFxAOAuthClientPromise = null;
 
     // clearError calls notifyStatusChanged so should be done last when the
     // state is clean.
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -2,21 +2,16 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Test FxA logins with Loop.
  */
 
 "use strict";
 
-const {
-  gFxAOAuthTokenData,
-  gFxAOAuthProfile,
-} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
-
 const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?";
 
 function* checkFxA401() {
   let err = MozLoopService.errors.get("login");
   ise(err.code, 401, "Check error code");
   ise(err.friendlyMessage, getLoopString("could_not_authenticate"),
       "Check friendlyMessage");
   ise(err.friendlyDetails, getLoopString("password_changed_question"),
@@ -206,16 +201,18 @@ add_task(function* registrationWithInval
   Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
 
   let tokenPromise = MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state");
   yield tokenPromise.then(body => {
     ok(false, "Promise should have rejected");
   },
   error => {
     is(error.code, 400, "Check error code");
+    checkFxAOAuthTokenData(null);
+    is(MozLoopService.userProfile, null, "Profile should be empty after invalid login");
   });
 });
 
 add_task(function* registrationWith401() {
   yield resetFxA();
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
@@ -227,16 +224,18 @@ add_task(function* registrationWith401()
   yield promiseOAuthParamsSetup(BASE_URL, params);
 
   let tokenPromise = MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state");
   yield tokenPromise.then(body => {
     ok(false, "Promise should have rejected");
   },
   error => {
     is(error.code, 401, "Check error code");
+    checkFxAOAuthTokenData(null);
+    is(MozLoopService.userProfile, null, "Profile should be empty after invalid login");
   });
 
   yield checkFxA401();
 });
 
 add_task(function* basicAuthorizationAndRegistration() {
   yield resetFxA();
   let params = {
@@ -316,17 +315,17 @@ add_task(function* loginWithParams401() 
   yield MozLoopService.register(mockPushHandler);
 
   let loginPromise = MozLoopService.logInToFxA();
   yield loginPromise.then(tokenData => {
     ok(false, "Promise should have rejected");
   },
   error => {
     ise(error.code, 401, "Check error code");
-    ise(gFxAOAuthTokenData, null, "Check there is no saved token data");
+    checkFxAOAuthTokenData(null);
   });
 
   yield checkFxA401();
 });
 
 add_task(function* logoutWithIncorrectPushURL() {
   yield resetFxA();
   let pushURL = "http://www.example.com/";
@@ -382,13 +381,13 @@ add_task(function* loginWithRegistration
   yield promiseOAuthParamsSetup(BASE_URL, params);
 
   let loginPromise = MozLoopService.logInToFxA();
   yield loginPromise.then(tokenData => {
     ok(false, "Promise should have rejected");
   },
   error => {
     ise(error.code, 401, "Check error code");
-    ise(gFxAOAuthTokenData, null, "Check there is no saved token data");
+    checkFxAOAuthTokenData(null);
   });
 
   yield checkFxA401();
 });
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -4,74 +4,76 @@
 /**
  * Test the toolbar button states.
  */
 
 "use strict";
 
 registerCleanupFunction(function*() {
   MozLoopService.doNotDisturb = false;
-  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.fxAOAuthProfile = null;
   yield MozLoopServiceInternal.clearError("testing");
 });
 
 add_task(function* test_doNotDisturb() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopService.doNotDisturb = true;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
   yield MozLoopService.doNotDisturb = false;
   Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is not in disabled state");
 });
 
 add_task(function* test_doNotDisturb_with_login() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopService.doNotDisturb = true;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
-  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
+  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
   yield MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   yield loadLoopPanel();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state after opening panel");
   let loopPanel = document.getElementById("loop-notification-panel");
   loopPanel.hidePopup();
   yield MozLoopService.doNotDisturb = false;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
-  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
   yield MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
 add_task(function* test_error() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopServiceInternal.setError("testing", {});
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
   yield MozLoopServiceInternal.clearError("testing");
   Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is not in error state");
 });
 
 add_task(function* test_error_with_login() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopServiceInternal.setError("testing", {});
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
-  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
   MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
   yield MozLoopServiceInternal.clearError("testing");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
-  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.fxAOAuthProfile = null;
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
 add_task(function* test_active() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
-  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
+  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
   yield MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   yield loadLoopPanel();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state after opening panel");
   let loopPanel = document.getElementById("loop-notification-panel");
   loopPanel.hidePopup();
-  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -115,37 +115,36 @@ function promiseOAuthParamsSetup(baseURL
   return deferred.promise;
 }
 
 function* resetFxA() {
   let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
   global.gHawkClient = null;
   global.gFxAOAuthClientPromise = null;
   global.gFxAOAuthClient = null;
-  global.gFxAOAuthTokenData = null;
-  global.gFxAOAuthProfile = null;
+  MozLoopServiceInternal.fxAOAuthProfile = null;
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   Services.prefs.clearUserPref(fxASessionPref);
   MozLoopService.errors.clear();
   let notified = promiseObserverNotified("loop-status-changed");
   MozLoopServiceInternal.notifyStatusChanged();
   yield notified;
 }
 
-function setInternalLoopGlobal(aName, aValue) {
-  let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
-  global[aName] = aValue;
+function checkFxAOAuthTokenData(aValue) {
+  ise(MozLoopServiceInternal.fxAOAuthTokenData, aValue, "fxAOAuthTokenData should be " + aValue);
 }
 
 function checkLoggedOutState() {
   let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
   ise(global.gFxAOAuthClientPromise, null, "gFxAOAuthClientPromise should be cleared");
-  ise(global.gFxAOAuthProfile, null, "gFxAOAuthProfile should be cleared");
+  ise(MozLoopService.userProfile, null, "fxAOAuthProfile should be cleared");
   ise(global.gFxAOAuthClient, null, "gFxAOAuthClient should be cleared");
-  ise(global.gFxAOAuthTokenData, null, "gFxAOAuthTokenData should be cleared");
+  checkFxAOAuthTokenData(null);
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   ise(Services.prefs.getPrefType(fxASessionPref), Services.prefs.PREF_INVALID,
       "FxA hawk session should be cleared anyways");
 }
 
 function promiseDeletedOAuthParams(baseURL) {
   let deferred = Promise.defer();
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
--- a/browser/components/loop/test/xpcshell/test_loopservice_busy.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_busy.js
@@ -47,19 +47,20 @@ function test_send_busy_on_call() {
 
 add_test(test_send_busy_on_call); //FXA call accepted, Guest call rejected
 add_test(test_send_busy_on_call); //No FXA call, first Guest call accepted, second rejected
 
 function run_test()
 {
   setupFakeLoopServer();
 
-  // Setup fake login (profile) state so we get FxA requests.
-  const serviceGlobal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
-  serviceGlobal.gFxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
+  // Setup fake login state so we get FxA requests.
+  const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).MozLoopServiceInternal;
+  MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
+  MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
 
   // For each notification received from the PushServer, MozLoopService will first query
   // for any pending calls on the FxA hawk session and then again using the guest session.
   // A pair of response objects in the callsResponses array will be consumed for each
   // notification. The even calls object is for the FxA session, the odd the Guest session.
 
   let callsRespCount = 0;
   let callsResponses = [
@@ -97,16 +98,16 @@ function run_test()
     response.finish();
   });
 
   do_register_cleanup(function() {
     // Revert original Chat.open implementation
     Chat.open = openChatOrig;
 
     // Revert fake login state
-    serviceGlobal.gFxAOAuthProfile = null;
+    MozLoopServiceInternal.fxAOAuthTokenData = null;
 
     // clear test pref
     Services.prefs.clearUserPref("loop.seenToS");
   });
 
   run_next_test();
 }
--- a/browser/components/loop/test/xpcshell/test_loopservice_initialize.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_initialize.js
@@ -5,18 +5,19 @@ var startTimerCalled = false;
 
 /**
  * Tests that registration doesn't happen when the expiry time is
  * not set.
  */
 add_task(function test_initialize_no_expiry() {
   startTimerCalled = false;
 
-  MozLoopService.initialize();
-
+  let initializedPromise = yield MozLoopService.initialize();
+  Assert.equal(initializedPromise, "registration not needed",
+               "Promise should be fulfilled");
   Assert.equal(startTimerCalled, false,
     "should not register when no expiry time is set");
 });
 
 /**
  * Tests that registration doesn't happen when the expiry time is
  * in the past.
  */
--- a/browser/components/loop/test/xpcshell/test_loopservice_registration.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_registration.js
@@ -35,17 +35,17 @@ add_test(function test_register_offline(
 add_test(function test_register_websocket_success_loop_server_fail() {
   mockPushHandler.registrationResult = null;
 
   MozLoopService.register(mockPushHandler).then(() => {
     do_throw("should not succeed when loop server registration fails");
   }, err => {
     // 404 is an expected failure indicated by the lack of route being set
     // up on the Loop server mock. This is added in the next test.
-    Assert.equal(err, 404, "Expected no errors in websocket registration");
+    Assert.equal(err.errno, 404, "Expected no errors in websocket registration");
 
     run_next_test();
   });
 });
 
 /**
  * Tests that we get a success response when both websocket and Loop server
  * registration are complete.
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_loopservice_restart.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const FAKE_FXA_TOKEN_DATA = JSON.stringify({
+  "token_type": "bearer",
+  "access_token": "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
+  "scope": "profile"
+});
+const FAKE_FXA_PROFILE = JSON.stringify({
+  "email": "test@example.com",
+  "uid": "999999994d9f4b08a2cbfc0999999999",
+  "avatar": null
+});
+const LOOP_FXA_TOKEN_PREF = "loop.fxa_oauth.tokendata";
+const LOOP_FXA_PROFILE_PREF = "loop.fxa_oauth.profile";
+const LOOP_URL_EXPIRY_PREF = "loop.urlsExpiryTimeSeconds";
+const LOOP_INITIAL_DELAY_PREF = "loop.initialDelay";
+
+/**
+ * This file is to test restart+reauth.
+ */
+
+add_task(function test_initialize_with_expired_urls_and_no_auth_token() {
+  // Set time to be 2 seconds in the past.
+  var nowSeconds = Date.now() / 1000;
+  Services.prefs.setIntPref(LOOP_URL_EXPIRY_PREF, nowSeconds - 2);
+  Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
+
+  yield MozLoopService.initialize(mockPushHandler).then((msg) => {
+    Assert.equal(msg, "registration not needed", "Initialize should not register when the " +
+                                                 "URLs are expired and there are no auth tokens");
+  }, (error) => {
+    Assert.ok(false, error, "should have resolved the promise that initialize returned");
+  });
+});
+
+add_task(function test_initialize_with_urls_and_no_auth_token() {
+  Services.prefs.setIntPref(LOOP_URL_EXPIRY_PREF, Date.now() / 1000 + 10);
+  Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
+
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+  });
+
+  yield MozLoopService.initialize(mockPushHandler).then((msg) => {
+    Assert.equal(msg, "initialized to guest status", "Initialize should register as a " +
+                                                     "guest when no auth tokens but expired URLs");
+  }, (error) => {
+    Assert.ok(false, error, "should have resolved the promise that initialize returned");
+  });
+});
+
+add_task(function test_initialize_with_invalid_fxa_token() {
+  Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
+  Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
+
+  // Only need to implement the FxA registration because the previous
+  // test registered as a guest.
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 401, "Unauthorized");
+    response.write(JSON.stringify({
+      code: 401,
+      errno: 110,
+      error: "Unauthorized",
+      message: "Unknown credentials",
+    }));
+  });
+
+  yield MozLoopService.initialize(mockPushHandler).then(() => {
+    Assert.ok(false, "Initializing with an invalid token should reject the promise");
+  },
+  (error) => {
+    let pushHandler = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gPushHandler;
+    Assert.equal(pushHandler.pushUrl, kEndPointUrl, "Push URL should match");
+    Assert.equal(Services.prefs.getCharPref(LOOP_FXA_TOKEN_PREF), "",
+                 "FXA pref should be cleared if token was invalid");
+    Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), "",
+                 "FXA profile pref should be cleared if token was invalid");
+  });
+});
+
+add_task(function test_initialize_with_fxa_token() {
+  Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
+  Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+  });
+
+  yield MozLoopService.initialize(mockPushHandler).then(() => {
+    Assert.equal(Services.prefs.getCharPref(LOOP_FXA_TOKEN_PREF), FAKE_FXA_TOKEN_DATA,
+                 "FXA pref should still be set after initialization");
+    Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), FAKE_FXA_PROFILE,
+                 "FXA profile should still be set after initialization");
+  });
+});
+
+function run_test() {
+  setupFakeLoopServer();
+  // Note, this is just used to speed up the test.
+  Services.prefs.setIntPref(LOOP_INITIAL_DELAY_PREF, 0);
+  mockPushHandler.pushUrl = kEndPointUrl;
+
+  do_register_cleanup(function() {
+    Services.prefs.clearUserPref(LOOP_INITIAL_DELAY_PREF);
+    Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
+    Services.prefs.clearUserPref(LOOP_FXA_PROFILE_PREF);
+    Services.prefs.clearUserPref(LOOP_URL_EXPIRY_PREF);
+  });
+
+  run_next_test();
+};
--- a/browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
@@ -13,21 +13,18 @@ add_test(function test_registration_inva
     // Due to the way the time stamp checking code works in hawkclient, we expect a couple
     // of authorization requests before we reset the token.
     if (request.hasHeader("Authorization")) {
       authorizationAttempts++;
       response.setStatusLine(null, 401, "Unauthorized");
       response.write(JSON.stringify({
         code: 401,
         errno: 110,
-        error: {
-          error: "Unauthorized",
-          message: "Unknown credentials",
-          statusCode: 401
-        }
+        error: "Unauthorized",
+        message: "Unknown credentials",
       }));
     } else {
       // We didn't have an authorization header, so check the pref has been cleared.
       Assert.equal(Services.prefs.prefHasUserValue(LOOP_HAWK_PREF), false);
       response.setStatusLine(null, 200, "OK");
       response.setHeader("Hawk-Session-Token", fakeSessionToken2, false);
     }
     response.processAsync();
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -10,13 +10,14 @@ skip-if = toolkit == 'gonk'
 [test_loopservice_dnd.js]
 [test_loopservice_expiry.js]
 [test_loopservice_hawk_errors.js]
 [test_loopservice_loop_prefs.js]
 [test_loopservice_initialize.js]
 [test_loopservice_locales.js]
 [test_loopservice_notification.js]
 [test_loopservice_registration.js]
+[test_loopservice_restart.js]
 [test_loopservice_token_invalid.js]
 [test_loopservice_token_save.js]
 [test_loopservice_token_send.js]
 [test_loopservice_token_validation.js]
 [test_loopservice_busy.js]
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -738,17 +738,20 @@ BrowserGlue.prototype = {
       SignInToWebsiteUX.uninit();
     }
 #endif
     webrtcUI.uninit();
     FormValidationHandler.uninit();
 
     // XXX: Temporary hack to allow Loop FxA login after a restart to work.
     // Remove this once bug 1071247 is deployed.
-    Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
+    if (Services.prefs.getPrefType("loop.autologin-after-restart") != Ci.nsIPrefBranch.PREF_BOOL ||
+        !Services.prefs.getBoolPref("loop.autologin-after-restart")) {
+      Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
+    }
   },
 
   // All initial windows have opened.
   _onWindowsRestored: function BG__onWindowsRestored() {
     // Show update notification, if needed.
     if (Services.prefs.prefHasUserValue("app.update.postupdate"))
       this._showUpdateNotification();