Bug 1574048 - Remove FxAccounts internal/external/Object.freeze()/ weirdness. r=lina,rfkelly
authorMark Hammond <mhammond@skippinet.com.au>
Thu, 12 Sep 2019 02:08:50 +0000
changeset 492746 404c6f03dfc702836e6b50721f6409c7a179ed79
parent 492745 d85fd47dbcc7b9b52ecacca05ce304cef7368399
child 492747 663df481fcbff090b06c1d8f0736396567e77609
push id36565
push userncsoregi@mozilla.com
push dateThu, 12 Sep 2019 09:41:22 +0000
treeherdermozilla-central@663df481fcbf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslina, rfkelly
bugs1574048
milestone71.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1574048 - Remove FxAccounts internal/external/Object.freeze()/ weirdness. r=lina,rfkelly Differential Revision: https://phabricator.services.mozilla.com/D44083
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsCommands.js
services/fxaccounts/FxAccountsDevice.jsm
services/fxaccounts/FxAccountsKeys.jsm
services/fxaccounts/FxAccountsProfile.jsm
services/fxaccounts/FxAccountsProfileClient.jsm
services/fxaccounts/FxAccountsPush.jsm
services/fxaccounts/FxAccountsWebChannel.jsm
services/fxaccounts/moz.build
services/fxaccounts/tests/mochitest/test_invalidEmailCase.html
services/fxaccounts/tests/xpcshell/test_accounts.js
services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
services/fxaccounts/tests/xpcshell/test_commands.js
services/fxaccounts/tests/xpcshell/test_device.js
services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
services/fxaccounts/tests/xpcshell/test_pairing.js
services/fxaccounts/tests/xpcshell/test_profile.js
services/fxaccounts/tests/xpcshell/test_profile_client.js
services/fxaccounts/tests/xpcshell/test_push_service.js
services/fxaccounts/tests/xpcshell/test_web_channel.js
services/sync/modules-testing/utils.js
services/sync/modules/browserid_identity.js
services/sync/tests/unit/test_browserid_identity.js
services/sync/tests/unit/test_syncscheduler.js
services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -22,17 +22,16 @@ const { clearTimeout, setTimeout } = Chr
 const { FxAccountsStorageManager } = ChromeUtils.import(
   "resource://gre/modules/FxAccountsStorage.jsm"
 );
 const {
   ASSERTION_LIFETIME,
   ASSERTION_USE_PERIOD,
   CERT_LIFETIME,
   COMMAND_SENDTAB,
-  DERIVED_KEYS_NAMES,
   ERRNO_DEVICE_SESSION_CONFLICT,
   ERRNO_INVALID_AUTH_TOKEN,
   ERRNO_UNKNOWN_DEVICE,
   ERROR_AUTH_ERROR,
   ERROR_INVALID_PARAMETER,
   ERROR_NO_ACCOUNT,
   ERROR_OFFLINE,
   ERROR_TO_GENERAL_ERROR_CLASS,
@@ -48,17 +47,16 @@ const {
   ONLOGOUT_NOTIFICATION,
   ONVERIFIED_NOTIFICATION,
   ON_DEVICE_DISCONNECTED_NOTIFICATION,
   ON_NEW_DEVICE_ID,
   POLL_SESSION,
   PREF_ACCOUNT_ROOT,
   PREF_LAST_FXA_USER,
   SERVER_ERRNO_TO_ERROR,
-  SCOPE_OLD_SYNC,
   log,
   logPII,
 } = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
 
 ChromeUtils.defineModuleGetter(
   this,
   "FxAccountsClient",
   "resource://gre/modules/FxAccountsClient.jsm"
@@ -91,72 +89,39 @@ ChromeUtils.defineModuleGetter(
 ChromeUtils.defineModuleGetter(
   this,
   "FxAccountsDevice",
   "resource://gre/modules/FxAccountsDevice.jsm"
 );
 
 ChromeUtils.defineModuleGetter(
   this,
+  "FxAccountsKeys",
+  "resource://gre/modules/FxAccountsKeys.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
   "FxAccountsProfile",
   "resource://gre/modules/FxAccountsProfile.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Preferences: "resource://gre/modules/Preferences.jsm",
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(
   this,
   "FXA_ENABLED",
   "identity.fxaccounts.enabled",
   true
 );
 
-// All properties exposed by the public FxAccounts API.
-var publicProperties = [
-  "_withCurrentAccountState", // fxaccounts package only!
-  "accountStatus",
-  "canGetKeys",
-  "checkVerificationStatus",
-  "commands",
-  "device",
-  "getAccountsClient",
-  "getAssertion",
-  "getDeviceList",
-  "getKeys",
-  "authorizeOAuthCode",
-  "getOAuthToken",
-  "getProfileCache",
-  "getPushSubscription",
-  "getScopedKeys",
-  "getSignedInUser",
-  "getSignedInUserProfile",
-  "handleAccountDestroyed",
-  "handleDeviceDisconnection",
-  "handleEmailUpdated",
-  "hasLocalSession",
-  "invalidateCertificate",
-  "loadAndPoll",
-  "localtimeOffsetMsec",
-  "notifyDevices",
-  "now",
-  "removeCachedOAuthToken",
-  "resendVerificationEmail",
-  "resetCredentials",
-  "sessionStatus",
-  "setProfileCache",
-  "setSignedInUser",
-  "signOut",
-  "updateDeviceRegistration",
-  "updateUserAccountData",
-  "whenVerified",
-];
-
 // An AccountState object holds all state related to one specific account.
+// It is considered "private" to the FxAccounts modules.
 // 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
 // attempt is made to resolve a promise on a "stale" state (eg, if an
 // operation starts, but a different user logs in before the operation
 // completes), the promise will be rejected.
@@ -263,18 +228,18 @@ AccountState.prototype = {
 
   reject(error) {
     // It could be argued that we should just let it reject with the original
     // error - but this runs the risk of the error being (eg) a 401, which
     // might cause the consumer to attempt some remediation and cause other
     // problems.
     if (!this.isCurrent) {
       log.info(
-        "An accountState promise was rejected, but we are ignoring that" +
-          "reason and rejecting it due to a different user being signed in." +
+        "An accountState promise was rejected, but we are ignoring that " +
+          "reason and rejecting it due to a different user being signed in. " +
           "Originally rejected with",
         error
       );
       return Promise.reject(new Error("A different user signed in"));
     }
     return Promise.reject(error);
   },
 
@@ -395,109 +360,464 @@ function copyObjectProperties(from, to, 
   }
 }
 
 function urlsafeBase64Encode(key) {
   return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
 }
 
 /**
- * The public API's constructor.
+ * The public API.
+ *
+ * TODO - *all* non-underscore stuff here should have sphinx docstrings so
+ * that docs magically appear on https://firefox-source-docs.mozilla.org/
+ * (although |./mach doc| is broken on windows (bug 1232403) and on Linux for
+ * markh (some obscure npm issue he gave up on) - so later...)
  */
-var FxAccounts = function(mockInternal) {
-  let external = {};
-  let internal;
+class FxAccounts {
+  constructor(mocks = null) {
+    this._internal = new FxAccountsInternal();
+    if (mocks) {
+      // it's slightly unfortunate that we need to mock the main "internal" object
+      // before calling initialize, primarily so a mock `newAccountState` is in
+      // place before initialize calls it, but we need to initialize the
+      // "sub-object" mocks after. This can probably be fixed, but whatever...
+      copyObjectProperties(
+        mocks,
+        this._internal,
+        this._internal,
+        Object.keys(mocks)
+      );
+    }
+    this._internal.initialize();
+    // allow mocking our "sub-objects" too.
+    if (mocks) {
+      for (let subobject of ["currentAccountState", "keys", "fxaPushService"]) {
+        if (typeof mocks[subobject] == "object") {
+          copyObjectProperties(
+            mocks[subobject],
+            this._internal[subobject],
+            this._internal[subobject],
+            Object.keys(mocks[subobject])
+          );
+        }
+      }
+    }
+  }
 
-  if (!mockInternal) {
-    internal = new FxAccountsInternal();
-    copyObjectProperties(
-      FxAccountsInternal.prototype,
-      external,
-      internal,
-      publicProperties
-    );
-  } else {
-    internal = Object.create(
-      FxAccountsInternal.prototype,
-      Object.getOwnPropertyDescriptors(mockInternal)
-    );
-    copyObjectProperties(internal, external, internal, publicProperties);
-    // Exposes the internal object for testing only.
-    external.internal = internal;
+  get commands() {
+    return this._internal.commands;
+  }
+
+  static get config() {
+    return FxAccountsConfig;
+  }
+
+  get device() {
+    return this._internal.device;
   }
 
-  if (!internal.fxaPushService) {
-    // internal.fxaPushService option is used in testing.
-    // Otherwise we load the service lazily.
-    XPCOMUtils.defineLazyGetter(internal, "fxaPushService", function() {
-      return Cc["@mozilla.org/fxaccounts/push;1"].getService(
-        Ci.nsISupports
-      ).wrappedJSObject;
+  get keys() {
+    return this._internal.keys;
+  }
+
+  _withCurrentAccountState(func) {
+    return this._internal.withCurrentAccountState(func);
+  }
+
+  _withVerifiedAccountState(func) {
+    return this._internal.withVerifiedAccountState(func);
+  }
+
+  getDeviceList() {
+    return this._internal.getDeviceList();
+  }
+
+  /**
+   * Retrieves an OAuth authorization code
+   *
+   * @param {Object} options
+   * @param options.client_id
+   * @param options.state
+   * @param options.scope
+   * @param options.access_type
+   * @param options.code_challenge_method
+   * @param options.code_challenge
+   * @param [options.keys_jwe]
+   * @returns {Promise<Object>} Object containing "code" and "state" properties.
+   */
+  authorizeOAuthCode(options) {
+    return this._withVerifiedAccountState(async state => {
+      const client = this._internal.oauthClient;
+      const oAuthURL = client.serverURL.href;
+      const params = { ...options };
+      if (params.keys_jwk) {
+        const jwk = JSON.parse(
+          new TextDecoder().decode(
+            ChromeUtils.base64URLDecode(params.keys_jwk, { padding: "reject" })
+          )
+        );
+        params.keys_jwe = await this._internal.createKeysJWE(
+          params.client_id,
+          params.scope,
+          jwk
+        );
+        delete params.keys_jwk;
+      }
+      try {
+        const assertion = await this._internal.getAssertion(oAuthURL);
+        return await client.authorizeCodeFromAssertion(assertion, params);
+      } catch (err) {
+        throw this._internal._errorToErrorClass(err);
+      }
     });
   }
 
-  if (!internal.observerPreloads) {
-    // A registry of promise-returning functions that `notifyObservers` should
-    // call before sending notifications. Primarily used so parts of Firefox
-    // which have yet to load for performance reasons can be force-loaded, and
-    // thus not miss notifications.
-    internal.observerPreloads = [
-      // Sync
-      () => {
-        let scope = {};
-        ChromeUtils.import("resource://services-sync/main.js", scope);
-        return scope.Weave.Service.promiseInitialized;
-      },
-    ];
+  /**
+   * Get an OAuth token for the user
+   *
+   * @param options
+   *        {
+   *          scope: (string/array) the oauth scope(s) being requested. As a
+   *                 convenience, you may pass a string if only one scope is
+   *                 required, or an array of strings if multiple are needed.
+   *        }
+   *
+   * @return Promise.<string | Error>
+   *        The promise resolves the oauth token as a string or rejects with
+   *        an error object ({error: ERROR, details: {}}) of the following:
+   *          INVALID_PARAMETER
+   *          NO_ACCOUNT
+   *          UNVERIFIED_ACCOUNT
+   *          NETWORK_ERROR
+   *          AUTH_ERROR
+   *          UNKNOWN_ERROR
+   */
+  getOAuthToken(options = {}) {
+    return this._internal.getOAuthToken(options);
+  }
+
+  /**
+   * Remove an OAuth token from the token cache.  Callers should call this
+   * after they determine a token is invalid, so a new token will be fetched
+   * on the next call to getOAuthToken().
+   *
+   * @param options
+   *        {
+   *          token: (string) A previously fetched token.
+   *        }
+   * @return Promise.<undefined> This function will always resolve, even if
+   *         an unknown token is passed.
+   */
+  removeCachedOAuthToken(options) {
+    return this._internal.removeCachedOAuthToken(options);
+  }
+
+  /**
+   * Get the user currently signed in to Firefox Accounts.
+   *
+   * @return Promise
+   *        The promise resolves to the credentials object of the signed-in user:
+   *        {
+   *          email: The user's email address
+   *          uid: The user's unique id
+   *          sessionToken: Session for the FxA server
+   *          kSync: An encryption key for Sync
+   *          kXCS: A key hash of kB for the X-Client-State header
+   *          kExtSync: An encryption key for WebExtensions syncing
+   *          kExtKbHash: A key hash of kB for WebExtensions syncing
+   *          verified: email verification status
+   *          authAt: The time (seconds since epoch) that this record was
+   *                  authenticated
+   *        }
+   *        or null if no user is signed in.
+   */
+  // XXX - for the public API we should consolidate this with
+  // getSignedInUserProfile - bug 1574052.
+  getSignedInUser() {
+    return this._withCurrentAccountState(async currentState => {
+      const data = await currentState.getUserAccountData();
+      if (!data) {
+        return null;
+      }
+      if (!FXA_ENABLED) {
+        await this.signOut();
+        return null;
+      }
+      if (this._internal.isUserEmailVerified(data)) {
+        // This is a work-around for preferences being reset (bug 1550967).
+        // Many things check this preference as a flag for "is sync configured",
+        // and if not, we try and avoid loading these modules at all. So if a user
+        // is signed in but this pref isn't set, things go weird.
+        // However, some thing do unconditionally load fxaccounts, such as
+        // about:prefs. When that happens we can detect the state and re-add the
+        // pref. Note that we only do this for verified users as that's what sync
+        // does (ie, if the user is unverified, sync will set it on verification)
+        if (
+          !Services.prefs.prefHasUserValue("services.sync.username") &&
+          data.email
+        ) {
+          Services.prefs.setStringPref("services.sync.username", data.email);
+        }
+      } else {
+        // If the email is not verified, start polling for verification,
+        // but return null right away.  We don't want to return a promise
+        // that might not be fulfilled for a long time.
+        this._internal.startVerifiedCheck(data);
+      }
+      return data;
+    });
+  }
+
+  // XXX - consolidate with getSignedInUser - bug 1574052.
+  /**
+   * Get the user's account and profile data if it is locally cached. If
+   * not cached it will return null, but cause the profile data to be fetched
+   * in the background, after which a ON_PROFILE_CHANGE_NOTIFICATION
+   * observer notification will be sent, at which time this can be called
+   * again to obtain the most recent profile info.
+   *
+   * @return Promise.<object | Error>
+   *        The promise resolves to an accountData object with extra profile
+   *        information such as profileImageUrl, or rejects with
+   *        an error object ({error: ERROR, details: {}}) of the following:
+   *          INVALID_PARAMETER
+   *          NO_ACCOUNT
+   *          UNVERIFIED_ACCOUNT
+   *          NETWORK_ERROR
+   *          AUTH_ERROR
+   *          UNKNOWN_ERROR
+   */
+  getSignedInUserProfile() {
+    return this._withCurrentAccountState(async currentState => {
+      try {
+        let profileData = await this._internal.profile.getProfile();
+        let profile = Cu.cloneInto(profileData, {});
+        return profile;
+      } catch (error) {
+        log.error("Could not retrieve profile data", error);
+        throw this._internal._errorToErrorClass(error);
+      }
+    });
   }
 
-  // wait until after the mocks are setup before initializing.
-  internal.initialize();
+  /**
+   * Checks if the current account still exists.
+   */
+  // This should be killed as part of bug 1574051 - we are doing something wrong
+  // if our public API says a user is logged in, but to an account which doesn't
+  // exist!
+  accountStatus() {
+    return this._withCurrentAccountState(async state => {
+      let data = await state.getUserAccountData();
+      if (!data) {
+        return false;
+      }
+      return this._internal.fxAccountsClient.accountStatus(data.uid);
+    });
+  }
 
-  return Object.freeze(external);
-};
-this.FxAccounts.config = FxAccountsConfig;
+  /**
+   * Checks if we have a valid local session state for the current account.
+   *
+   * @return Promise
+   *        Resolves with a boolean, with true indicating that we appear to
+   *        have a valid local session, or false if we need to reauthenticate
+   *        with the content server to obtain one.
+   *        Note that this only checks local state, although typically that's
+   *        OK, because we drop the local session information whenever we detect
+   *        we are in this state. However, see sessionStatus() for a way to
+   *        check the session token with the server, which can be considered the
+   *        canonical way to determine if we have a valid local session.
+   *
+   * XXX - this will be refactored in bug 1574051.
+   */
+  async hasLocalSession() {
+    let data = await this.getSignedInUser();
+    return data && data.sessionToken;
+  }
+
+  /**
+   *
+   * @return Promise
+   *        Resolves with a boolean indicating if the session is still valid.
+   *
+   * Because this hits the server, you should only call this method when you have
+   * reason to believe the session very recently became invalid (eg, because
+   * you saw an auth related exception from a remote service.)
+   *
+   * XXX - this will be refactored in bug 1574051.
+   */
+  sessionStatus() {
+    return this._withCurrentAccountState(async currentState => {
+      return this._internal.sessionStatus(currentState);
+    });
+  }
 
-/**
- * The internal API's constructor.
- */
-function FxAccountsInternal() {
-  // Make a local copy of this constant so we can mock it in testing
-  this.POLL_SESSION = POLL_SESSION;
+  /**
+   * Send a message to a set of devices in the same account
+   *
+   * @param deviceIds: (null/string/array) The device IDs to send the message to.
+   *                   If null, will be sent to all devices.
+   *
+   * @param excludedIds: (null/string/array) If deviceIds is null, this may
+   *                     list device IDs which should not receive the message.
+   *
+   * @param payload: (object) The payload, which will be JSON.stringified.
+   *
+   * @param TTL: How long the message should be retained before it is discarded.
+   */
+  // XXX - used only by sync to tell other devices that the clients collection
+  // has changed so they should sync asap. The API here is somewhat vague (ie,
+  // "an object"), but to be useful across devices, the payload really needs
+  // formalizing. We should try and do something better here.
+  notifyDevices(deviceIds, excludedIds, payload, TTL) {
+    return this._internal.notifyDevices(deviceIds, excludedIds, payload, TTL);
+  }
 
-  // All significant initialization should be done in the initialize() method
-  // below as it helps with testing.
+  /**
+   * Resend the verification email for the currently signed-in user.
+   *
+   */
+  resendVerificationEmail() {
+    return this._withCurrentAccountState(currentState => {
+      return currentState.getUserAccountData().then(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) {
+          if (!data.sessionToken) {
+            return Promise.reject(
+              new Error(
+                "resendVerificationEmail called without a session token"
+              )
+            );
+          }
+          this._internal.startPollEmailStatus(
+            currentState,
+            data.sessionToken,
+            "start"
+          );
+          return this._internal.fxAccountsClient
+            .resendVerificationEmail(data.sessionToken)
+            .catch(err => this._internal._handleTokenError(err));
+        }
+        throw new Error("Cannot resend verification email; no signed-in user");
+      });
+    });
+  }
+
+  async signOut(localOnly) {
+    // Note that we do not use _withCurrentAccountState here, otherwise we
+    // end up with an exception due to the user signing out before the call is
+    // complete - but that's the entire point of this method :)
+    return this._internal.signOut(localOnly);
+  }
+
+  // XXX - we should consider killing this - the only reason it is public is
+  // so that sync can change it when it notices the device name being changed,
+  // and that could probably be replaced with a pref observer.
+  updateDeviceRegistration() {
+    return this._withCurrentAccountState(_ => {
+      return this._internal.updateDeviceRegistration();
+    });
+  }
+
+  // we should try and kill this too.
+  whenVerified(data) {
+    return this._withCurrentAccountState(_ => {
+      return this._internal.whenVerified(data);
+    });
+  }
 }
 
+var FxAccountsInternal = function() {};
+
 /**
  * The internal API's prototype.
  */
 FxAccountsInternal.prototype = {
+  // Make a local copy of this constant so we can mock it in testing
+  POLL_SESSION,
+
   // The timeout (in ms) we use to poll for a verified mail for the first
   // VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD minutes if the user has
   // logged-in in this session.
   VERIFICATION_POLL_TIMEOUT_INITIAL: 60000, // 1 minute.
   // All the other cases (> 5 min, on restart etc).
   VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 5 * 60000, // 5 minutes.
   // After X minutes, the polling will slow down to _SUBSEQUENT if we have
   // logged-in in this session.
   VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD: 5,
   // The current version of the device registration, we use this to re-register
   // devices after we update what we send on device registration.
   DEVICE_REGISTRATION_VERSION: 2,
 
   _fxAccountsClient: null,
 
-  // All significant initialization should be done in this initialize() method,
-  // as it's called after this object has been mocked for tests.
+  // All significant initialization should be done in this initialize() method
+  // to help with our mocking story.
   initialize() {
+    XPCOMUtils.defineLazyGetter(this, "fxaPushService", function() {
+      return Cc["@mozilla.org/fxaccounts/push;1"].getService(
+        Ci.nsISupports
+      ).wrappedJSObject;
+    });
+
+    this.keys = new FxAccountsKeys(this);
+
+    if (!this.observerPreloads) {
+      // A registry of promise-returning functions that `notifyObservers` should
+      // call before sending notifications. Primarily used so parts of Firefox
+      // which have yet to load for performance reasons can be force-loaded, and
+      // thus not miss notifications.
+      this.observerPreloads = [
+        // Sync
+        () => {
+          let scope = {};
+          ChromeUtils.import("resource://services-sync/main.js", scope);
+          return scope.Weave.Service.promiseInitialized;
+        },
+      ];
+    }
+
     this.currentTimer = null;
+    // This object holds details about, and storage for, the current user. It
+    // is replaced when a different user signs in. Instead of using it directly,
+    // you should try and use `withCurrentAccountState`.
     this.currentAccountState = this.newAccountState();
   },
 
+  async withCurrentAccountState(func) {
+    const state = this.currentAccountState;
+    let result;
+    try {
+      result = await func(state);
+    } catch (ex) {
+      return state.reject(ex);
+    }
+    return state.resolve(result);
+  },
+
+  async withVerifiedAccountState(func) {
+    return this.withCurrentAccountState(async state => {
+      let data = await state.getUserAccountData();
+      if (!data) {
+        // No signed-in user
+        throw this._error(ERROR_NO_ACCOUNT);
+      }
+
+      if (!this.isUserEmailVerified(data)) {
+        // Signed-in user has not verified email
+        throw this._error(ERROR_UNVERIFIED_ACCOUNT);
+      }
+      return func(state);
+    });
+  },
+
   get fxAccountsClient() {
     if (!this._fxAccountsClient) {
       this._fxAccountsClient = new FxAccountsClient();
     }
     return this._fxAccountsClient;
   },
 
   // The profile object used to fetch the actual user profile.
@@ -547,37 +867,16 @@ FxAccountsInternal.prototype = {
 
   // A hook-point for tests who may want a mocked AccountState or mocked storage.
   newAccountState(credentials) {
     let storage = new FxAccountsStorageManager();
     storage.initialize(credentials);
     return new AccountState(storage);
   },
 
-  // "Friend" classes of FxAccounts (e.g. FxAccountsCommands) know about the
-  // "current account state" system. This method allows them to read and write
-  // safely in it.
-  // Example of usage:
-  // fxAccounts._withCurrentAccountState(async (getUserData, updateUserData) => {
-  //   const userData = await getUserData(['device']);
-  //   ...
-  //   await updateUserData({device: null});
-  // });
-  _withCurrentAccountState(func) {
-    const state = this.currentAccountState;
-    const getUserData = fields => state.getUserAccountData(fields);
-    const updateUserData = data => state.updateUserAccountData(data);
-    return func(getUserData, updateUserData);
-  },
-
-  /**
-   * Send a message to a set of devices in the same account
-   *
-   * @return Promise
-   */
   notifyDevices(deviceIds, excludedIds, payload, TTL) {
     if (typeof deviceIds == "string") {
       deviceIds = [deviceIds];
     }
     return this.currentAccountState.getUserAccountData().then(data => {
       if (!data) {
         throw this._error(ERROR_NO_ACCOUNT);
       }
@@ -600,123 +899,63 @@ FxAccountsInternal.prototype = {
   /**
    * Return the current time in milliseconds as an integer.  Allows tests to
    * manipulate the date to simulate certificate expiration.
    */
   now() {
     return this.fxAccountsClient.now();
   },
 
-  getAccountsClient() {
-    return this.fxAccountsClient;
-  },
-
   /**
    * Return clock offset in milliseconds, as reported by the fxAccountsClient.
    * This can be overridden for testing.
    *
    * The offset is the number of milliseconds that must be added to the client
    * clock to make it equal to the server clock.  For example, if the client is
    * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
    */
   get localtimeOffsetMsec() {
     return this.fxAccountsClient.localtimeOffsetMsec;
   },
 
+  async sessionStatus(currentState) {
+    let data = await currentState.getUserAccountData();
+    if (!data.sessionToken) {
+      throw new Error("sessionStatus called without a session token");
+    }
+    return this.fxAccountsClient.sessionStatus(data.sessionToken);
+  },
+
   /**
    * Ask the server whether the user's email has been verified
    */
   checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) {
     if (!sessionToken) {
       return Promise.reject(
         new Error("checkEmailStatus called without a session token")
       );
     }
     return this.fxAccountsClient
       .recoveryEmailStatus(sessionToken, options)
       .catch(error => this._handleTokenError(error));
   },
 
-  /**
-   * Once the user's email is verified, we can request the keys
-   */
-  fetchKeys: function fetchKeys(keyFetchToken) {
-    log.debug("fetchKeys: " + !!keyFetchToken);
-    if (logPII) {
-      log.debug("fetchKeys - the token is " + keyFetchToken);
-    }
-    return this.fxAccountsClient.accountKeys(keyFetchToken);
-  },
-
   // set() makes sure that polling is happening, if necessary.
   // get() does not wait for verification, and returns an object even if
   // unverified. The caller of get() must check .verified .
   // The "fxaccounts:onverified" event will fire only when the verified
   // state goes from false to true, so callers must register their observer
   // and then call get(). In particular, it will not fire when the account
   // was found to be verified in a previous boot: if our stored state says
   // the account is verified, the event will never fire. So callers must do:
   //   register notification observer (go)
   //   userdata = get()
   //   if (userdata.verified()) {go()}
 
   /**
-   * Get the user currently signed in to Firefox Accounts.
-   *
-   * @return Promise
-   *        The promise resolves to the credentials object of the signed-in user:
-   *        {
-   *          email: The user's email address
-   *          uid: The user's unique id
-   *          sessionToken: Session for the FxA server
-   *          kSync: An encryption key for Sync
-   *          kXCS: A key hash of kB for the X-Client-State header
-   *          kExtSync: An encryption key for WebExtensions syncing
-   *          kExtKbHash: A key hash of kB for WebExtensions syncing
-   *          verified: email verification status
-   *          authAt: The time (seconds since epoch) that this record was
-   *                  authenticated
-   *        }
-   *        or null if no user is signed in.
-   */
-  async getSignedInUser() {
-    let currentState = this.currentAccountState;
-    const data = await currentState.getUserAccountData();
-    if (!data) {
-      return currentState.resolve(null);
-    }
-    if (!FXA_ENABLED) {
-      await this.signOut();
-      return currentState.resolve(null);
-    }
-    if (this.isUserEmailVerified(data)) {
-      // This is a work-around for preferences being reset (bug 1550967).
-      // Many things check this preference as a flag for "is sync configured",
-      // and if not, we try and avoid loading these modules at all. So if a user
-      // is signed in but this pref isn't set, things go weird.
-      // However, some thing do unconditionally load fxaccounts, such as
-      // about:prefs. When that happens we can detect the state and re-add the
-      // pref. Note that we only do this for verified users as that's what sync
-      // does (ie, if the user is unverified, sync will set it on verification)
-      if (
-        !Services.prefs.prefHasUserValue("services.sync.username") &&
-        data.email
-      ) {
-        Services.prefs.setStringPref("services.sync.username", data.email);
-      }
-    } else {
-      // If the email is not verified, start polling for verification,
-      // but return null right away.  We don't want to return a promise
-      // that might not be fulfilled for a long time.
-      this.startVerifiedCheck(data);
-    }
-    return currentState.resolve(data);
-  },
-
-  /**
    * Set the current user signed in to Firefox Accounts.
    *
    * @param credentials
    *        The credentials object obtained by logging in or creating
    *        an account on the FxA server:
    *        {
    *          authAt: The time (seconds since epoch) that this record was
    *                  authenticated
@@ -733,17 +972,17 @@ FxAccountsInternal.prototype = {
    *         successfully and is rejected on error.
    */
   async setSignedInUser(credentials) {
     if (!FXA_ENABLED) {
       throw new Error("Cannot call setSignedInUser when FxA is disabled.");
     }
     Preferences.resetBranch(PREF_ACCOUNT_ROOT);
     log.debug("setSignedInUser - aborting any existing flows");
-    const signedInUser = await this.getSignedInUser();
+    const signedInUser = await this.currentAccountState.getUserAccountData();
     if (signedInUser) {
       await this._signOutServer(
         signedInUser.sessionToken,
         signedInUser.oauthTokens
       );
     }
     await this.abortExistingFlow();
     let currentAccountState = (this.currentAccountState = this.newAccountState(
@@ -842,24 +1081,16 @@ FxAccountsInternal.prototype = {
             );
           }
         );
       })
       .catch(err => this._handleTokenError(err))
       .then(result => currentState.resolve(result));
   },
 
-  /**
-   * Invalidate the FxA certificate, so that it will be refreshed from the server
-   * the next time it is needed.
-   */
-  invalidateCertificate() {
-    return this.currentAccountState.updateUserAccountData({ cert: null });
-  },
-
   async checkDeviceUpdateNeeded(device) {
     // There is no device registered or the device registration is outdated.
     // Either way, we should register the device with FxA
     // before returning the id to the caller.
     const availableCommandsKeys = Object.keys(
       await this.availableCommands()
     ).sort();
     return (
@@ -869,54 +1100,31 @@ FxAccountsInternal.prototype = {
       !device.registeredCommandsKeys ||
       !CommonUtils.arrayEqual(
         device.registeredCommandsKeys,
         availableCommandsKeys
       )
     );
   },
 
-  async getDeviceList() {
-    const accountData = await this._getVerifiedAccountOrReject();
-    const devices = await this.fxAccountsClient.getDeviceList(
-      accountData.sessionToken
-    );
+  getDeviceList() {
+    return this.withVerifiedAccountState(async state => {
+      let accountData = await state.getUserAccountData();
 
-    // Check if our push registration is still good.
-    const ourDevice = devices.find(device => device.isCurrentDevice);
-    if (ourDevice.pushEndpointExpired) {
-      await this.fxaPushService.unsubscribe();
-      await this._registerOrUpdateDevice(accountData);
-    }
-
-    return devices;
-  },
+      const devices = await this.fxAccountsClient.getDeviceList(
+        accountData.sessionToken
+      );
 
-  /**
-   * Resend the verification email fot the currently signed-in user.
-   *
-   */
-  resendVerificationEmail: function resendVerificationEmail() {
-    let currentState = this.currentAccountState;
-    return this.getSignedInUser().then(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) {
-        if (!data.sessionToken) {
-          return Promise.reject(
-            new Error("resendVerificationEmail called without a session token")
-          );
-        }
-        this.startPollEmailStatus(currentState, data.sessionToken, "start");
-        return this.fxAccountsClient
-          .resendVerificationEmail(data.sessionToken)
-          .catch(err => this._handleTokenError(err));
+      // Check if our push registration is still good.
+      const ourDevice = devices.find(device => device.isCurrentDevice);
+      if (ourDevice.pushEndpointExpired) {
+        await this.fxaPushService.unsubscribe();
+        await this._registerOrUpdateDevice(accountData);
       }
-      throw new Error("Cannot resend verification email; no signed-in user");
+      return devices;
     });
   },
 
   /*
    * Reset state such that any previous flow is canceled.
    */
   abortExistingFlow() {
     if (this.currentTimer) {
@@ -940,31 +1148,30 @@ FxAccountsInternal.prototype = {
     return this.currentAccountState.getUserAccountData().then(data => {
       if (!data) {
         return false;
       }
       return this.fxAccountsClient.accountStatus(data.uid);
     });
   },
 
-  checkVerificationStatus() {
+  async checkVerificationStatus() {
     log.trace("checkVerificationStatus");
-    let currentState = this.currentAccountState;
-    return currentState.getUserAccountData().then(data => {
-      if (!data) {
-        log.trace("checkVerificationStatus - no user data");
-        return null;
-      }
+    let state = this.currentAccountState;
+    let data = await state.getUserAccountData();
+    if (!data) {
+      log.trace("checkVerificationStatus - no user data");
+      return null;
+    }
 
-      // Always check the verification status, even if the local state indicates
-      // we're already verified. If the user changed their password, the check
-      // will fail, and we'll enter the reauth state.
-      log.trace("checkVerificationStatus - forcing verification status check");
-      return this.startPollEmailStatus(currentState, data.sessionToken, "push");
-    });
+    // Always check the verification status, even if the local state indicates
+    // we're already verified. If the user changed their password, the check
+    // will fail, and we'll enter the reauth state.
+    log.trace("checkVerificationStatus - forcing verification status check");
+    return this.startPollEmailStatus(state, data.sessionToken, "push");
   },
 
   _destroyOAuthToken(tokenData) {
     let client = new FxAccountsOAuthGrantClient({
       serverURL: tokenData.server,
       client_id: FX_OAUTH_CLIENT_ID,
     });
     return client.destroyToken(tokenData.token);
@@ -1037,274 +1244,16 @@ FxAccountsInternal.prototype = {
     log.debug("Destroying all OAuth tokens.");
     try {
       await this._destroyAllOAuthTokens(tokensToRevoke);
     } catch (err) {
       log.error("Error during destruction of oauth tokens during signout", err);
     }
   },
 
-  /**
-   * 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);
-    });
-  },
-
-  /**
-   * Checks if we have a valid local session state for the current account.
-   *
-   * @return Promise
-   *        Resolves with a boolean, with true indicating that we appear to
-   *        have a valid local session, or false if we need to reauthenticate
-   *        with the content server to obtain one.
-   *        Note that this doesn't check with the server - it really just tells
-   *        us if we are even able to perform that server check. To fully check
-   *        the account status, you should first call this method, and if this
-   *        returns true, you should then call sessionStatus() to check with
-   *        the server.
-   */
-  async hasLocalSession() {
-    let data = await this.getSignedInUser();
-    return data && data.sessionToken;
-  },
-
-  /**
-   * Checks if we currently have encryption keys or if we have enough to
-   * be able to successfully fetch them for the signed-in-user.
-   */
-  async canGetKeys() {
-    let currentState = this.currentAccountState;
-    let userData = await currentState.getUserAccountData();
-    if (!userData) {
-      throw new Error("Can't possibly get keys; User is not signed in");
-    }
-    // - keyFetchToken means we can almost certainly grab them.
-    // - kSync, kXCS, kExtSync and kExtKbHash means we already have them.
-    // - kB is deprecated but |getKeys| will help us migrate to kSync and friends.
-    return (
-      userData &&
-      (userData.keyFetchToken ||
-        DERIVED_KEYS_NAMES.every(k => userData[k]) ||
-        userData.kB)
-    );
-  },
-
-  /**
-   * 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:
-   *        {
-   *          email: The user's email address
-   *          uid: The user's unique id
-   *          sessionToken: Session for the FxA server
-   *          kSync: An encryption key for Sync
-   *          kXCS: A key hash of kB for the X-Client-State header
-   *          kExtSync: An encryption key for WebExtensions syncing
-   *          kExtKbHash: A key hash of kB for WebExtensions syncing
-   *          verified: email verification status
-   *        }
-   *        or null if no user is signed in
-   */
-  async getKeys() {
-    let currentState = this.currentAccountState;
-    try {
-      let userData = await currentState.getUserAccountData();
-      if (!userData) {
-        throw new Error("Can't get keys; User is not signed in");
-      }
-      if (userData.kB) {
-        // Bug 1426306 - Migrate from kB to derived keys.
-        log.info("Migrating kB to derived keys.");
-        const { uid, kB } = userData;
-        await this.updateUserAccountData({
-          uid,
-          ...(await this._deriveKeys(uid, CommonUtils.hexToBytes(kB))),
-          kA: null, // Remove kA and kB from storage.
-          kB: null,
-        });
-        userData = await this.getUserAccountData();
-      }
-      if (DERIVED_KEYS_NAMES.every(k => userData[k])) {
-        return currentState.resolve(userData);
-      }
-      if (!currentState.whenKeysReadyDeferred) {
-        currentState.whenKeysReadyDeferred = PromiseUtils.defer();
-        if (userData.keyFetchToken) {
-          this.fetchAndUnwrapKeys(userData.keyFetchToken).then(
-            dataWithKeys => {
-              if (DERIVED_KEYS_NAMES.some(k => !dataWithKeys[k])) {
-                const missing = DERIVED_KEYS_NAMES.filter(
-                  k => !dataWithKeys[k]
-                );
-                currentState.whenKeysReadyDeferred.reject(
-                  new Error(`user data missing: ${missing.join(", ")}`)
-                );
-                return;
-              }
-              currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
-            },
-            err => {
-              currentState.whenKeysReadyDeferred.reject(err);
-            }
-          );
-        } else {
-          currentState.whenKeysReadyDeferred.reject("No keyFetchToken");
-        }
-      }
-      return await currentState.resolve(
-        currentState.whenKeysReadyDeferred.promise
-      );
-    } catch (err) {
-      return currentState.resolve(this._handleTokenError(err));
-    }
-  },
-
-  async fetchAndUnwrapKeys(keyFetchToken) {
-    if (logPII) {
-      log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
-    }
-    let currentState = this.currentAccountState;
-    // Sign out if we don't have a key fetch token.
-    if (!keyFetchToken) {
-      log.warn("improper fetchAndUnwrapKeys() call: token missing");
-      await this.signOut();
-      return currentState.resolve(null);
-    }
-
-    let { wrapKB } = await this.fetchKeys(keyFetchToken);
-
-    let data = await currentState.getUserAccountData();
-
-    // Sanity check that the user hasn't changed out from under us
-    if (data.keyFetchToken !== keyFetchToken) {
-      throw new Error("Signed in user changed while fetching keys!");
-    }
-
-    // Next statements must be synchronous until we updateUserAccountData
-    // so that we don't risk getting into a weird state.
-    let kBbytes = CryptoUtils.xor(
-      CommonUtils.hexToBytes(data.unwrapBKey),
-      wrapKB
-    );
-
-    if (logPII) {
-      log.debug("kBbytes: " + kBbytes);
-    }
-    let updateData = {
-      ...(await this._deriveKeys(data.uid, kBbytes)),
-      keyFetchToken: null, // null values cause the item to be removed.
-      unwrapBKey: null,
-    };
-
-    log.debug(
-      "Keys Obtained:" +
-        DERIVED_KEYS_NAMES.map(k => `${k}=${!!updateData[k]}`).join(", ")
-    );
-    if (logPII) {
-      log.debug(
-        "Keys Obtained:" +
-          DERIVED_KEYS_NAMES.map(k => `${k}=${updateData[k]}`).join(", ")
-      );
-    }
-
-    await currentState.updateUserAccountData(updateData);
-    // We are now ready for business. This should only be invoked once
-    // per setSignedInUser(), regardless of whether we've rebooted since
-    // setSignedInUser() was called.
-    await this.notifyObservers(ONVERIFIED_NOTIFICATION);
-    // Some parts of the device registration depend on the Sync keys being available,
-    // so let's re-trigger it now that we have them.
-    await this.updateDeviceRegistration();
-    data = await currentState.getUserAccountData();
-    return currentState.resolve(data);
-  },
-
-  async _deriveKeys(uid, kBbytes) {
-    return {
-      kSync: CommonUtils.bytesAsHex(await this._deriveSyncKey(kBbytes)),
-      kXCS: CommonUtils.bytesAsHex(this._deriveXClientState(kBbytes)),
-      kExtSync: CommonUtils.bytesAsHex(
-        await this._deriveWebExtSyncStoreKey(kBbytes)
-      ),
-      kExtKbHash: CommonUtils.bytesAsHex(
-        this._deriveWebExtKbHash(uid, kBbytes)
-      ),
-    };
-  },
-
-  /**
-   * Derive the Sync Key given the byte string kB.
-   *
-   * @returns HKDF(kB, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)
-   */
-  _deriveSyncKey(kBbytes) {
-    return CryptoUtils.hkdfLegacy(
-      kBbytes,
-      undefined,
-      "identity.mozilla.com/picl/v1/oldsync",
-      2 * 32
-    );
-  },
-
-  /**
-   * Derive the WebExtensions Sync Storage Key given the byte string kB.
-   *
-   * @returns HKDF(kB, undefined, "identity.mozilla.com/picl/v1/chrome.storage.sync", 64)
-   */
-  _deriveWebExtSyncStoreKey(kBbytes) {
-    return CryptoUtils.hkdfLegacy(
-      kBbytes,
-      undefined,
-      "identity.mozilla.com/picl/v1/chrome.storage.sync",
-      2 * 32
-    );
-  },
-
-  /**
-   * Derive the WebExtensions kbHash given the byte string kB.
-   *
-   * @returns SHA256(uid + kB)
-   */
-  _deriveWebExtKbHash(uid, kBbytes) {
-    return this._sha256(uid + kBbytes);
-  },
-
-  /**
-   * Derive the X-Client-State header given the byte string kB.
-   *
-   * @returns SHA256(kB)[:16]
-   */
-  _deriveXClientState(kBbytes) {
-    return this._sha256(kBbytes).slice(0, 16);
-  },
-
-  _sha256(bytes) {
-    let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
-      Ci.nsICryptoHash
-    );
-    hasher.init(hasher.SHA256);
-    return CryptoUtils.digestBytes(bytes, hasher);
-  },
-
   async getAssertionFromCert(data, keyPair, cert, audience) {
     log.debug("getAssertionFromCert");
     let options = {
       duration: ASSERTION_LIFETIME,
       localtimeOffsetMsec: this.localtimeOffsetMsec,
       now: this.now(),
     };
     let currentState = this.currentAccountState;
@@ -1441,61 +1390,18 @@ FxAccountsInternal.prototype = {
       await currentState.updateUserAccountData(toUpdate);
     }
     return {
       keyPair: keyPair.rawKeyPair,
       certificate,
     };
   },
 
-  /**
-   * @param {String} scope Single key bearing scope
-   */
-  async getKeyForScope(scope, { keyRotationTimestamp }) {
-    if (scope !== SCOPE_OLD_SYNC) {
-      throw new Error(`Unavailable key material for ${scope}`);
-    }
-    let { kSync, kXCS } = await this.getKeys();
-    if (!kSync || !kXCS) {
-      throw new Error("Could not find requested key.");
-    }
-    kXCS = ChromeUtils.base64URLEncode(CommonUtils.hexToArrayBuffer(kXCS), {
-      pad: false,
-    });
-    kSync = ChromeUtils.base64URLEncode(CommonUtils.hexToArrayBuffer(kSync), {
-      pad: false,
-    });
-    const kid = `${keyRotationTimestamp}-${kXCS}`;
-    return {
-      scope,
-      kid,
-      k: kSync,
-      kty: "oct",
-    };
-  },
-
-  /**
-   * @param {String} scopes Space separated requested scopes
-   */
-  async getScopedKeys(scopes, clientId) {
-    const { sessionToken } = await this._getVerifiedAccountOrReject();
-    const keyData = await this.fxAccountsClient.getScopedKeyData(
-      sessionToken,
-      clientId,
-      scopes
-    );
-    const scopedKeys = {};
-    for (const [scope, data] of Object.entries(keyData)) {
-      scopedKeys[scope] = await this.getKeyForScope(scope, data);
-    }
-    return scopedKeys;
-  },
-
-  getUserAccountData() {
-    return this.currentAccountState.getUserAccountData();
+  getUserAccountData(fieldNames = null) {
+    return this.currentAccountState.getUserAccountData(fieldNames);
   },
 
   isUserEmailVerified: function isUserEmailVerified(data) {
     return !!(data && data.verified);
   },
 
   /**
    * Setup for and if necessary do email verification polling.
@@ -1528,18 +1434,25 @@ FxAccountsInternal.prototype = {
     //
     // Login is truly complete once keys have been fetched, so once getKeys()
     // obtains and stores kSync kXCS kExtSync and kExtKbHash, it will fire the
     // onverified observer notification.
 
     // The callers of startVerifiedCheck never consume a returned promise (ie,
     // this is simply kicking off a background fetch) so we must add a rejection
     // handler to avoid runtime warnings about the rejection not being handled.
+
     this.whenVerified(data).then(
-      () => this.getKeys(),
+      () => {
+        log.info("the user became verified");
+        // We are now ready for business. This should only be invoked once
+        // per setSignedInUser(), regardless of whether we've rebooted since
+        // setSignedInUser() was called.
+        return this.notifyObservers(ONVERIFIED_NOTIFICATION);
+      },
       err => log.info("startVerifiedCheck promise was rejected: " + err)
     );
   },
 
   whenVerified(data) {
     let currentState = this.currentAccountState;
     if (data.verified) {
       log.debug("already verified");
@@ -1673,208 +1586,188 @@ FxAccountsInternal.prototype = {
     }
   },
 
   _rejectWhenVerified(currentState, error) {
     currentState.whenVerifiedDeferred.reject(error);
     delete currentState.whenVerifiedDeferred;
   },
 
-  /**
-   * Get an OAuth token for the user
-   *
-   * @param options
-   *        {
-   *          scope: (string/array) the oauth scope(s) being requested. As a
-   *                 convenience, you may pass a string if only one scope is
-   *                 required, or an array of strings if multiple are needed.
-   *        }
-   *
-   * @return Promise.<string | Error>
-   *        The promise resolves the oauth token as a string or rejects with
-   *        an error object ({error: ERROR, details: {}}) of the following:
-   *          INVALID_PARAMETER
-   *          NO_ACCOUNT
-   *          UNVERIFIED_ACCOUNT
-   *          NETWORK_ERROR
-   *          AUTH_ERROR
-   *          UNKNOWN_ERROR
-   */
-  async getOAuthToken(options = {}) {
-    log.debug("getOAuthToken enter");
-    let scope = options.scope;
-    if (typeof scope === "string") {
-      scope = [scope];
-    }
-
-    if (!scope || !scope.length) {
-      throw this._error(
-        ERROR_INVALID_PARAMETER,
-        "Missing or invalid 'scope' option"
-      );
-    }
-
-    await this._getVerifiedAccountOrReject();
-
-    // Early exit for a cached token.
-    let currentState = this.currentAccountState;
-    let cached = currentState.getCachedToken(scope);
-    if (cached) {
-      log.debug("getOAuthToken returning a cached token");
-      return cached.token;
-    }
-
-    // Build the string we use in our "inflight" map and that we send to the
-    // server. Because it's used as a key in the map we sort the scopes.
-    let scopeString = scope.sort().join(" ");
-    let client = options.client || this.oauthClient;
-    let oAuthURL = client.serverURL.href;
-
-    // We keep a map of in-flight requests to avoid multiple promise-based
-    // consumers concurrently requesting the same token.
-    let maybeInFlight = currentState.inFlightTokenRequests.get(scopeString);
-    if (maybeInFlight) {
-      log.debug("getOAuthToken has an in-flight request for this scope");
-      return maybeInFlight;
-    }
-
-    // We need to start a new fetch and stick the promise in our in-flight map
-    // and remove it when it resolves.
-    let promise = this._doTokenFetch(client, scopeString)
-      .then(token => {
-        // As a sanity check, ensure something else hasn't raced getting a token
-        // of the same scope. If something has we just make noise rather than
-        // taking any concrete action because it should never actually happen.
-        if (currentState.getCachedToken(scope)) {
-          log.error(`detected a race for oauth token with scope ${scope}`);
-        }
-        // If we got one, cache it.
-        if (token) {
-          let entry = { token, server: oAuthURL };
-          currentState.setCachedToken(scope, entry);
-        }
-        return token;
-      })
-      .finally(() => {
-        // Remove ourself from the in-flight map. There's no need to check the
-        // result of .delete() to handle a signout race, because setCachedToken
-        // above will fail in that case and cause the entire call to fail.
-        currentState.inFlightTokenRequests.delete(scopeString);
-      });
-
-    currentState.inFlightTokenRequests.set(scopeString, promise);
-    return promise;
-  },
-
+  // Does the actual fetch of an oauth token for getOAuthToken()
   async _doTokenFetch(client, scopeString) {
     let oAuthURL = client.serverURL.href;
     try {
       log.debug("getOAuthToken fetching new token from", oAuthURL);
       let assertion = await this.getAssertion(oAuthURL);
       let result = await client.getTokenFromAssertion(assertion, scopeString);
       let token = result.access_token;
       return token;
     } catch (err) {
       throw this._errorToErrorClass(err);
     }
   },
 
+  getOAuthToken(options = {}) {
+    log.debug("getOAuthToken enter");
+    let scope = options.scope;
+    if (typeof scope === "string") {
+      scope = [scope];
+    }
+
+    if (!scope || !scope.length) {
+      return Promise.reject(
+        this._error(
+          ERROR_INVALID_PARAMETER,
+          "Missing or invalid 'scope' option"
+        )
+      );
+    }
+
+    return this.withVerifiedAccountState(async currentState => {
+      // Early exit for a cached token.
+      let cached = currentState.getCachedToken(scope);
+      if (cached) {
+        log.debug("getOAuthToken returning a cached token");
+        return cached.token;
+      }
+
+      // Build the string we use in our "inflight" map and that we send to the
+      // server. Because it's used as a key in the map we sort the scopes.
+      let scopeString = scope.sort().join(" ");
+      let client = options.client || this.oauthClient;
+      let oAuthURL = client.serverURL.href;
+
+      // We keep a map of in-flight requests to avoid multiple promise-based
+      // consumers concurrently requesting the same token.
+      let maybeInFlight = currentState.inFlightTokenRequests.get(scopeString);
+      if (maybeInFlight) {
+        log.debug("getOAuthToken has an in-flight request for this scope");
+        return maybeInFlight;
+      }
+
+      // We need to start a new fetch and stick the promise in our in-flight map
+      // and remove it when it resolves.
+      let promise = this._doTokenFetch(client, scopeString)
+        .then(token => {
+          // As a sanity check, ensure something else hasn't raced getting a token
+          // of the same scope. If something has we just make noise rather than
+          // taking any concrete action because it should never actually happen.
+          if (currentState.getCachedToken(scope)) {
+            log.error(`detected a race for oauth token with scope ${scope}`);
+          }
+          // If we got one, cache it.
+          if (token) {
+            let entry = { token, server: oAuthURL };
+            currentState.setCachedToken(scope, entry);
+          }
+          return token;
+        })
+        .finally(() => {
+          // Remove ourself from the in-flight map. There's no need to check the
+          // result of .delete() to handle a signout race, because setCachedToken
+          // above will fail in that case and cause the entire call to fail.
+          currentState.inFlightTokenRequests.delete(scopeString);
+        });
+
+      currentState.inFlightTokenRequests.set(scopeString, promise);
+      return promise;
+    });
+  },
+
+  removeCachedOAuthToken(options) {
+    if (!options.token || typeof options.token !== "string") {
+      throw this._error(
+        ERROR_INVALID_PARAMETER,
+        "Missing or invalid 'token' option"
+      );
+    }
+    return this.withCurrentAccountState(currentState => {
+      let existing = currentState.removeCachedToken(options.token);
+      if (existing) {
+        // background destroy.
+        this._destroyOAuthToken(existing).catch(err => {
+          log.warn("FxA failed to revoke a cached token", err);
+        });
+      }
+    });
+  },
+
   /**
    *
    * @param {String} clientId
    * @param {String} scope Space separated requested scopes
    * @param {Object} jwk
    */
   async createKeysJWE(clientId, scope, jwk) {
     let scopedKeys = await this.getScopedKeys(scope, clientId);
     scopedKeys = new TextEncoder().encode(JSON.stringify(scopedKeys));
     return jwcrypto.generateJWE(jwk, scopedKeys);
   },
 
-  /**
-   * Retrieves an OAuth authorization code
-   *
-   * @param {Object} options
-   * @param options.client_id
-   * @param options.state
-   * @param options.scope
-   * @param options.access_type
-   * @param options.code_challenge_method
-   * @param options.code_challenge
-   * @param [options.keys_jwe]
-   * @returns {Promise<Object>} Object containing "code" and "state" properties.
-   */
-  async authorizeOAuthCode(options) {
-    await this._getVerifiedAccountOrReject();
-    const client = this.oauthClient;
-    const oAuthURL = client.serverURL.href;
-    const params = { ...options };
-    if (params.keys_jwk) {
-      const jwk = JSON.parse(
-        new TextDecoder().decode(
-          ChromeUtils.base64URLDecode(params.keys_jwk, { padding: "reject" })
-        )
-      );
-      params.keys_jwe = await this.createKeysJWE(
-        params.client_id,
-        params.scope,
-        jwk
-      );
-      delete params.keys_jwk;
-    }
-    try {
-      const assertion = await this.getAssertion(oAuthURL);
-      return client.authorizeCodeFromAssertion(assertion, params);
-    } catch (err) {
-      throw this._errorToErrorClass(err);
-    }
-  },
-
-  /**
-   * Remove an OAuth token from the token cache.  Callers should call this
-   * after they determine a token is invalid, so a new token will be fetched
-   * on the next call to getOAuthToken().
-   *
-   * @param options
-   *        {
-   *          token: (string) A previously fetched token.
-   *        }
-   * @return Promise.<undefined> This function will always resolve, even if
-   *         an unknown token is passed.
-   */
-  async removeCachedOAuthToken(options) {
-    if (!options.token || typeof options.token !== "string") {
-      throw this._error(
-        ERROR_INVALID_PARAMETER,
-        "Missing or invalid 'token' option"
-      );
-    }
-    let currentState = this.currentAccountState;
-    let existing = currentState.removeCachedToken(options.token);
-    if (existing) {
-      // background destroy.
-      this._destroyOAuthToken(existing).catch(err => {
-        log.warn("FxA failed to revoke a cached token", err);
-      });
-    }
-  },
-
   async _getVerifiedAccountOrReject() {
     let data = await this.currentAccountState.getUserAccountData();
     if (!data) {
       // No signed-in user
       throw this._error(ERROR_NO_ACCOUNT);
     }
     if (!this.isUserEmailVerified(data)) {
       // Signed-in user has not verified email
       throw this._error(ERROR_UNVERIFIED_ACCOUNT);
     }
     return data;
   },
 
+  // _handle* methods used by push, used when the account/device status is
+  // changed on a different device.
+  async _handleAccountDestroyed(uid) {
+    let state = this.currentAccountState;
+    const accountData = await state.getUserAccountData();
+    const localUid = accountData ? accountData.uid : null;
+    if (!localUid) {
+      log.info(
+        `Account destroyed push notification received, but we're already logged-out`
+      );
+      return null;
+    }
+    if (uid == localUid) {
+      const data = JSON.stringify({ isLocalDevice: true });
+      await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
+      return this.signOut(true);
+    }
+    log.info(
+      `The destroyed account uid doesn't match with the local uid. ` +
+        `Local: ${localUid}, account uid destroyed: ${uid}`
+    );
+    return null;
+  },
+
+  async _handleDeviceDisconnection(deviceId) {
+    let state = this.currentAccountState;
+    const accountData = await state.getUserAccountData();
+    if (!accountData || !accountData.device) {
+      // Nothing we can do here.
+      return;
+    }
+    const localDeviceId = accountData.device.id;
+    const isLocalDevice = deviceId == localDeviceId;
+    if (isLocalDevice) {
+      this.signOut(true);
+    }
+    const data = JSON.stringify({ isLocalDevice });
+    await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
+  },
+
+  async _handleEmailUpdated(newEmail) {
+    Services.prefs.setStringPref(
+      PREF_LAST_FXA_USER,
+      CryptoUtils.sha256Base64(newEmail)
+    );
+    await this.currentAccountState.updateUserAccountData({ email: newEmail });
+  },
+
   /*
    * Coerce an error into one of the general error cases:
    *          NETWORK_ERROR
    *          AUTH_ERROR
    *          UNKNOWN_ERROR
    *
    * These errors will pass through:
    *          INVALID_PARAMETER
@@ -1907,146 +1800,50 @@ FxAccountsInternal.prototype = {
     });
     let reason = new Error(aError);
     if (aDetails) {
       reason.details = aDetails;
     }
     return reason;
   },
 
-  /**
-   * Get the user's account and profile data if it is locally cached. If
-   * not cached it will return null, but cause the profile data to be fetched
-   * in the background, after which a ON_PROFILE_CHANGE_NOTIFICATION
-   * observer notification will be sent, at which time this can be called
-   * again to obtain the most recent profile info.
-   *
-   * @return Promise.<object | Error>
-   *        The promise resolves to an accountData object with extra profile
-   *        information such as profileImageUrl, or rejects with
-   *        an error object ({error: ERROR, details: {}}) of the following:
-   *          INVALID_PARAMETER
-   *          NO_ACCOUNT
-   *          UNVERIFIED_ACCOUNT
-   *          NETWORK_ERROR
-   *          AUTH_ERROR
-   *          UNKNOWN_ERROR
-   */
-  getSignedInUserProfile() {
-    let currentState = this.currentAccountState;
-    return this.profile
-      .getProfile()
-      .then(
-        profileData => {
-          let profile = Cu.cloneInto(profileData, {});
-          return currentState.resolve(profile);
-        },
-        error => {
-          log.error("Could not retrieve profile data", error);
-          return currentState.reject(error);
-        }
-      )
-      .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.
   async updateDeviceRegistration() {
     try {
-      const signedInUser = await this.getSignedInUser();
+      const signedInUser = await this.currentAccountState.getUserAccountData();
       if (signedInUser) {
         await this._registerOrUpdateDevice(signedInUser);
       }
     } catch (error) {
       await this._logErrorAndResetDeviceRegistrationVersion(error);
     }
   },
 
-  async handleDeviceDisconnection(deviceId) {
-    const accountData = await this.currentAccountState.getUserAccountData();
-    if (!accountData || !accountData.device) {
-      // Nothing we can do here.
-      return;
-    }
-    const localDeviceId = accountData.device.id;
-    const isLocalDevice = deviceId == localDeviceId;
-    if (isLocalDevice) {
-      this.signOut(true);
-    }
-    const data = JSON.stringify({ isLocalDevice });
-    await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
-  },
-
-  handleEmailUpdated(newEmail) {
-    Services.prefs.setStringPref(
-      PREF_LAST_FXA_USER,
-      CryptoUtils.sha256Base64(newEmail)
-    );
-    return this.currentAccountState.updateUserAccountData({ email: newEmail });
-  },
-
-  async handleAccountDestroyed(uid) {
-    const accountData = await this.currentAccountState.getUserAccountData();
-    const localUid = accountData ? accountData.uid : null;
-    if (!localUid) {
-      log.info(
-        `Account destroyed push notification received, but we're already logged-out`
-      );
-      return null;
-    }
-    if (uid == localUid) {
-      const data = JSON.stringify({ isLocalDevice: true });
-      await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
-      return this.signOut(true);
-    }
-    log.info(
-      `The destroyed account uid doesn't match with the local uid. ` +
-        `Local: ${localUid}, account uid destroyed: ${uid}`
-    );
-    return null;
-  },
-
   /**
-   * Delete all the cached persisted credentials we store for FxA.
+   * Delete all the persisted credentials we store for FxA. After calling
+   * this, the user will be forced to re-authenticate to continue.
    *
    * @return Promise resolves when the user data has been persisted
    */
-  resetCredentials() {
+  dropCredentials(state) {
     // 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);
-  },
-
-  getProfileCache() {
-    return this.currentAccountState
-      .getUserAccountData(["profileCache"])
-      .then(data => (data ? data.profileCache : null));
-  },
-
-  setProfileCache(profileCache) {
-    return this.currentAccountState.updateUserAccountData({
-      profileCache,
-    });
-  },
-
-  // @returns Promise<Subscription>.
-  getPushSubscription() {
-    return this.fxaPushService.getSubscription();
+    return state.updateUserAccountData(updateData);
   },
 
   async availableCommands() {
     if (
       !Services.prefs.getBoolPref("identity.fxaccounts.commands.enabled", true)
     ) {
       return {};
     }
@@ -2105,17 +1902,19 @@ FxAccountsInternal.prototype = {
           deviceName,
           this.device.getLocalType(),
           deviceOptions
         );
         Services.obs.notifyObservers(null, ON_NEW_DEVICE_ID);
       }
 
       // Get the freshest device props before updating them.
-      let { device: deviceProps } = await this.getSignedInUser();
+      let {
+        device: deviceProps,
+      } = await this.currentAccountState.getUserAccountData();
       await this.currentAccountState.updateUserAccountData({
         device: {
           ...deviceProps, // Copy the other properties (e.g. handledCommands).
           id: device.id,
           registrationVersion: this.DEVICE_REGISTRATION_VERSION,
           registeredCommandsKeys: availableCommandsKeys,
         },
       });
@@ -2223,26 +2022,26 @@ FxAccountsInternal.prototype = {
       .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);
         }
         log.info("clearing credentials to handle invalid token error");
-        return this.resetCredentials();
+        return this.dropCredentials(this.currentAccountState);
       })
       .then(() => Promise.reject(err));
   },
 };
 
 // A getter for the instance to export
 XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
   let a = new FxAccounts();
 
   // XXX Bug 947061 - We need a strategy for resuming email verification after
   // browser restart
-  a.loadAndPoll();
+  a._internal.loadAndPoll();
 
   return a;
 });
 
 var EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
--- a/services/fxaccounts/FxAccountsCommands.js
+++ b/services/fxaccounts/FxAccountsCommands.js
@@ -23,31 +23,31 @@ const { Observers } = ChromeUtils.import
 XPCOMUtils.defineLazyModuleGetters(this, {
   BulkKeyBundle: "resource://services-sync/keys.js",
   CommonUtils: "resource://services-common/utils.js",
   CryptoUtils: "resource://services-crypto/utils.js",
   CryptoWrapper: "resource://services-sync/record.js",
 });
 
 class FxAccountsCommands {
-  constructor(fxAccounts) {
-    this._fxAccounts = fxAccounts;
-    this.sendTab = new SendTab(this, fxAccounts);
+  constructor(fxAccountsInternal) {
+    this._fxai = fxAccountsInternal;
+    this.sendTab = new SendTab(this, fxAccountsInternal);
   }
 
   async invoke(command, device, payload) {
-    const userData = await this._fxAccounts.getSignedInUser();
+    const userData = await this._fxai.currentAccountState.getSignedInUser();
     if (!userData) {
       throw new Error("No user.");
     }
     const { sessionToken } = userData;
     if (!sessionToken) {
       throw new Error("_send called without a session token.");
     }
-    const client = this._fxAccounts.getAccountsClient();
+    const client = this._fxai.fxAccountsClient;
     await client.invokeCommand(sessionToken, command, device.id, payload);
     log.info(`Payload sent to device ${device.id}.`);
   }
 
   /**
    * Poll and handle device commands for the current device.
    * This method can be called either in response to a Push message,
    * or by itself as a "commands recovery" mechanism.
@@ -62,69 +62,67 @@ class FxAccountsCommands {
     // to fetch missed messages.
     const scheduledFetch = receivedIndex == 0;
     if (
       !Services.prefs.getBoolPref("identity.fxaccounts.commands.enabled", true)
     ) {
       return false;
     }
     log.info(`Polling device commands.`);
-    await this._fxAccounts._withCurrentAccountState(
-      async (getUserData, updateUserData) => {
-        const { device } = await getUserData(["device"]);
-        if (!device) {
-          throw new Error("No device registration.");
-        }
-        // We increment lastCommandIndex by 1 because the server response includes the current index.
-        // If we don't have a `lastCommandIndex` stored, we fall back on the index from the push message we just got.
-        const lastCommandIndex = device.lastCommandIndex + 1 || receivedIndex;
-        // We have already received this message before.
-        if (receivedIndex > 0 && receivedIndex < lastCommandIndex) {
-          return;
+    await this._fxai.withCurrentAccountState(async state => {
+      const { device } = await state.getUserAccountData(["device"]);
+      if (!device) {
+        throw new Error("No device registration.");
+      }
+      // We increment lastCommandIndex by 1 because the server response includes the current index.
+      // If we don't have a `lastCommandIndex` stored, we fall back on the index from the push message we just got.
+      const lastCommandIndex = device.lastCommandIndex + 1 || receivedIndex;
+      // We have already received this message before.
+      if (receivedIndex > 0 && receivedIndex < lastCommandIndex) {
+        return;
+      }
+      const { index, messages } = await this._fetchDeviceCommands(
+        lastCommandIndex
+      );
+      if (messages.length) {
+        await state.updateUserAccountData({
+          device: { ...device, lastCommandIndex: index },
+        });
+        log.info(`Handling ${messages.length} messages`);
+        if (scheduledFetch) {
+          Services.telemetry.scalarAdd(
+            "identity.fxaccounts.missed_commands_fetched",
+            messages.length
+          );
         }
-        const { index, messages } = await this._fetchDeviceCommands(
-          lastCommandIndex
-        );
-        if (messages.length) {
-          await updateUserData({
-            device: { ...device, lastCommandIndex: index },
-          });
-          log.info(`Handling ${messages.length} messages`);
-          if (scheduledFetch) {
-            Services.telemetry.scalarAdd(
-              "identity.fxaccounts.missed_commands_fetched",
-              messages.length
-            );
-          }
-          await this._handleCommands(messages);
-        }
+        await this._handleCommands(messages);
       }
-    );
+    });
     return true;
   }
 
   async _fetchDeviceCommands(index, limit = null) {
-    const userData = await this._fxAccounts.getSignedInUser();
+    const userData = await this._fxai.getUserAccountData();
     if (!userData) {
       throw new Error("No user.");
     }
     const { sessionToken } = userData;
     if (!sessionToken) {
       throw new Error("No session token.");
     }
-    const client = this._fxAccounts.getAccountsClient();
+    const client = this._fxai.fxAccountsClient;
     const opts = { index };
     if (limit != null) {
       opts.limit = limit;
     }
     return client.getCommands(sessionToken, opts);
   }
 
   async _handleCommands(messages) {
-    const fxaDevices = await this._fxAccounts.getDeviceList();
+    const fxaDevices = await this._fxai.getDeviceList();
     // We debounce multiple incoming tabs so we show a single notification.
     const tabsReceived = [];
     for (const { data } of messages) {
       const { command, payload, sender: senderId } = data;
       const sender = senderId ? fxaDevices.find(d => d.id == senderId) : null;
       if (!sender) {
         log.warn(
           "Incoming command is from an unknown device (maybe disconnected?)"
@@ -157,19 +155,19 @@ class FxAccountsCommands {
 /**
  * Send Tab is built on top of FxA commands.
  *
  * Devices exchange keys wrapped in kSync between themselves (getEncryptedKey)
  * during the device registration flow. The FxA server can theorically never
  * retrieve the send tab keys since it doesn't know kSync.
  */
 class SendTab {
-  constructor(commands, fxAccounts) {
+  constructor(commands, fxAccountsInternal) {
     this._commands = commands;
-    this._fxAccounts = fxAccounts;
+    this._fxai = fxAccountsInternal;
   }
   /**
    * @param {Device[]} to - Device objects (typically returned by fxAccounts.getDevicesList()).
    * @param {Object} tab
    * @param {string} tab.url
    * @param {string} tab.title
    * @returns A report object, in the shape of
    *          {succeded: [Device], error: [{device: Device, error: Exception}]}
@@ -226,17 +224,17 @@ class SendTab {
     };
   }
 
   async _encrypt(bytes, device) {
     let bundle = device.availableCommands[COMMAND_SENDTAB];
     if (!bundle) {
       throw new Error(`Device ${device.id} does not have send tab keys.`);
     }
-    const { kSync, kXCS: ourKid } = await this._fxAccounts.getKeys();
+    const { kSync, kXCS: ourKid } = await this._fxai.keys.getKeys();
     const { kid: theirKid } = JSON.parse(
       device.availableCommands[COMMAND_SENDTAB]
     );
     if (theirKid != ourKid) {
       throw new Error("Target Send Tab key ID is different from ours");
     }
     const json = JSON.parse(bundle);
     const wrapper = new CryptoWrapper();
@@ -250,17 +248,17 @@ class SendTab {
       bytes,
       publicKey,
       authSecret
     );
     return urlsafeBase64Encode(encrypted);
   }
 
   async _getKeys() {
-    const { device } = await this._fxAccounts.getSignedInUser();
+    const { device } = await this._fxai.getUserAccountData(["device"]);
     return device && device.sendTabKeys;
   }
 
   async _decrypt(ciphertext) {
     let { privateKey, publicKey, authSecret } = await this._getKeys();
     publicKey = urlsafeBase64Decode(publicKey);
     authSecret = urlsafeBase64Decode(authSecret);
     ciphertext = new Uint8Array(urlsafeBase64Decode(ciphertext));
@@ -279,27 +277,25 @@ class SendTab {
     publicKey = urlsafeBase64Encode(publicKey);
     let authSecret = PushCrypto.generateAuthenticationSecret();
     authSecret = urlsafeBase64Encode(authSecret);
     const sendTabKeys = {
       publicKey,
       privateKey,
       authSecret,
     };
-    await this._fxAccounts._withCurrentAccountState(
-      async (getUserData, updateUserData) => {
-        const { device } = await getUserData();
-        await updateUserData({
-          device: {
-            ...device,
-            sendTabKeys,
-          },
-        });
-      }
-    );
+    await this._fxai.withCurrentAccountState(async state => {
+      const { device } = await state.getUserAccountData(["device"]);
+      await state.updateUserAccountData({
+        device: {
+          ...device,
+          sendTabKeys,
+        },
+      });
+    });
     return sendTabKeys;
   }
 
   async getEncryptedKey() {
     let sendTabKeys = await this._getKeys();
     if (!sendTabKeys) {
       sendTabKeys = await this._generateAndPersistKeys();
     }
@@ -307,17 +303,17 @@ class SendTab {
     const keyToEncrypt = {
       publicKey: sendTabKeys.publicKey,
       authSecret: sendTabKeys.authSecret,
     };
     // getEncryptedKey() can be called right after a sign-in/up to FxA:
     // We get -cached- keys using getSignedInUser() instead of getKeys()
     // because we will await on getKeys() which is already awaiting on
     // the promise we return.
-    const { kSync, kXCS } = await this._fxAccounts.getSignedInUser();
+    const { kSync, kXCS } = await this._fxai.getUserAccountData();
     if (!kSync || !kXCS) {
       return null;
     }
     const wrapper = new CryptoWrapper();
     wrapper.cleartext = keyToEncrypt;
     const keyBundle = BulkKeyBundle.fromHexKey(kSync);
     await wrapper.encrypt(keyBundle);
     return JSON.stringify({
--- a/services/fxaccounts/FxAccountsDevice.jsm
+++ b/services/fxaccounts/FxAccountsDevice.jsm
@@ -30,29 +30,29 @@ XPCOMUtils.defineLazyPreferenceGetter(
 );
 
 const PREF_DEPRECATED_DEVICE_NAME = "services.sync.client.name";
 
 // Everything to do with FxA devices.
 // TODO: Move more device stuff from FxAccounts.jsm into here - eg, device
 // registration, device lists, etc.
 class FxAccountsDevice {
-  constructor(fxa) {
-    this._fxa = fxa;
+  constructor(fxai) {
+    this._fxai = fxai;
   }
 
   async getLocalId() {
-    let data = await this._fxa.currentAccountState.getUserAccountData();
+    let data = await this._fxai.currentAccountState.getUserAccountData();
     if (!data) {
       // Without a signed-in user, there can be no device id.
       return null;
     }
     const { device } = data;
-    if (await this._fxa.checkDeviceUpdateNeeded(device)) {
-      return this._fxa._registerOrUpdateDevice(data);
+    if (await this._fxai.checkDeviceUpdateNeeded(device)) {
+      return this._fxai._registerOrUpdateDevice(data);
     }
     // Return the device id that we already registered with the server.
     return device.id;
   }
 
   // Generate a client name if we don't have a useful one yet
   getDefaultLocalName() {
     let env = Cc["@mozilla.org/process/environment;1"].getService(
@@ -132,17 +132,17 @@ class FxAccountsDevice {
     }
     return name;
   }
 
   setLocalName(newName) {
     Services.prefs.clearUserPref(PREF_DEPRECATED_DEVICE_NAME);
     Services.prefs.setStringPref(PREF_LOCAL_DEVICE_NAME, newName);
     // Update the registration in the background.
-    this._fxa.updateDeviceRegistration().catch(error => {
+    this._fxai.updateDeviceRegistration().catch(error => {
       log.warn("failed to update fxa device registration", error);
     });
   }
 
   getLocalType() {
     return DEVICE_TYPE_DESKTOP;
   }
 }
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsKeys.jsm
@@ -0,0 +1,314 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { PromiseUtils } = ChromeUtils.import(
+  "resource://gre/modules/PromiseUtils.jsm"
+);
+const { CommonUtils } = ChromeUtils.import(
+  "resource://services-common/utils.js"
+);
+
+const { CryptoUtils } = ChromeUtils.import(
+  "resource://services-crypto/utils.js"
+);
+
+const { DERIVED_KEYS_NAMES, SCOPE_OLD_SYNC, log, logPII } = ChromeUtils.import(
+  "resource://gre/modules/FxAccountsCommon.js"
+);
+
+class FxAccountsKeys {
+  constructor(fxAccountsInternal) {
+    this._fxia = fxAccountsInternal;
+  }
+
+  /**
+   * Checks if we currently have encryption keys or if we have enough to
+   * be able to successfully fetch them for the signed-in-user.
+   */
+  canGetKeys() {
+    return this._fxia.withCurrentAccountState(async currentState => {
+      let userData = await currentState.getUserAccountData();
+      if (!userData) {
+        throw new Error("Can't possibly get keys; User is not signed in");
+      }
+      // - keyFetchToken means we can almost certainly grab them.
+      // - kSync, kXCS, kExtSync and kExtKbHash means we already have them.
+      // - kB is deprecated but |getKeys| will help us migrate to kSync and friends.
+      return (
+        userData &&
+        (userData.keyFetchToken ||
+          DERIVED_KEYS_NAMES.every(k => userData[k]) ||
+          userData.kB)
+      );
+    });
+  }
+
+  /**
+   * 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:
+   *        {
+   *          email: The user's email address
+   *          uid: The user's unique id
+   *          sessionToken: Session for the FxA server
+   *          kSync: An encryption key for Sync
+   *          kXCS: A key hash of kB for the X-Client-State header
+   *          kExtSync: An encryption key for WebExtensions syncing
+   *          kExtKbHash: A key hash of kB for WebExtensions syncing
+   *          verified: email verification status
+   *        }
+   *        or null if no user is signed in
+   */
+  async getKeys() {
+    return this._fxia.withCurrentAccountState(async currentState => {
+      try {
+        let userData = await currentState.getUserAccountData();
+        if (!userData) {
+          throw new Error("Can't get keys; User is not signed in");
+        }
+        if (userData.kB) {
+          // Bug 1426306 - Migrate from kB to derived keys.
+          log.info("Migrating kB to derived keys.");
+          const { uid, kB } = userData;
+          await currentState.updateUserAccountData({
+            uid,
+            ...(await this._deriveKeys(uid, CommonUtils.hexToBytes(kB))),
+            kA: null, // Remove kA and kB from storage.
+            kB: null,
+          });
+          userData = await currentState.getUserAccountData();
+        }
+        if (DERIVED_KEYS_NAMES.every(k => userData[k])) {
+          return userData;
+        }
+        if (!currentState.whenKeysReadyDeferred) {
+          currentState.whenKeysReadyDeferred = PromiseUtils.defer();
+          if (userData.keyFetchToken) {
+            this.fetchAndUnwrapKeys(userData.keyFetchToken).then(
+              dataWithKeys => {
+                if (DERIVED_KEYS_NAMES.some(k => !dataWithKeys[k])) {
+                  const missing = DERIVED_KEYS_NAMES.filter(
+                    k => !dataWithKeys[k]
+                  );
+                  currentState.whenKeysReadyDeferred.reject(
+                    new Error(`user data missing: ${missing.join(", ")}`)
+                  );
+                  return;
+                }
+                currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
+              },
+              err => {
+                currentState.whenKeysReadyDeferred.reject(err);
+              }
+            );
+          } else {
+            currentState.whenKeysReadyDeferred.reject("No keyFetchToken");
+          }
+        }
+        return await currentState.whenKeysReadyDeferred.promise;
+      } catch (err) {
+        return this._fxia._handleTokenError(err);
+      }
+    });
+  }
+
+  /**
+   * Once the user's email is verified, we can request the keys
+   */
+  fetchKeys(keyFetchToken) {
+    let client = this._fxia.fxAccountsClient;
+    log.debug(
+      `Fetching keys with token ${!!keyFetchToken} from ${client.host}`
+    );
+    if (logPII) {
+      log.debug("fetchKeys - the token is " + keyFetchToken);
+    }
+    return client.accountKeys(keyFetchToken);
+  }
+
+  fetchAndUnwrapKeys(keyFetchToken) {
+    return this._fxia.withCurrentAccountState(async currentState => {
+      if (logPII) {
+        log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
+      }
+      // Sign out if we don't have a key fetch token.
+      if (!keyFetchToken) {
+        // this seems really bad and we should remove this - bug 1572313.
+        log.warn("improper fetchAndUnwrapKeys() call: token missing");
+        await this._fxia.signOut();
+        return null;
+      }
+
+      let { wrapKB } = await this.fetchKeys(keyFetchToken);
+
+      let data = await currentState.getUserAccountData();
+
+      // Sanity check that the user hasn't changed out from under us (which
+      // should be impossible given our _withCurrentAccountState, but...)
+      if (data.keyFetchToken !== keyFetchToken) {
+        throw new Error("Signed in user changed while fetching keys!");
+      }
+
+      let kBbytes = CryptoUtils.xor(
+        CommonUtils.hexToBytes(data.unwrapBKey),
+        wrapKB
+      );
+
+      if (logPII) {
+        log.debug("kBbytes: " + kBbytes);
+      }
+      let updateData = {
+        ...(await this._deriveKeys(data.uid, kBbytes)),
+        keyFetchToken: null, // null values cause the item to be removed.
+        unwrapBKey: null,
+      };
+
+      log.debug(
+        "Keys Obtained:" +
+          DERIVED_KEYS_NAMES.map(k => `${k}=${!!updateData[k]}`).join(", ")
+      );
+      if (logPII) {
+        log.debug(
+          "Keys Obtained:" +
+            DERIVED_KEYS_NAMES.map(k => `${k}=${updateData[k]}`).join(", ")
+        );
+      }
+
+      await currentState.updateUserAccountData(updateData);
+      // Some parts of the device registration depend on the Sync keys being available,
+      // so let's re-trigger it now that we have them.
+      await this._fxia.updateDeviceRegistration();
+      data = await currentState.getUserAccountData();
+      return data;
+    });
+  }
+
+  /**
+   * @param {String} scope Single key bearing scope
+   */
+  async getKeyForScope(scope, { keyRotationTimestamp }) {
+    if (scope !== SCOPE_OLD_SYNC) {
+      throw new Error(`Unavailable key material for ${scope}`);
+    }
+    let { kSync, kXCS } = await this.getKeys();
+    if (!kSync || !kXCS) {
+      throw new Error("Could not find requested key.");
+    }
+    kXCS = ChromeUtils.base64URLEncode(CommonUtils.hexToArrayBuffer(kXCS), {
+      pad: false,
+    });
+    kSync = ChromeUtils.base64URLEncode(CommonUtils.hexToArrayBuffer(kSync), {
+      pad: false,
+    });
+    const kid = `${keyRotationTimestamp}-${kXCS}`;
+    return {
+      scope,
+      kid,
+      k: kSync,
+      kty: "oct",
+    };
+  }
+
+  /**
+   * @param {String} scopes Space separated requested scopes
+   */
+  async getScopedKeys(scopes, clientId) {
+    const { sessionToken } = await this._fxia._getVerifiedAccountOrReject();
+    const keyData = await this._fxia.fxAccountsClient.getScopedKeyData(
+      sessionToken,
+      clientId,
+      scopes
+    );
+    const scopedKeys = {};
+    for (const [scope, data] of Object.entries(keyData)) {
+      scopedKeys[scope] = await this.getKeyForScope(scope, data);
+    }
+    return scopedKeys;
+  }
+
+  async _deriveKeys(uid, kBbytes) {
+    return {
+      kSync: CommonUtils.bytesAsHex(await this._deriveSyncKey(kBbytes)),
+      kXCS: CommonUtils.bytesAsHex(this._deriveXClientState(kBbytes)),
+      kExtSync: CommonUtils.bytesAsHex(
+        await this._deriveWebExtSyncStoreKey(kBbytes)
+      ),
+      kExtKbHash: CommonUtils.bytesAsHex(
+        this._deriveWebExtKbHash(uid, kBbytes)
+      ),
+    };
+  }
+
+  /**
+   * Derive the Sync Key given the byte string kB.
+   *
+   * @returns Promise<HKDF(kB, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)>
+   */
+  _deriveSyncKey(kBbytes) {
+    return CryptoUtils.hkdfLegacy(
+      kBbytes,
+      undefined,
+      "identity.mozilla.com/picl/v1/oldsync",
+      2 * 32
+    );
+  }
+
+  /**
+   * Invalidate the FxA certificate, so that it will be refreshed from the server
+   * the next time it is needed.
+   */
+  invalidateCertificate() {
+    return this._fxia.withCurrentAccountState(async state => {
+      await state.updateUserAccountData({ cert: null });
+    });
+  }
+
+  /**
+   * Derive the WebExtensions Sync Storage Key given the byte string kB.
+   *
+   * @returns Promise<HKDF(kB, undefined, "identity.mozilla.com/picl/v1/chrome.storage.sync", 64)>
+   */
+  _deriveWebExtSyncStoreKey(kBbytes) {
+    return CryptoUtils.hkdfLegacy(
+      kBbytes,
+      undefined,
+      "identity.mozilla.com/picl/v1/chrome.storage.sync",
+      2 * 32
+    );
+  }
+
+  /**
+   * Derive the WebExtensions kbHash given the byte string kB.
+   *
+   * @returns SHA256(uid + kB)
+   */
+  _deriveWebExtKbHash(uid, kBbytes) {
+    return this._sha256(uid + kBbytes);
+  }
+
+  /**
+   * Derive the X-Client-State header given the byte string kB.
+   *
+   * @returns SHA256(kB)[:16]
+   */
+  _deriveXClientState(kBbytes) {
+    return this._sha256(kBbytes).slice(0, 16);
+  }
+
+  _sha256(bytes) {
+    let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+      Ci.nsICryptoHash
+    );
+    hasher.init(hasher.SHA256);
+    return CryptoUtils.digestBytes(bytes, hasher);
+  }
+}
+
+var EXPORTED_SYMBOLS = ["FxAccountsKeys"];
--- a/services/fxaccounts/FxAccountsProfile.jsm
+++ b/services/fxaccounts/FxAccountsProfile.jsm
@@ -27,21 +27,21 @@ ChromeUtils.defineModuleGetter(
   "FxAccountsProfileClient",
   "resource://gre/modules/FxAccountsProfileClient.jsm"
 );
 
 var FxAccountsProfile = function(options = {}) {
   this._currentFetchPromise = null;
   this._cachedAt = 0; // when we saved the cached version.
   this._isNotifying = false; // are we sending a notification?
-  this.fxa = options.fxa || fxAccounts;
+  this.fxai = options.fxai || fxAccounts._internal;
   this.client =
     options.profileClient ||
     new FxAccountsProfileClient({
-      fxa: this.fxa,
+      fxai: this.fxai,
       serverURL: options.profileServerUrl,
     });
 
   // An observer to invalidate our _cachedAt optimization. We use a weak-ref
   // just incase this.tearDown isn't called in some cases.
   Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true);
   // for testing
   if (options.channel) {
@@ -60,52 +60,61 @@ this.FxAccountsProfile.prototype = {
     // ignore our "freshness threshold"
     if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) {
       log.debug("FxAccountsProfile observed profile change");
       this._cachedAt = 0;
     }
   },
 
   tearDown() {
-    this.fxa = null;
+    this.fxai = null;
     this.client = null;
     Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION);
   },
 
   _notifyProfileChange(uid) {
     this._isNotifying = true;
     Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
     this._isNotifying = false;
   },
 
   // Cache fetched data and send out a notification so that UI can update.
-  async _cacheProfile(response) {
-    const profile = response.body;
-    const userData = await this.fxa.getSignedInUser();
-    if (profile.uid != userData.uid) {
-      throw new Error(
-        "The fetched profile does not correspond with the current account."
-      );
-    }
-    let profileCache = {
-      profile,
-      etag: response.etag,
-    };
-    await this.fxa.setProfileCache(profileCache);
-    if (profile.email != userData.email) {
-      await this.fxa.handleEmailUpdated(profile.email);
-    }
-    log.debug("notifying profile changed for user ${uid}", userData);
-    this._notifyProfileChange(userData.uid);
-    return profile;
+  _cacheProfile(response) {
+    return this.fxai.withCurrentAccountState(async state => {
+      const profile = response.body;
+      const userData = await state.getUserAccountData();
+      if (profile.uid != userData.uid) {
+        throw new Error(
+          "The fetched profile does not correspond with the current account."
+        );
+      }
+      let profileCache = {
+        profile,
+        etag: response.etag,
+      };
+      await state.updateUserAccountData({ profileCache });
+      if (profile.email != userData.email) {
+        await this.fxai._handleEmailUpdated(profile.email);
+      }
+      log.debug("notifying profile changed for user ${uid}", userData);
+      this._notifyProfileChange(userData.uid);
+      return profile;
+    });
+  },
+
+  async _getProfileCache() {
+    let data = await this.fxai.currentAccountState.getUserAccountData([
+      "profileCache",
+    ]);
+    return data ? data.profileCache : null;
   },
 
   async _fetchAndCacheProfileInternal() {
     try {
-      const profileCache = await this.fxa.getProfileCache();
+      const profileCache = await this._getProfileCache();
       const etag = profileCache ? profileCache.etag : null;
       const response = await this.client.fetchProfile(etag);
 
       // response may be null if the profile was not modified (same ETag).
       if (!response) {
         return null;
       }
       return await this._cacheProfile(response);
@@ -122,17 +131,17 @@ this.FxAccountsProfile.prototype = {
     return this._currentFetchPromise;
   },
 
   // Returns cached data right away if available, otherwise returns null - if
   // it returns null, or if the profile is possibly stale, it attempts to
   // fetch the latest profile data in the background. After data is fetched a
   // notification will be sent out if the profile has changed.
   async getProfile() {
-    const profileCache = await this.fxa.getProfileCache();
+    const profileCache = await this._getProfileCache();
     if (!profileCache) {
       // fetch and cache it in the background.
       this._fetchAndCacheProfile().catch(err => {
         log.error("Background refresh of initial profile failed", err);
       });
       return null;
     }
     if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
--- a/services/fxaccounts/FxAccountsProfileClient.jsm
+++ b/services/fxaccounts/FxAccountsProfileClient.jsm
@@ -47,17 +47,17 @@ XPCOMUtils.defineLazyGlobalGetters(this,
  *   The bearer token to access the profile server
  * @constructor
  */
 var FxAccountsProfileClient = function(options) {
   if (!options || !options.serverURL) {
     throw new Error("Missing 'serverURL' configuration option");
   }
 
-  this.fxa = options.fxa || fxAccounts;
+  this.fxai = options.fxai || fxAccounts._internal;
   // This is a work-around for loop that manages its own oauth tokens.
   // * If |token| is in options we use it and don't attempt any token refresh
   //  on 401. This is for loop.
   // * If |token| doesn't exist we will fetch our own token. This is for the
   //   normal FxAccounts methods for obtaining the profile.
   // We should nuke all |this.token| support once loop moves closer to FxAccounts.
   this.token = options.token;
 
@@ -97,46 +97,46 @@ this.FxAccountsProfileClient.prototype =
    *         Resolves: {body: Object, etag: Object} Successful response from the Profile server.
    *         Rejects: {FxAccountsProfileClientError} Profile client error.
    * @private
    */
   async _createRequest(path, method = "GET", etag = null) {
     let token = this.token;
     if (!token) {
       // tokens are cached, so getting them each request is cheap.
-      token = await this.fxa.getOAuthToken(this.oauthOptions);
+      token = await this.fxai.getOAuthToken(this.oauthOptions);
     }
     try {
       return await this._rawRequest(path, method, token, etag);
     } catch (ex) {
       if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
         throw ex;
       }
       // If this object was instantiated with a token then we don't refresh it.
       if (this.token) {
         throw ex;
       }
       // it's an auth error - assume our token expired and retry.
       log.info(
         "Fetching the profile returned a 401 - revoking our token and retrying"
       );
-      await this.fxa.removeCachedOAuthToken({ token });
-      token = await this.fxa.getOAuthToken(this.oauthOptions);
+      await this.fxai.removeCachedOAuthToken({ token });
+      token = await this.fxai.getOAuthToken(this.oauthOptions);
       // and try with the new token - if that also fails then we fail after
       // revoking the token.
       try {
         return await this._rawRequest(path, method, token, etag);
       } catch (ex) {
         if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
           throw ex;
         }
         log.info(
           "Retry fetching the profile still returned a 401 - revoking our token and failing"
         );
-        await this.fxa.removeCachedOAuthToken({ token });
+        await this.fxai.removeCachedOAuthToken({ token });
         throw ex;
       }
     }
   },
 
   /**
    * Remote "raw" request helper - doesn't handle auth errors and tokens.
    *
--- a/services/fxaccounts/FxAccountsPush.jsm
+++ b/services/fxaccounts/FxAccountsPush.jsm
@@ -46,19 +46,19 @@ FxAccountsPushService.prototype = {
    * Helps only initialize observers once.
    */
   _initialized: false,
   /**
    * Instance of the nsIPushService or a mocked object.
    */
   pushService: null,
   /**
-   * Instance of FxAccounts or a mocked object.
+   * Instance of FxAccountsInternal or a mocked object.
    */
-  fxAccounts: null,
+  fxai: null,
   /**
    * Component ID of this service, helps register this component.
    */
   classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"),
   /**
    * Register used interfaces in this service
    */
   QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
@@ -77,24 +77,24 @@ FxAccountsPushService.prototype = {
     if (options.pushService) {
       this.pushService = options.pushService;
     } else {
       this.pushService = Cc["@mozilla.org/push/Service;1"].getService(
         Ci.nsIPushService
       );
     }
 
-    if (options.fxAccounts) {
-      this.fxAccounts = options.fxAccounts;
+    if (options.fxai) {
+      this.fxai = options.fxai;
     } else {
-      ChromeUtils.defineModuleGetter(
-        this,
-        "fxAccounts",
-        "resource://gre/modules/FxAccounts.jsm"
+      let { fxAccounts } = ChromeUtils.import(
+        "resource://gre/modules/FxAccounts.jsm",
+        {}
       );
+      this.fxai = fxAccounts._internal;
     }
 
     this.asyncObserver = Async.asyncObserver(this, this.log);
     // We use an async observer because a device waking up can
     // observe multiple "Send Tab received" push notifications at the same time.
     // The way these notifications are handled is as follows:
     // Read index from storage, make network request, update the index.
     // You can imagine what happens when multiple calls race: we load
@@ -180,46 +180,46 @@ FxAccountsPushService.prototype = {
    * @private
    * @returns {Promise}
    */
   async _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.log.debug("empty push message - checking account status");
-      this.fxAccounts.checkVerificationStatus();
+      this.fxai.checkVerificationStatus();
       return;
     }
     let payload = message.data.json();
     this.log.debug(`push command: ${payload.command}`);
     switch (payload.command) {
       case ON_COMMAND_RECEIVED_NOTIFICATION:
-        await this.fxAccounts.commands.pollDeviceCommands(payload.data.index);
+        await this.fxai.commands.pollDeviceCommands(payload.data.index);
         break;
       case ON_DEVICE_CONNECTED_NOTIFICATION:
         Services.obs.notifyObservers(
           null,
           ON_DEVICE_CONNECTED_NOTIFICATION,
           payload.data.deviceName
         );
         break;
       case ON_DEVICE_DISCONNECTED_NOTIFICATION:
-        this.fxAccounts.handleDeviceDisconnection(payload.data.id);
+        this.fxai._handleDeviceDisconnection(payload.data.id);
         return;
       case ON_PROFILE_UPDATED_NOTIFICATION:
         // We already have a "profile updated" notification sent via WebChannel,
         // let's just re-use that.
         Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION);
         return;
       case ON_PASSWORD_CHANGED_NOTIFICATION:
       case ON_PASSWORD_RESET_NOTIFICATION:
         this._onPasswordChanged();
         return;
       case ON_ACCOUNT_DESTROYED_NOTIFICATION:
-        this.fxAccounts.handleAccountDestroyed(payload.data.uid);
+        this.fxai._handleAccountDestroyed(payload.data.uid);
         return;
       case ON_COLLECTION_CHANGED_NOTIFICATION:
         Services.obs.notifyObservers(
           null,
           ON_COLLECTION_CHANGED_NOTIFICATION,
           payload.data.collections
         );
         return;
@@ -237,33 +237,38 @@ FxAccountsPushService.prototype = {
   /**
    * 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
    */
-  async _onPasswordChanged() {
-    if (!(await this.fxAccounts.sessionStatus())) {
-      await this.fxAccounts.resetCredentials();
-      Services.obs.notifyObservers(null, ON_ACCOUNT_STATE_CHANGE_NOTIFICATION);
-    }
+  _onPasswordChanged() {
+    return this.fxai.withCurrentAccountState(async state => {
+      if (!(await this.fxai.sessionStatus(state))) {
+        await this.fxai.dropCredentials();
+        Services.obs.notifyObservers(
+          null,
+          ON_ACCOUNT_STATE_CHANGE_NOTIFICATION
+        );
+      }
+    });
   },
   /**
    * 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");
-    return this.fxAccounts.updateDeviceRegistration();
+    return this.fxai.updateDeviceRegistration();
   },
   /**
    * Unsubscribe from the Push server
    *
    * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
    *
    * @returns {Promise}
    * @private
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -454,17 +454,17 @@ this.FxAccountsWebChannelHelpers.prototy
     // Remember who it was so we can log out next time.
     this.setPreviousAccountNameHashPref(accountData.email);
 
     // A sync-specific hack - we want to ensure sync has been initialized
     // before we set the signed-in user.
     let xps = Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
       .wrappedJSObject;
     return xps.whenLoaded().then(() => {
-      return this._fxAccounts.setSignedInUser(accountData);
+      return this._fxAccounts._internal.setSignedInUser(accountData);
     });
   },
 
   /**
    * logout the fxaccounts service
    *
    * @param the uid of the account which have been logged out
    */
@@ -583,25 +583,26 @@ this.FxAccountsWebChannelHelpers.prototy
         name == "uid" ||
         FxAccountsStorageManagerCanStoreField(name)
       ) {
         newCredentials[name] = credentials[name];
       } else {
         log.info("changePassword ignoring unsupported field", name);
       }
     }
-    await this._fxAccounts.updateUserAccountData(newCredentials);
+    await this._fxAccounts._internal.updateUserAccountData(newCredentials);
     // Force the keys derivation, to be able to register a send-tab command
-    // in updateDeviceRegistration.
+    // in updateDeviceRegistration (but it's not clear we really do need to
+    // force keys here - see bug 1580398 for more)
     try {
-      await this._fxAccounts.getKeys();
+      await this._fxAccounts.keys.getKeys();
     } catch (e) {
       log.error("getKeys errored", e);
     }
-    await this._fxAccounts.updateDeviceRegistration();
+    await this._fxAccounts._internal.updateDeviceRegistration();
   },
 
   /**
    * Get the hash of account name of the previously signed in account
    */
   getPreviousAccountNameHashPref() {
     try {
       return Services.prefs.getStringPref(PREF_LAST_FXA_USER);
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -18,16 +18,17 @@ XPCSHELL_TESTS_MANIFESTS += ['tests/xpcs
 EXTRA_JS_MODULES += [
   'Credentials.jsm',
   'FxAccounts.jsm',
   'FxAccountsClient.jsm',
   'FxAccountsCommands.js',
   'FxAccountsCommon.js',
   'FxAccountsConfig.jsm',
   'FxAccountsDevice.jsm',
+  'FxAccountsKeys.jsm',
   'FxAccountsOAuthGrantClient.jsm',
   'FxAccountsPairing.jsm',
   'FxAccountsPairingChannel.js',
   'FxAccountsProfile.jsm',
   'FxAccountsProfileClient.jsm',
   'FxAccountsPush.jsm',
   'FxAccountsStorage.jsm',
   'FxAccountsWebChannel.jsm',
--- a/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html
+++ b/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html
@@ -70,30 +70,30 @@ let wrongEmail = "greta.garbo@gmail.com"
 let rightEmail = "Greta.Garbo@gmail.COM";
 let password = "123456";
 
 function runTest() {
   is(Services.prefs.getCharPref("identity.fxaccounts.auth.uri"), TEST_SERVER,
      "Pref for auth.uri should be set to test server");
 
   let fxa = new MockFxAccounts();
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
 
   is(true, !!fxa, "Couldn't mock fxa");
   is(true, !!client, "Couldn't mock fxa client");
   is(client.host, TEST_SERVER, "Should be using the test auth server uri");
 
   // First try to sign in using the email with the wrong capitalization.  The
   // FxAccountsClient will receive a 400 from the server with the corrected email.
   // It will automatically try to sign in again.  We expect this to succeed.
   client.signIn(wrongEmail, password).then(
     user => {
       // Now store the signed-in user state.  This will include the correct
       // email capitalization.
-      fxa.setSignedInUser(user).then(
+      fxa._internal.setSignedInUser(user).then(
         () => {
           // Confirm that the correct email got stored.
           fxa.getSignedInUser().then(
             data => {
               is(data.email, rightEmail);
               SimpleTest.finish();
             },
             getUserError => {
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -145,106 +145,116 @@ MockFxAccountsClient.prototype = {
 };
 
 /*
  * We need to mock the FxAccounts module's interfaces to external
  * services, such as storage and the FxAccounts client.  We also
  * mock the now() method, so that we can simulate the passing of
  * time and verify that signatures expire correctly.
  */
-function MockFxAccounts() {
-  return new FxAccounts({
+function MockFxAccounts(credentials = null) {
+  let result = new FxAccounts({
     VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms
 
     _getCertificateSigned_calls: [],
     _d_signCertificate: PromiseUtils.defer(),
     _now_is: new Date(),
     now() {
       return this._now_is;
     },
-    newAccountState(credentials) {
+    newAccountState(newCredentials) {
       // we use a real accountState but mocked storage.
       let storage = new MockStorageManager();
-      storage.initialize(credentials);
+      storage.initialize(newCredentials);
       return new AccountState(storage);
     },
     getCertificateSigned(sessionToken, serializedPublicKey) {
       _("mock getCertificateSigned\n");
       this._getCertificateSigned_calls.push([
         sessionToken,
         serializedPublicKey,
       ]);
       return this._d_signCertificate.promise;
     },
     _registerOrUpdateDevice() {
       return Promise.resolve();
     },
     fxAccountsClient: new MockFxAccountsClient(),
     observerPreloads: [],
   });
+  // and for convenience so we don't have to touch as many lines in this test
+  // when we refactored FxAccounts.jsm :)
+  result.setSignedInUser = function(creds) {
+    return result._internal.setSignedInUser(creds);
+  };
+  return result;
 }
 
 /*
  * Some tests want a "real" fxa instance - however, we still mock the storage
  * to keep the tests fast on b2g.
  */
-function MakeFxAccounts(internal = {}) {
+async function MakeFxAccounts({ internal = {}, credentials } = {}) {
   if (!internal.newAccountState) {
     // we use a real accountState but mocked storage.
-    internal.newAccountState = function(credentials) {
+    internal.newAccountState = function(newCredentials) {
       let storage = new MockStorageManager();
-      storage.initialize(credentials);
+      storage.initialize(newCredentials);
       return new AccountState(storage);
     };
   }
   if (!internal._signOutServer) {
     internal._signOutServer = () => Promise.resolve();
   }
   if (!internal._registerOrUpdateDevice) {
     internal._registerOrUpdateDevice = () => Promise.resolve();
   }
   if (!internal.observerPreloads) {
     internal.observerPreloads = [];
   }
-  return new FxAccounts(internal);
+  let result = new FxAccounts(internal);
+
+  if (credentials) {
+    await result._internal.setSignedInUser(credentials);
+  }
+  return result;
 }
 
 add_task(async function test_get_signed_in_user_initially_unset() {
   _("Check getSignedInUser initially and after signout reports no user");
-  let account = MakeFxAccounts();
+  let account = await MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
     kSync: "beef",
     kXCS: "cafe",
     kExtSync: "bacon",
     kExtKbHash: "cheese",
     verified: true,
   };
   let result = await account.getSignedInUser();
   Assert.equal(result, null);
 
-  await account.setSignedInUser(credentials);
+  await account._internal.setSignedInUser(credentials);
   let histogram = Services.telemetry.getHistogramById("FXA_CONFIGURED");
   Assert.equal(histogram.snapshot().sum, 1);
   histogram.clear();
 
   result = await account.getSignedInUser();
   Assert.equal(result.email, credentials.email);
   Assert.equal(result.assertion, credentials.assertion);
   Assert.equal(result.kSync, credentials.kSync);
   Assert.equal(result.kXCS, credentials.kXCS);
   Assert.equal(result.kExtSync, credentials.kExtSync);
   Assert.equal(result.kExtKbHash, credentials.kExtKbHash);
 
   // Delete the memory cache and force the user
   // to be read and parsed from storage (e.g. disk via JSONStorage).
-  delete account.internal.signedInUser;
   result = await account.getSignedInUser();
   Assert.equal(result.email, credentials.email);
   Assert.equal(result.assertion, credentials.assertion);
   Assert.equal(result.kSync, credentials.kSync);
   Assert.equal(result.kXCS, credentials.kXCS);
   Assert.equal(result.kExtSync, credentials.kExtSync);
   Assert.equal(result.kExtKbHash, credentials.kExtKbHash);
 
@@ -254,232 +264,229 @@ add_task(async function test_get_signed_
 
   // user should be undefined after sign out
   result = await account.getSignedInUser();
   Assert.equal(result, null);
 });
 
 add_task(async function test_set_signed_in_user_signs_out_previous_account() {
   _("Check setSignedInUser signs out the previous account.");
-  let account = MakeFxAccounts();
   let signOutServerCalled = false;
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
     kSync: "beef",
     kXCS: "cafe",
     kExtSync: "bacon",
     kExtKbHash: "cheese",
     verified: true,
   };
-  await account.setSignedInUser(credentials);
+  let account = await MakeFxAccounts({ credentials });
 
-  account.internal._signOutServer = () => {
+  account._internal._signOutServer = () => {
     signOutServerCalled = true;
     return Promise.resolve(true);
   };
 
-  await account.setSignedInUser(credentials);
+  await account._internal.setSignedInUser(credentials);
   Assert.ok(signOutServerCalled);
 });
 
 add_task(async function test_update_account_data() {
   _("Check updateUserAccountData does the right thing.");
-  let account = MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
     kSync: "beef",
     kXCS: "cafe",
     kExtSync: "bacon",
     kExtKbHash: "cheese",
     verified: true,
   };
-  await account.setSignedInUser(credentials);
+  let account = await MakeFxAccounts({ credentials });
 
   let newCreds = {
     email: credentials.email,
     uid: credentials.uid,
     assertion: "new_assertion",
   };
-  await account.updateUserAccountData(newCreds);
+  await account._internal.updateUserAccountData(newCreds);
   Assert.equal(
     (await account.getSignedInUser()).assertion,
     "new_assertion",
     "new field value was saved"
   );
 
   // but we should fail attempting to change the uid.
   newCreds = {
     email: credentials.email,
     uid: "another_uid",
     assertion: "new_assertion",
   };
   await Assert.rejects(
-    account.updateUserAccountData(newCreds),
+    account._internal.updateUserAccountData(newCreds),
     /The specified credentials aren't for the current user/
   );
 
   // should fail without the uid.
   newCreds = {
     assertion: "new_assertion",
   };
   await Assert.rejects(
-    account.updateUserAccountData(newCreds),
+    account._internal.updateUserAccountData(newCreds),
     /The specified credentials aren't for the current user/
   );
 
   // and should fail with a field name that's not known by storage.
   newCreds = {
     email: credentials.email,
     uid: "another_uid",
     foo: "bar",
   };
   await Assert.rejects(
-    account.updateUserAccountData(newCreds),
+    account._internal.updateUserAccountData(newCreds),
     /The specified credentials aren't for the current user/
   );
 });
 
 add_task(async function test_getCertificateOffline() {
   _("getCertificateOffline()");
-  let fxa = MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     sessionToken: "dead",
     verified: true,
   };
-
-  await fxa.setSignedInUser(credentials);
-
+  let fxa = await MakeFxAccounts({ credentials });
   // Test that an expired cert throws if we're offline.
   let offline = Services.io.offline;
   Services.io.offline = true;
-  await fxa.internal
-    .getKeypairAndCertificate(fxa.internal.currentAccountState)
+  await fxa._internal
+    .getKeypairAndCertificate(fxa._internal.currentAccountState)
     .then(
       result => {
         Services.io.offline = offline;
         do_throw("Unexpected success");
       },
       err => {
         Services.io.offline = offline;
         // ... so we have to check the error string.
         Assert.equal(err, "Error: OFFLINE");
       }
     );
   await fxa.signOut(/* localOnly = */ true);
 });
 
 add_task(async function test_getCertificateCached() {
   _("getCertificateCached()");
-  let fxa = MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     sessionToken: "dead",
     verified: true,
     // A cached keypair and cert that remain valid.
     keyPair: {
       validUntil: Date.now() + KEY_LIFETIME + 10000,
       rawKeyPair: "good-keypair",
     },
     cert: {
       validUntil: Date.now() + CERT_LIFETIME + 10000,
       rawCert: "good-cert",
     },
   };
+  let fxa = await MakeFxAccounts({ credentials });
 
-  await fxa.setSignedInUser(credentials);
-  let { keyPair, certificate } = await fxa.internal.getKeypairAndCertificate(
-    fxa.internal.currentAccountState
+  let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
+    fxa._internal.currentAccountState
   );
   // should have the same keypair and cert.
   Assert.equal(keyPair, credentials.keyPair.rawKeyPair);
   Assert.equal(certificate, credentials.cert.rawCert);
   await fxa.signOut(/* localOnly = */ true);
 });
 
 add_task(async function test_getCertificateExpiredCert() {
   _("getCertificateExpiredCert()");
-  let fxa = MakeFxAccounts({
-    getCertificateSigned() {
-      return "new cert";
-    },
-  });
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     sessionToken: "dead",
     verified: true,
     // A cached keypair that remains valid.
     keyPair: {
       validUntil: Date.now() + KEY_LIFETIME + 10000,
       rawKeyPair: "good-keypair",
     },
     // A cached certificate which has expired.
     cert: {
       validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT"),
       rawCert: "expired-cert",
     },
   };
-  await fxa.setSignedInUser(credentials);
-  let { keyPair, certificate } = await fxa.internal.getKeypairAndCertificate(
-    fxa.internal.currentAccountState
+  let fxa = await MakeFxAccounts({
+    internal: {
+      getCertificateSigned() {
+        return "new cert";
+      },
+    },
+    credentials,
+  });
+  let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
+    fxa._internal.currentAccountState
   );
   // should have the same keypair but a new cert.
   Assert.equal(keyPair, credentials.keyPair.rawKeyPair);
   Assert.notEqual(certificate, credentials.cert.rawCert);
   await fxa.signOut(/* localOnly = */ true);
 });
 
 add_task(async function test_getCertificateExpiredKeypair() {
   _("getCertificateExpiredKeypair()");
-  let fxa = MakeFxAccounts({
-    getCertificateSigned() {
-      return "new cert";
-    },
-  });
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     sessionToken: "dead",
     verified: true,
     // A cached keypair that has expired.
     keyPair: {
       validUntil: Date.now() - 1000,
       rawKeyPair: "expired-keypair",
     },
     // A cached certificate which remains valid.
     cert: {
       validUntil: Date.now() + CERT_LIFETIME + 10000,
       rawCert: "expired-cert",
     },
   };
-
-  await fxa.setSignedInUser(credentials);
-  let { keyPair, certificate } = await fxa.internal.getKeypairAndCertificate(
-    fxa.internal.currentAccountState
+  let fxa = await MakeFxAccounts({
+    internal: {
+      getCertificateSigned() {
+        return "new cert";
+      },
+    },
+    credentials,
+  });
+  let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
+    fxa._internal.currentAccountState
   );
   // even though the cert was valid, the fact the keypair was not means we
   // should have fetched both.
   Assert.notEqual(keyPair, credentials.keyPair.rawKeyPair);
   Assert.notEqual(certificate, credentials.cert.rawCert);
   await fxa.signOut(/* localOnly = */ true);
 });
 
 // Sanity-check that our mocked client is working correctly
 add_test(function test_client_mock() {
   let fxa = new MockFxAccounts();
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
   Assert.equal(client._verified, false);
   Assert.equal(typeof client.signIn, "function");
 
   // The recoveryEmailStatus function eventually fulfills its promise
   client.recoveryEmailStatus().then(response => {
     Assert.equal(response.verified, false);
     run_next_test();
   });
@@ -492,38 +499,38 @@ add_test(function test_client_mock() {
 add_test(function test_verification_poll() {
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("francine");
   let login_notification_received = false;
 
   makeObserver(ONVERIFIED_NOTIFICATION, function() {
     log.debug("test_verification_poll observed onverified");
     // Once email verification is complete, we will observe onverified
-    fxa.internal.getUserAccountData().then(user => {
+    fxa._internal.getUserAccountData().then(user => {
       // And confirm that the user's state has changed
       Assert.equal(user.verified, true);
       Assert.equal(user.email, test_user.email);
       Assert.ok(login_notification_received);
       run_next_test();
     });
   });
 
   makeObserver(ONLOGIN_NOTIFICATION, function() {
     log.debug("test_verification_poll observer onlogin");
     login_notification_received = true;
   });
 
   fxa.setSignedInUser(test_user).then(() => {
-    fxa.internal.getUserAccountData().then(user => {
+    fxa._internal.getUserAccountData().then(user => {
       // The user is signing in, but email has not been verified yet
       Assert.equal(user.verified, false);
       do_timeout(200, function() {
         log.debug("Mocking verification of francine's email");
-        fxa.internal.fxAccountsClient._email = test_user.email;
-        fxa.internal.fxAccountsClient._verified = true;
+        fxa._internal.fxAccountsClient._email = test_user.email;
+        fxa._internal.fxAccountsClient._verified = true;
       });
     });
   });
 });
 
 // Sign in the user, but never verify the email.  The check-email
 // poll should time out.  No verifiedlogin event should be observed, and the
 // internal whenVerified promise should be rejected
@@ -535,19 +542,19 @@ add_test(function test_polling_timeout()
 
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("carol");
 
   let removeObserver = makeObserver(ONVERIFIED_NOTIFICATION, function() {
     do_throw("We should not be getting a login event!");
   });
 
-  fxa.internal.POLL_SESSION = 1;
+  fxa._internal.POLL_SESSION = 1;
 
-  let p = fxa.internal.whenVerified({});
+  let p = fxa._internal.whenVerified({});
 
   fxa.setSignedInUser(test_user).then(() => {
     p.then(
       success => {
         do_throw("this should not succeed");
       },
       fail => {
         removeObserver();
@@ -556,132 +563,132 @@ add_test(function test_polling_timeout()
     );
   });
 });
 
 add_test(function test_pollEmailStatus_start_verified() {
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("carol");
 
-  fxa.internal.POLL_SESSION = 20 * 60000;
-  fxa.internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 50000;
+  fxa._internal.POLL_SESSION = 20 * 60000;
+  fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 50000;
 
   fxa.setSignedInUser(test_user).then(() => {
-    fxa.internal.getUserAccountData().then(user => {
-      fxa.internal.fxAccountsClient._email = test_user.email;
-      fxa.internal.fxAccountsClient._verified = true;
-      const mock = sinon.mock(fxa.internal);
+    fxa._internal.getUserAccountData().then(user => {
+      fxa._internal.fxAccountsClient._email = test_user.email;
+      fxa._internal.fxAccountsClient._verified = true;
+      const mock = sinon.mock(fxa._internal);
       mock.expects("_scheduleNextPollEmailStatus").never();
-      fxa.internal
+      fxa._internal
         .startPollEmailStatus(
-          fxa.internal.currentAccountState,
+          fxa._internal.currentAccountState,
           user.sessionToken,
           "start"
         )
         .then(() => {
           mock.verify();
           mock.restore();
           run_next_test();
         });
     });
   });
 });
 
 add_test(function test_pollEmailStatus_start() {
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("carol");
 
-  fxa.internal.POLL_SESSION = 20 * 60000;
-  fxa.internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 123456;
+  fxa._internal.POLL_SESSION = 20 * 60000;
+  fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 123456;
 
   fxa.setSignedInUser(test_user).then(() => {
-    fxa.internal.getUserAccountData().then(user => {
-      const mock = sinon.mock(fxa.internal);
+    fxa._internal.getUserAccountData().then(user => {
+      const mock = sinon.mock(fxa._internal);
       mock
         .expects("_scheduleNextPollEmailStatus")
         .once()
         .withArgs(
-          fxa.internal.currentAccountState,
+          fxa._internal.currentAccountState,
           user.sessionToken,
           123456,
           "start"
         );
-      fxa.internal
+      fxa._internal
         .startPollEmailStatus(
-          fxa.internal.currentAccountState,
+          fxa._internal.currentAccountState,
           user.sessionToken,
           "start"
         )
         .then(() => {
           mock.verify();
           mock.restore();
           run_next_test();
         });
     });
   });
 });
 
 add_test(function test_pollEmailStatus_start_subsequent() {
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("carol");
 
-  fxa.internal.POLL_SESSION = 20 * 60000;
-  fxa.internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 123456;
-  fxa.internal.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT = 654321;
-  fxa.internal.VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD = -1;
+  fxa._internal.POLL_SESSION = 20 * 60000;
+  fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 123456;
+  fxa._internal.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT = 654321;
+  fxa._internal.VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD = -1;
 
   fxa.setSignedInUser(test_user).then(() => {
-    fxa.internal.getUserAccountData().then(user => {
-      const mock = sinon.mock(fxa.internal);
+    fxa._internal.getUserAccountData().then(user => {
+      const mock = sinon.mock(fxa._internal);
       mock
         .expects("_scheduleNextPollEmailStatus")
         .once()
         .withArgs(
-          fxa.internal.currentAccountState,
+          fxa._internal.currentAccountState,
           user.sessionToken,
           654321,
           "start"
         );
-      fxa.internal
+      fxa._internal
         .startPollEmailStatus(
-          fxa.internal.currentAccountState,
+          fxa._internal.currentAccountState,
           user.sessionToken,
           "start"
         )
         .then(() => {
           mock.verify();
           mock.restore();
           run_next_test();
         });
     });
   });
 });
 
 add_test(function test_pollEmailStatus_browser_startup() {
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("carol");
 
-  fxa.internal.POLL_SESSION = 20 * 60000;
-  fxa.internal.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT = 654321;
+  fxa._internal.POLL_SESSION = 20 * 60000;
+  fxa._internal.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT = 654321;
 
   fxa.setSignedInUser(test_user).then(() => {
-    fxa.internal.getUserAccountData().then(user => {
-      const mock = sinon.mock(fxa.internal);
+    fxa._internal.getUserAccountData().then(user => {
+      const mock = sinon.mock(fxa._internal);
       mock
         .expects("_scheduleNextPollEmailStatus")
         .once()
         .withArgs(
-          fxa.internal.currentAccountState,
+          fxa._internal.currentAccountState,
           user.sessionToken,
           654321,
           "browser-startup"
         );
-      fxa.internal
+      fxa._internal
         .startPollEmailStatus(
-          fxa.internal.currentAccountState,
+          fxa._internal.currentAccountState,
           user.sessionToken,
           "browser-startup"
         )
         .then(() => {
           mock.verify();
           mock.restore();
           run_next_test();
         });
@@ -689,22 +696,22 @@ add_test(function test_pollEmailStatus_b
   });
 });
 
 add_test(function test_pollEmailStatus_push() {
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("carol");
 
   fxa.setSignedInUser(test_user).then(() => {
-    fxa.internal.getUserAccountData().then(user => {
-      const mock = sinon.mock(fxa.internal);
+    fxa._internal.getUserAccountData().then(user => {
+      const mock = sinon.mock(fxa._internal);
       mock.expects("_scheduleNextPollEmailStatus").never();
-      fxa.internal
+      fxa._internal
         .startPollEmailStatus(
-          fxa.internal.currentAccountState,
+          fxa._internal.currentAccountState,
           user.sessionToken,
           "push"
         )
         .then(() => {
           mock.verify();
           mock.restore();
           run_next_test();
         });
@@ -725,20 +732,20 @@ add_test(function test_getKeys() {
       Assert.equal(!!user2.kSync, false);
       Assert.equal(!!user2.kXCS, false);
       Assert.equal(!!user2.kExtSync, false);
       Assert.equal(!!user2.kExtKbHash, false);
       // And we still have a key-fetch token and unwrapBKey to use
       Assert.equal(!!user2.keyFetchToken, true);
       Assert.equal(!!user2.unwrapBKey, true);
 
-      fxa.internal.getKeys().then(() => {
+      fxa.keys.getKeys().then(() => {
         fxa.getSignedInUser().then(user3 => {
           // Now we should have keys
-          Assert.equal(fxa.internal.isUserEmailVerified(user3), true);
+          Assert.equal(fxa._internal.isUserEmailVerified(user3), true);
           Assert.equal(!!user3.verified, true);
           Assert.notEqual(null, user3.kSync);
           Assert.notEqual(null, user3.kXCS);
           Assert.notEqual(null, user3.kExtSync);
           Assert.notEqual(null, user3.kExtKbHash);
           Assert.equal(user3.keyFetchToken, undefined);
           Assert.equal(user3.unwrapBKey, undefined);
           run_next_test();
@@ -753,17 +760,17 @@ add_task(async function test_getKeys_kb_
   let user = getTestUser("eusebius");
 
   user.verified = true;
   // Set-up the deprecated set of keys.
   user.kA = "e0245ab7f10e483470388e0a28f0a03379a3b417174fb2b42feab158b4ac2dbd";
   user.kB = "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9";
 
   await fxa.setSignedInUser(user);
-  await fxa.internal.getKeys();
+  await fxa.keys.getKeys();
   let newUser = await fxa.getSignedInUser();
   Assert.equal(newUser.kA, null);
   Assert.equal(newUser.kB, null);
   Assert.equal(
     newUser.kSync,
     "0d6fe59791b05fa489e463ea25502e3143f6b7a903aa152e95cd9c6eddbac5b4" +
       "dc68a19097ef65dbd147010ee45222444e66b8b3d7c8a441ebb7dd3dce015a9e"
   );
@@ -778,17 +785,17 @@ add_task(async function test_getKeys_kb_
     "25ed0ab3ae2f1e5365d923c9402d4255770dbe6ce79b09ed49f516985c0aa0c1"
   );
 });
 
 add_task(async function test_getKeys_nonexistent_account() {
   let fxa = new MockFxAccounts();
   let bismarck = getTestUser("bismarck");
 
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
   client.accountStatus = () => Promise.resolve(false);
   client.accountKeys = () => {
     return Promise.reject({
       code: 401,
       errno: ERRNO_INVALID_AUTH_TOKEN,
     });
   };
 
@@ -796,65 +803,60 @@ add_task(async function test_getKeys_non
 
   let promiseLogout = new Promise(resolve => {
     makeObserver(ONLOGOUT_NOTIFICATION, function() {
       log.debug("test_getKeys_nonexistent_account observed logout");
       resolve();
     });
   });
 
-  try {
-    await fxa.internal.getKeys();
-    Assert.ok(false);
-  } catch (err) {
-    Assert.equal(err.code, 401);
-    Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
-  }
+  // XXX - the exception message here isn't ideal, but doesn't really matter...
+  await Assert.rejects(fxa.keys.getKeys(), /A different user signed in/);
 
   await promiseLogout;
 
-  let user = await fxa.internal.getUserAccountData();
+  let user = await fxa._internal.getUserAccountData();
   Assert.equal(user, null);
 });
 
 // getKeys with invalid keyFetchToken should delete keyFetchToken from storage
 add_task(async function test_getKeys_invalid_token() {
   let fxa = new MockFxAccounts();
   let yusuf = getTestUser("yusuf");
 
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
   client.accountStatus = () => Promise.resolve(true);
   client.accountKeys = () => {
     return Promise.reject({
       code: 401,
       errno: ERRNO_INVALID_AUTH_TOKEN,
     });
   };
 
   await fxa.setSignedInUser(yusuf);
 
   try {
-    await fxa.internal.getKeys();
+    await fxa.keys.getKeys();
     Assert.ok(false);
   } catch (err) {
     Assert.equal(err.code, 401);
     Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
   }
 
-  let user = await fxa.internal.getUserAccountData();
+  let user = await fxa._internal.getUserAccountData();
   Assert.equal(user.email, yusuf.email);
   Assert.equal(user.keyFetchToken, null);
-  await fxa.internal.abortExistingFlow();
+  await fxa._internal.abortExistingFlow();
 });
 
 // This is the exact same test vectors as
 // https://github.com/mozilla/fxa-crypto-relier/blob/f94f441159029a645a474d4b6439c38308da0bb0/test/deriver/ScopedKeys.js#L58
 add_task(async function test_getScopedKeys_oldsync() {
   let fxa = new MockFxAccounts();
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
   client.getScopedKeyData = () =>
     Promise.resolve({
       "https://identity.mozilla.com/apps/oldsync": {
         identifier: "https://identity.mozilla.com/apps/oldsync",
         keyRotationSecret:
           "0000000000000000000000000000000000000000000000000000000000000000",
         keyRotationTimestamp: 1510726317123,
       },
@@ -867,34 +869,34 @@ add_task(async function test_getScopedKe
       "0d6fe59791b05fa489e463ea25502e3143f6b7a903aa152e95cd9c6eddbac5b4dc68a19097ef65dbd147010ee45222444e66b8b3d7c8a441ebb7dd3dce015a9e",
     kXCS: "22a42fe289dced5715135913424cb23b",
     kExtSync:
       "baded53eb3587d7900e604e8a68d860abf9de30b5c955d3c4d5dba63f26fd88265cd85923f6e9dcd16aef3b82bc88039a89c59ecd9e88de09a7418c7d94f90c9",
     kExtKbHash:
       "b776a89db29f22daedd154b44ff88397d0b210223fb956f5a749521dd8de8ddf",
   };
   await fxa.setSignedInUser(user);
-  const keys = await fxa.internal.getScopedKeys(
+  const keys = await fxa.keys.getScopedKeys(
     `${SCOPE_OLD_SYNC} profile`,
     "123456789a"
   );
   Assert.deepEqual(keys, {
     [SCOPE_OLD_SYNC]: {
       scope: SCOPE_OLD_SYNC,
       kid: "1510726317123-IqQv4onc7VcVE1kTQkyyOw",
       k:
         "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
       kty: "oct",
     },
   });
 });
 
 add_task(async function test_getScopedKeys_unavailable_key() {
   let fxa = new MockFxAccounts();
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
   client.getScopedKeyData = () =>
     Promise.resolve({
       "https://identity.mozilla.com/apps/oldsync": {
         identifier: "https://identity.mozilla.com/apps/oldsync",
         keyRotationSecret:
           "0000000000000000000000000000000000000000000000000000000000000000",
         keyRotationTimestamp: 1510726317123,
       },
@@ -914,41 +916,42 @@ add_task(async function test_getScopedKe
     kXCS: "22a42fe289dced5715135913424cb23b",
     kExtSync:
       "baded53eb3587d7900e604e8a68d860abf9de30b5c955d3c4d5dba63f26fd88265cd85923f6e9dcd16aef3b82bc88039a89c59ecd9e88de09a7418c7d94f90c9",
     kExtKbHash:
       "b776a89db29f22daedd154b44ff88397d0b210223fb956f5a749521dd8de8ddf",
   };
   await fxa.setSignedInUser(user);
   await Assert.rejects(
-    fxa.internal.getScopedKeys(
+    fxa.keys.getScopedKeys(
       `${SCOPE_OLD_SYNC} otherkeybearingscope profile`,
       "123456789a"
     ),
     /Unavailable key material for otherkeybearingscope/
   );
 });
 
-//  fetchAndUnwrapKeys with no keyFetchToken should trigger signOut
+// fetchAndUnwrapKeys with no keyFetchToken should trigger signOut
+// XXX - actually, it probably shouldn't - bug 1572313.
 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");
-    fxa.internal.getUserAccountData().then(user2 => {
-      fxa.internal.abortExistingFlow().then(run_next_test);
+    fxa._internal.getUserAccountData().then(user2 => {
+      fxa._internal.abortExistingFlow().then(run_next_test);
     });
   });
 
   fxa
     .setSignedInUser(user)
     .then(user2 => {
-      return fxa.internal.fetchAndUnwrapKeys();
+      return fxa.keys.fetchAndUnwrapKeys();
     })
     .catch(error => {
       log.info("setSignedInUser correctly rejected");
     });
 });
 
 // Alice (User A) signs up but never verifies her email.  Then Bob (User B)
 // signs in with a verified email.  Ensure that no sign-in events are triggered
@@ -956,48 +959,48 @@ add_test(function test_fetchAndUnwrapKey
 add_test(function test_overlapping_signins() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   let bob = getTestUser("bob");
 
   makeObserver(ONVERIFIED_NOTIFICATION, function() {
     log.debug("test_overlapping_signins observed onverified");
     // Once email verification is complete, we will observe onverified
-    fxa.internal.getUserAccountData().then(user => {
+    fxa._internal.getUserAccountData().then(user => {
       Assert.equal(user.email, bob.email);
       Assert.equal(user.verified, true);
       run_next_test();
     });
   });
 
   // Alice is the user signing in; her email is unverified.
   fxa.setSignedInUser(alice).then(() => {
     log.debug("Alice signing in ...");
-    fxa.internal.getUserAccountData().then(user => {
+    fxa._internal.getUserAccountData().then(user => {
       Assert.equal(user.email, alice.email);
       Assert.equal(user.verified, false);
       log.debug("Alice has not verified her email ...");
 
       // Now Bob signs in instead and actually verifies his email
       log.debug("Bob signing in ...");
       fxa.setSignedInUser(bob).then(() => {
         do_timeout(200, function() {
           // Mock email verification ...
           log.debug("Bob verifying his email ...");
-          fxa.internal.fxAccountsClient._verified = true;
+          fxa._internal.fxAccountsClient._verified = true;
         });
       });
     });
   });
 });
 
 add_task(async function test_getAssertion_invalid_token() {
   let fxa = new MockFxAccounts();
 
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
   client.accountStatus = () => Promise.resolve(true);
 
   let creds = {
     sessionToken: "sessionToken",
     kSync: expandHex("11"),
     kXCS: expandHex("66"),
     kExtSync: expandHex("88"),
     kExtKbHash: expandHex("22"),
@@ -1005,29 +1008,29 @@ add_task(async function test_getAssertio
     email: "sonia@example.com",
   };
   await fxa.setSignedInUser(creds);
   // we have what we still believe to be a valid session token, so we should
   // consider that we have a local session.
   Assert.ok(await fxa.hasLocalSession());
 
   try {
-    let promiseAssertion = fxa.getAssertion("audience.example.com");
-    fxa.internal._d_signCertificate.reject({
+    let promiseAssertion = fxa._internal.getAssertion("audience.example.com");
+    fxa._internal._d_signCertificate.reject({
       code: 401,
       errno: ERRNO_INVALID_AUTH_TOKEN,
     });
     await promiseAssertion;
     Assert.ok(false, "getAssertion should reject invalid session token");
   } catch (err) {
     Assert.equal(err.code, 401);
     Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
   }
 
-  let user = await fxa.internal.getUserAccountData();
+  let user = await fxa._internal.getUserAccountData();
   Assert.equal(user.email, creds.email);
   Assert.equal(user.sessionToken, null);
   Assert.ok(!(await fxa.hasLocalSession()));
 });
 
 add_task(async function test_getAssertion() {
   let fxa = new MockFxAccounts();
 
@@ -1044,25 +1047,25 @@ add_task(async function test_getAssertio
   await fxa.setSignedInUser(creds);
 
   _("== ready to go\n");
   // Start with a nice arbitrary but realistic date.  Here we use a nice RFC
   // 1123 date string like we would get from an HTTP header. Over the course of
   // the test, we will update 'now', but leave 'start' where it is.
   let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
   let start = now;
-  fxa.internal._now_is = now;
+  fxa._internal._now_is = now;
 
-  let d = fxa.getAssertion("audience.example.com");
+  let d = fxa._internal.getAssertion("audience.example.com");
   // At this point, a thread has been spawned to generate the keys.
   _("-- back from fxa.getAssertion\n");
-  fxa.internal._d_signCertificate.resolve("cert1");
+  fxa._internal._d_signCertificate.resolve("cert1");
   let assertion = await d;
-  Assert.equal(fxa.internal._getCertificateSigned_calls.length, 1);
-  Assert.equal(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken");
+  Assert.equal(fxa._internal._getCertificateSigned_calls.length, 1);
+  Assert.equal(fxa._internal._getCertificateSigned_calls[0][0], "sessionToken");
   Assert.notEqual(assertion, null);
   _("ASSERTION: " + assertion + "\n");
   let pieces = assertion.split("~");
   Assert.equal(pieces[0], "cert1");
   let userData = await fxa.getSignedInUser();
   let keyPair = userData.keyPair;
   let cert = userData.cert;
   Assert.notEqual(keyPair, undefined);
@@ -1077,32 +1080,32 @@ add_task(async function test_getAssertio
   Assert.equal(keyPair.validUntil, start + KEY_LIFETIME);
   Assert.equal(cert.validUntil, start + CERT_LIFETIME);
   _("delta: " + Date.parse(payload.exp - start) + "\n");
   let exp = Number(payload.exp);
 
   Assert.equal(exp, now + ASSERTION_LIFETIME);
 
   // Reset for next call.
-  fxa.internal._d_signCertificate = PromiseUtils.defer();
+  fxa._internal._d_signCertificate = PromiseUtils.defer();
 
   // Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for
   // a new audience, should not provoke key generation or a signing request.
-  assertion = await fxa.getAssertion("other.example.com");
+  assertion = await fxa._internal.getAssertion("other.example.com");
 
   // There were no additional calls - same number of getcert calls as before
-  Assert.equal(fxa.internal._getCertificateSigned_calls.length, 1);
+  Assert.equal(fxa._internal._getCertificateSigned_calls.length, 1);
 
   // Wait an hour; assertion use period expires, but not the certificate
   now += ONE_HOUR_MS;
-  fxa.internal._now_is = now;
+  fxa._internal._now_is = now;
 
   // This won't block on anything - will make an assertion, but not get a
   // new certificate.
-  assertion = await fxa.getAssertion("third.example.com");
+  assertion = await fxa._internal.getAssertion("third.example.com");
 
   // Test will time out if that failed (i.e., if that had to go get a new cert)
   pieces = assertion.split("~");
   Assert.equal(pieces[0], "cert1");
   p2 = pieces[1].split(".");
   header = JSON.parse(atob(p2[0]));
   payload = JSON.parse(atob(p2[1]));
   Assert.equal(payload.aud, "third.example.com");
@@ -1118,22 +1121,22 @@ add_task(async function test_getAssertio
   Assert.equal(keyPair.validUntil, start + KEY_LIFETIME);
   Assert.equal(cert.validUntil, start + CERT_LIFETIME);
   exp = Number(payload.exp);
   Assert.equal(exp, now + ASSERTION_LIFETIME);
 
   // Now we wait even longer, and expect both assertion and cert to expire.  So
   // we will have to get a new keypair and cert.
   now += ONE_DAY_MS;
-  fxa.internal._now_is = now;
-  d = fxa.getAssertion("fourth.example.com");
-  fxa.internal._d_signCertificate.resolve("cert2");
+  fxa._internal._now_is = now;
+  d = fxa._internal.getAssertion("fourth.example.com");
+  fxa._internal._d_signCertificate.resolve("cert2");
   assertion = await d;
-  Assert.equal(fxa.internal._getCertificateSigned_calls.length, 2);
-  Assert.equal(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken");
+  Assert.equal(fxa._internal._getCertificateSigned_calls.length, 2);
+  Assert.equal(fxa._internal._getCertificateSigned_calls[1][0], "sessionToken");
   pieces = assertion.split("~");
   Assert.equal(pieces[0], "cert2");
   p2 = pieces[1].split(".");
   header = JSON.parse(atob(p2[0]));
   payload = JSON.parse(atob(p2[1]));
   Assert.equal(payload.aud, "fourth.example.com");
   userData = await fxa.getSignedInUser();
   keyPair = userData.keyPair;
@@ -1171,112 +1174,112 @@ add_test(function test_accountStatus() {
     .then(result => {
       Assert.ok(!result);
     })
     .then(() => {
       fxa.setSignedInUser(alice).then(() => {
         fxa.accountStatus().then(result => {
           // FxAccounts.accountStatus() should match Client.accountStatus()
           Assert.ok(result);
-          fxa.internal.fxAccountsClient._deletedOnServer = true;
+          fxa._internal.fxAccountsClient._deletedOnServer = true;
           fxa.accountStatus().then(result2 => {
             Assert.ok(!result2);
-            fxa.internal.fxAccountsClient._deletedOnServer = false;
+            fxa._internal.fxAccountsClient._deletedOnServer = false;
             fxa.signOut().then(run_next_test);
           });
         });
       });
     });
 });
 
 add_task(async function test_resend_email_invalid_token() {
   let fxa = new MockFxAccounts();
   let sophia = getTestUser("sophia");
   Assert.notEqual(sophia.sessionToken, null);
 
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
   client.resendVerificationEmail = () => {
     return Promise.reject({
       code: 401,
       errno: ERRNO_INVALID_AUTH_TOKEN,
     });
   };
   client.accountStatus = () => Promise.resolve(true);
 
   await fxa.setSignedInUser(sophia);
-  let user = await fxa.internal.getUserAccountData();
+  let user = await fxa._internal.getUserAccountData();
   Assert.equal(user.email, sophia.email);
   Assert.equal(user.verified, false);
   log.debug("Sophia wants verification email resent");
 
   try {
     await fxa.resendVerificationEmail();
     Assert.ok(
       false,
       "resendVerificationEmail should reject invalid session token"
     );
   } catch (err) {
     Assert.equal(err.code, 401);
     Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
   }
 
-  user = await fxa.internal.getUserAccountData();
+  user = await fxa._internal.getUserAccountData();
   Assert.equal(user.email, sophia.email);
   Assert.equal(user.sessionToken, null);
-  await fxa.internal.abortExistingFlow();
+  await fxa._internal.abortExistingFlow();
 });
 
 add_test(function test_resend_email() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
 
-  let initialState = fxa.internal.currentAccountState;
+  let initialState = fxa._internal.currentAccountState;
 
   // Alice is the user signing in; her email is unverified.
   fxa.setSignedInUser(alice).then(() => {
     log.debug("Alice signing in");
 
     // We're polling for the first email
-    Assert.ok(fxa.internal.currentAccountState !== initialState);
-    let aliceState = fxa.internal.currentAccountState;
+    Assert.ok(fxa._internal.currentAccountState !== initialState);
+    let aliceState = fxa._internal.currentAccountState;
 
     // The polling timer is ticking
-    Assert.ok(fxa.internal.currentTimer > 0);
+    Assert.ok(fxa._internal.currentTimer > 0);
 
-    fxa.internal.getUserAccountData().then(user => {
+    fxa._internal.getUserAccountData().then(user => {
       Assert.equal(user.email, alice.email);
       Assert.equal(user.verified, false);
       log.debug("Alice wants verification email resent");
 
       fxa.resendVerificationEmail().then(result => {
         // Mock server response; ensures that the session token actually was
         // passed to the client to make the hawk call
         Assert.equal(result, "alice's session token");
 
         // Timer was not restarted
-        Assert.ok(fxa.internal.currentAccountState === aliceState);
+        Assert.ok(fxa._internal.currentAccountState === aliceState);
 
         // Timer is still ticking
-        Assert.ok(fxa.internal.currentTimer > 0);
+        Assert.ok(fxa._internal.currentTimer > 0);
 
         // Ok abort polling before we go on to the next test
-        fxa.internal.abortExistingFlow();
+        fxa._internal.abortExistingFlow();
         run_next_test();
       });
     });
   });
 });
 
 add_test(function test_getOAuthToken() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
   let getTokenFromAssertionCalled = false;
 
-  fxa.internal._d_signCertificate.resolve("cert1");
+  fxa._internal._d_signCertificate.resolve("cert1");
 
   // create a mock oauth client
   let client = new FxAccountsOAuthGrantClient({
     serverURL: "https://example.com/v1",
     client_id: "abc123",
   });
   client.getTokenFromAssertion = function() {
     getTokenFromAssertionCalled = true;
@@ -1293,17 +1296,17 @@ add_test(function test_getOAuthToken() {
 });
 
 add_test(function test_getOAuthTokenScoped() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
   let getTokenFromAssertionCalled = false;
 
-  fxa.internal._d_signCertificate.resolve("cert1");
+  fxa._internal._d_signCertificate.resolve("cert1");
 
   // create a mock oauth client
   let client = new FxAccountsOAuthGrantClient({
     serverURL: "https://example.com/v1",
     client_id: "abc123",
   });
   client.getTokenFromAssertion = function(assertion, scopeString) {
     equal(scopeString, "bar foo"); // scopes are sorted locally before request.
@@ -1321,17 +1324,17 @@ add_test(function test_getOAuthTokenScop
 });
 
 add_task(async function test_getOAuthTokenCached() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
   let numTokenFromAssertionCalls = 0;
 
-  fxa.internal._d_signCertificate.resolve("cert1");
+  fxa._internal._d_signCertificate.resolve("cert1");
 
   // create a mock oauth client
   let client = new FxAccountsOAuthGrantClient({
     serverURL: "https://example.com/v1",
     client_id: "abc123",
   });
   client.getTokenFromAssertion = function() {
     numTokenFromAssertionCalls += 1;
@@ -1366,17 +1369,17 @@ add_task(async function test_getOAuthTok
 });
 
 add_task(async function test_getOAuthTokenCachedScopeNormalization() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
   let numTokenFromAssertionCalls = 0;
 
-  fxa.internal._d_signCertificate.resolve("cert1");
+  fxa._internal._d_signCertificate.resolve("cert1");
 
   // create a mock oauth client
   let client = new FxAccountsOAuthGrantClient({
     serverURL: "https://example.com/v1",
     client_id: "abc123",
   });
   client.getTokenFromAssertion = function() {
     numTokenFromAssertionCalls += 1;
@@ -1454,17 +1457,17 @@ add_test(function test_getOAuthToken_mis
     );
     fxa.signOut().then(run_next_test);
   });
 });
 
 add_test(function test_getOAuthToken_no_account() {
   let fxa = new MockFxAccounts();
 
-  fxa.internal.currentAccountState.getUserAccountData = function() {
+  fxa._internal.currentAccountState.getUserAccountData = function() {
     return Promise.resolve(null);
   };
 
   fxa.getOAuthToken({ scope: "profile" }).catch(err => {
     Assert.equal(err.message, "NO_ACCOUNT");
     fxa.signOut().then(run_next_test);
   });
 });
@@ -1481,17 +1484,17 @@ add_test(function test_getOAuthToken_unv
   });
 });
 
 add_test(function test_getOAuthToken_network_error() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
 
-  fxa.internal._d_signCertificate.resolve("cert1");
+  fxa._internal._d_signCertificate.resolve("cert1");
 
   // create a mock oauth client
   let client = new FxAccountsOAuthGrantClient({
     serverURL: "https://example.com/v1",
     client_id: "abc123",
   });
   client.getTokenFromAssertion = function() {
     return Promise.reject(
@@ -1511,17 +1514,17 @@ add_test(function test_getOAuthToken_net
   });
 });
 
 add_test(function test_getOAuthToken_auth_error() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
 
-  fxa.internal._d_signCertificate.resolve("cert1");
+  fxa._internal._d_signCertificate.resolve("cert1");
 
   // create a mock oauth client
   let client = new FxAccountsOAuthGrantClient({
     serverURL: "https://example.com/v1",
     client_id: "abc123",
   });
   client.getTokenFromAssertion = function() {
     return Promise.reject(
@@ -1541,17 +1544,17 @@ add_test(function test_getOAuthToken_aut
   });
 });
 
 add_test(function test_getOAuthToken_unknown_error() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
 
-  fxa.internal._d_signCertificate.resolve("cert1");
+  fxa._internal._d_signCertificate.resolve("cert1");
 
   // create a mock oauth client
   let client = new FxAccountsOAuthGrantClient({
     serverURL: "https://example.com/v1",
     client_id: "abc123",
   });
   client.getTokenFromAssertion = function() {
     return Promise.reject("boom");
@@ -1579,38 +1582,38 @@ add_test(function test_getSignedInUserPr
     _signOutServer() {
       return Promise.resolve();
     },
     _registerOrUpdateDevice() {
       return Promise.resolve();
     },
   });
 
-  fxa.setSignedInUser(alice).then(() => {
-    fxa.internal._profile = mockProfile;
+  fxa._internal.setSignedInUser(alice).then(() => {
+    fxa._internal._profile = mockProfile;
     fxa.getSignedInUserProfile().then(result => {
       Assert.ok(!!result);
       Assert.equal(result.avatar, "image");
       run_next_test();
     });
   });
 });
 
 add_test(function test_getSignedInUserProfile_error_uses_account_data() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
 
-  fxa.internal.getSignedInUser = function() {
+  fxa._internal.getSignedInUser = function() {
     return Promise.resolve({ email: "foo@bar.com" });
   };
 
   let teardownCalled = false;
   fxa.setSignedInUser(alice).then(() => {
-    fxa.internal._profile = {
+    fxa._internal._profile = {
       getProfile() {
         return Promise.reject("boom");
       },
       tearDown() {
         teardownCalled = true;
       },
     };
 
@@ -1624,51 +1627,49 @@ add_test(function test_getSignedInUserPr
   });
 });
 
 add_task(async function test_checkVerificationStatusFailed() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
 
-  let client = fxa.internal.fxAccountsClient;
+  let client = fxa._internal.fxAccountsClient;
   client.recoveryEmailStatus = () => {
     return Promise.reject({
       code: 401,
       errno: ERRNO_INVALID_AUTH_TOKEN,
     });
   };
   client.accountStatus = () => Promise.resolve(true);
 
   await fxa.setSignedInUser(alice);
-  let user = await fxa.internal.getUserAccountData();
+  let user = await fxa._internal.getUserAccountData();
   Assert.notEqual(alice.sessionToken, null);
   Assert.equal(user.email, alice.email);
   Assert.equal(user.verified, true);
 
-  await fxa.checkVerificationStatus();
+  await fxa._internal.checkVerificationStatus();
 
-  user = await fxa.internal.getUserAccountData();
+  user = await fxa._internal.getUserAccountData();
   Assert.equal(user.email, alice.email);
   Assert.equal(user.sessionToken, null);
 });
 
 add_task(async function test_deriveKeys() {
-  let account = MakeFxAccounts();
+  let account = await MakeFxAccounts();
   let kBhex =
     "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
   let kB = CommonUtils.hexToBytes(kBhex);
   const uid = "1ad7f502-4cc7-4ec1-a209-071fd2fae348";
 
-  const {
-    kSync,
-    kXCS,
-    kExtSync,
-    kExtKbHash,
-  } = await account.internal._deriveKeys(uid, kB);
+  const { kSync, kXCS, kExtSync, kExtKbHash } = await account.keys._deriveKeys(
+    uid,
+    kB
+  );
 
   Assert.equal(
     kSync,
     "ad501a50561be52b008878b2e0d8a73357778a712255f7722f497b5d4df14b05" +
       "dc06afb836e1521e882f521eb34691d172337accdbf6e2a5b968b05a7bbb9885"
   );
   Assert.equal(kXCS, "6ae94683571c7a7c54dab4700aa3995f");
   Assert.equal(
--- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
@@ -138,39 +138,43 @@ async function MockFxAccounts(credential
         });
       },
       unsubscribe() {
         return Promise.resolve();
       },
     },
     DEVICE_REGISTRATION_VERSION,
   });
-  await fxa.internal.setSignedInUser(credentials);
+  await fxa._internal.setSignedInUser(credentials);
   Services.prefs.setStringPref(
     "identity.fxaccounts.account.device.name",
     device.name || "mock device name"
   );
   return fxa;
 }
 
+function updateUserAccountData(fxa, data) {
+  return fxa._internal.updateUserAccountData(data);
+}
+
 add_task(async function test_updateDeviceRegistration_with_new_device() {
   const deviceName = "foo";
   const deviceType = "bar";
 
   const credentials = getTestUser("baz");
   const fxa = await MockFxAccounts(credentials, { name: deviceName });
   // Remove the current device registration (setSignedInUser does one!).
-  await fxa.updateUserAccountData({ uid: credentials.uid, device: null });
+  await updateUserAccountData(fxa, { uid: credentials.uid, device: null });
 
   const spy = {
     registerDevice: { count: 0, args: [] },
     updateDevice: { count: 0, args: [] },
     getDeviceList: { count: 0, args: [] },
   };
-  const client = fxa.internal.fxAccountsClient;
+  const client = fxa._internal.fxAccountsClient;
   client.registerDevice = function() {
     spy.registerDevice.count += 1;
     spy.registerDevice.args.push(arguments);
     return Promise.resolve({
       id: "newly-generated device id",
       createdAt: Date.now(),
       name: deviceName,
       type: deviceType,
@@ -198,44 +202,44 @@ add_task(async function test_updateDevic
   Assert.equal(spy.registerDevice.args[0][2], "desktop");
   Assert.equal(
     spy.registerDevice.args[0][3].pushCallback,
     "http://mochi.test:8888"
   );
   Assert.equal(spy.registerDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
   Assert.equal(spy.registerDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
 
-  const state = fxa.internal.currentAccountState;
+  const state = fxa._internal.currentAccountState;
   const data = await state.getUserAccountData();
 
   Assert.equal(data.device.id, "newly-generated device id");
   Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION);
 });
 
 add_task(async function test_updateDeviceRegistration_with_existing_device() {
   const deviceId = "my device id";
   const deviceName = "phil's device";
 
   const credentials = getTestUser("pb");
   const fxa = await MockFxAccounts(credentials, { name: deviceName });
-  await fxa.updateUserAccountData({
+  await updateUserAccountData(fxa, {
     uid: credentials.uid,
     device: {
       id: deviceId,
       registeredCommandsKeys: [],
       registrationVersion: 1, // < 42
     },
   });
 
   const spy = {
     registerDevice: { count: 0, args: [] },
     updateDevice: { count: 0, args: [] },
     getDeviceList: { count: 0, args: [] },
   };
-  const client = fxa.internal.fxAccountsClient;
+  const client = fxa._internal.fxAccountsClient;
   client.registerDevice = function() {
     spy.registerDevice.count += 1;
     spy.registerDevice.args.push(arguments);
     return Promise.resolve({});
   };
   client.updateDevice = function() {
     spy.updateDevice.count += 1;
     spy.updateDevice.args.push(arguments);
@@ -260,46 +264,46 @@ add_task(async function test_updateDevic
   Assert.equal(spy.updateDevice.args[0][2], deviceName);
   Assert.equal(
     spy.updateDevice.args[0][3].pushCallback,
     "http://mochi.test:8888"
   );
   Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
   Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
 
-  const state = fxa.internal.currentAccountState;
+  const state = fxa._internal.currentAccountState;
   const data = await state.getUserAccountData();
 
   Assert.equal(data.device.id, deviceId);
   Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION);
 });
 
 add_task(
   async function test_updateDeviceRegistration_with_unknown_device_error() {
     const deviceName = "foo";
     const deviceType = "bar";
     const currentDeviceId = "my device id";
 
     const credentials = getTestUser("baz");
     const fxa = await MockFxAccounts(credentials, { name: deviceName });
-    await fxa.updateUserAccountData({
+    await updateUserAccountData(fxa, {
       uid: credentials.uid,
       device: {
         id: currentDeviceId,
         registeredCommandsKeys: [],
         registrationVersion: 1, // < 42
       },
     });
 
     const spy = {
       registerDevice: { count: 0, args: [] },
       updateDevice: { count: 0, args: [] },
       getDeviceList: { count: 0, args: [] },
     };
-    const client = fxa.internal.fxAccountsClient;
+    const client = fxa._internal.fxAccountsClient;
     client.registerDevice = function() {
       spy.registerDevice.count += 1;
       spy.registerDevice.args.push(arguments);
       return Promise.resolve({
         id: "a different newly-generated device id",
         createdAt: Date.now(),
         name: deviceName,
         type: deviceType,
@@ -330,47 +334,47 @@ add_task(
     Assert.equal(spy.updateDevice.args[0][2], deviceName);
     Assert.equal(
       spy.updateDevice.args[0][3].pushCallback,
       "http://mochi.test:8888"
     );
     Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
     Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
 
-    const state = fxa.internal.currentAccountState;
+    const state = fxa._internal.currentAccountState;
     const data = await state.getUserAccountData();
 
     Assert.equal(null, data.device);
   }
 );
 
 add_task(
   async function test_updateDeviceRegistration_with_device_session_conflict_error() {
     const deviceName = "foo";
     const deviceType = "bar";
     const currentDeviceId = "my device id";
     const conflictingDeviceId = "conflicting device id";
 
     const credentials = getTestUser("baz");
     const fxa = await MockFxAccounts(credentials, { name: deviceName });
-    await fxa.updateUserAccountData({
+    await updateUserAccountData(fxa, {
       uid: credentials.uid,
       device: {
         id: currentDeviceId,
         registeredCommandsKeys: [],
         registrationVersion: 1, // < 42
       },
     });
 
     const spy = {
       registerDevice: { count: 0, args: [] },
       updateDevice: { count: 0, args: [], times: [] },
       getDeviceList: { count: 0, args: [] },
     };
-    const client = fxa.internal.fxAccountsClient;
+    const client = fxa._internal.fxAccountsClient;
     client.registerDevice = function() {
       spy.registerDevice.count += 1;
       spy.registerDevice.args.push(arguments);
       return Promise.resolve({});
     };
     client.updateDevice = function() {
       spy.updateDevice.count += 1;
       spy.updateDevice.args.push(arguments);
@@ -420,38 +424,38 @@ add_task(
     );
     Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
     Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
     Assert.equal(spy.getDeviceList.count, 1);
     Assert.equal(spy.getDeviceList.args[0].length, 1);
     Assert.equal(spy.getDeviceList.args[0][0], credentials.sessionToken);
     Assert.ok(spy.getDeviceList.time >= spy.updateDevice.time);
 
-    const state = fxa.internal.currentAccountState;
+    const state = fxa._internal.currentAccountState;
     const data = await state.getUserAccountData();
 
     Assert.equal(data.device.id, conflictingDeviceId);
     Assert.equal(data.device.registrationVersion, null);
   }
 );
 
 add_task(
   async function test_updateDeviceRegistration_with_unrecoverable_error() {
     const deviceName = "foo";
 
     const credentials = getTestUser("baz");
     const fxa = await MockFxAccounts(credentials, { name: deviceName });
-    await fxa.updateUserAccountData({ uid: credentials.uid, device: null });
+    await updateUserAccountData(fxa, { uid: credentials.uid, device: null });
 
     const spy = {
       registerDevice: { count: 0, args: [] },
       updateDevice: { count: 0, args: [] },
       getDeviceList: { count: 0, args: [] },
     };
-    const client = fxa.internal.fxAccountsClient;
+    const client = fxa._internal.fxAccountsClient;
     client.registerDevice = function() {
       spy.registerDevice.count += 1;
       spy.registerDevice.args.push(arguments);
       return Promise.reject({
         code: 400,
         errno: ERRNO_TOO_MANY_CLIENT_REQUESTS,
       });
     };
@@ -468,151 +472,151 @@ add_task(
 
     await fxa.updateDeviceRegistration();
 
     Assert.equal(spy.getDeviceList.count, 0);
     Assert.equal(spy.updateDevice.count, 0);
     Assert.equal(spy.registerDevice.count, 1);
     Assert.equal(spy.registerDevice.args[0].length, 4);
 
-    const state = fxa.internal.currentAccountState;
+    const state = fxa._internal.currentAccountState;
     const data = await state.getUserAccountData();
 
     Assert.equal(null, data.device);
   }
 );
 
 add_task(
   async function test_getDeviceId_with_no_device_id_invokes_device_registration() {
     const credentials = getTestUser("foo");
     credentials.verified = true;
     const fxa = await MockFxAccounts(credentials);
-    await fxa.updateUserAccountData({ uid: credentials.uid, device: null });
+    await updateUserAccountData(fxa, { uid: credentials.uid, device: null });
 
     const spy = { count: 0, args: [] };
-    fxa.internal.currentAccountState.getUserAccountData = () =>
+    fxa._internal.currentAccountState.getUserAccountData = () =>
       Promise.resolve({
         email: credentials.email,
         registrationVersion: DEVICE_REGISTRATION_VERSION,
       });
-    fxa.internal._registerOrUpdateDevice = function() {
+    fxa._internal._registerOrUpdateDevice = function() {
       spy.count += 1;
       spy.args.push(arguments);
       return Promise.resolve("bar");
     };
 
-    const result = await fxa.internal.device.getLocalId();
+    const result = await fxa.device.getLocalId();
 
     Assert.equal(spy.count, 1);
     Assert.equal(spy.args[0].length, 1);
     Assert.equal(spy.args[0][0].email, credentials.email);
     Assert.equal(null, spy.args[0][0].device);
     Assert.equal(result, "bar");
   }
 );
 
 add_task(
   async function test_getDeviceId_with_registration_version_outdated_invokes_device_registration() {
     const credentials = getTestUser("foo");
     credentials.verified = true;
     const fxa = await MockFxAccounts(credentials);
 
     const spy = { count: 0, args: [] };
-    fxa.internal.currentAccountState.getUserAccountData = () =>
+    fxa._internal.currentAccountState.getUserAccountData = () =>
       Promise.resolve({
         device: {
           id: "my id",
           registrationVersion: 0,
           registeredCommandsKeys: [],
         },
       });
-    fxa.internal._registerOrUpdateDevice = function() {
+    fxa._internal._registerOrUpdateDevice = function() {
       spy.count += 1;
       spy.args.push(arguments);
       return Promise.resolve("wibble");
     };
 
-    const result = await fxa.internal.device.getLocalId();
+    const result = await fxa.device.getLocalId();
 
     Assert.equal(spy.count, 1);
     Assert.equal(spy.args[0].length, 1);
     Assert.equal(spy.args[0][0].device.id, "my id");
     Assert.equal(result, "wibble");
   }
 );
 
 add_task(
   async function test_getDeviceId_with_device_id_and_uptodate_registration_version_doesnt_invoke_device_registration() {
     const credentials = getTestUser("foo");
     credentials.verified = true;
     const fxa = await MockFxAccounts(credentials);
 
     const spy = { count: 0 };
-    fxa.internal.currentAccountState.getUserAccountData = async () => ({
+    fxa._internal.currentAccountState.getUserAccountData = async () => ({
       device: {
         id: "foo's device id",
         registrationVersion: DEVICE_REGISTRATION_VERSION,
         registeredCommandsKeys: [],
       },
     });
-    fxa.internal._registerOrUpdateDevice = function() {
+    fxa._internal._registerOrUpdateDevice = function() {
       spy.count += 1;
       return Promise.resolve("bar");
     };
 
-    const result = await fxa.internal.device.getLocalId();
+    const result = await fxa.device.getLocalId();
 
     Assert.equal(spy.count, 0);
     Assert.equal(result, "foo's device id");
   }
 );
 
 add_task(
   async function test_getDeviceId_with_device_id_and_with_no_registration_version_invokes_device_registration() {
     const credentials = getTestUser("foo");
     credentials.verified = true;
     const fxa = await MockFxAccounts(credentials);
 
     const spy = { count: 0, args: [] };
-    fxa.internal.currentAccountState.getUserAccountData = () =>
+    fxa._internal.currentAccountState.getUserAccountData = () =>
       Promise.resolve({ device: { id: "wibble" } });
-    fxa.internal._registerOrUpdateDevice = function() {
+    fxa._internal._registerOrUpdateDevice = function() {
       spy.count += 1;
       spy.args.push(arguments);
       return Promise.resolve("wibble");
     };
 
-    const result = await fxa.internal.device.getLocalId();
+    const result = await fxa.device.getLocalId();
 
     Assert.equal(spy.count, 1);
     Assert.equal(spy.args[0].length, 1);
     Assert.equal(spy.args[0][0].device.id, "wibble");
     Assert.equal(result, "wibble");
   }
 );
 
 add_task(async function test_devicelist_pushendpointexpired() {
   const deviceId = "mydeviceid";
   const credentials = getTestUser("baz");
   credentials.verified = true;
   const fxa = await MockFxAccounts(credentials);
-  await fxa.updateUserAccountData({
+  await updateUserAccountData(fxa, {
     uid: credentials.uid,
     device: {
       id: deviceId,
       registeredCommandsKeys: [],
       registrationVersion: 1, // < 42
     },
   });
 
   const spy = {
     updateDevice: { count: 0, args: [] },
     getDeviceList: { count: 0, args: [] },
   };
-  const client = fxa.internal.fxAccountsClient;
+  const client = fxa._internal.fxAccountsClient;
   client.updateDevice = function() {
     spy.updateDevice.count += 1;
     spy.updateDevice.args.push(arguments);
     return Promise.resolve({});
   };
   client.getDeviceList = function() {
     spy.getDeviceList.count += 1;
     spy.getDeviceList.args.push(arguments);
--- a/services/fxaccounts/tests/xpcshell/test_commands.js
+++ b/services/fxaccounts/tests/xpcshell/test_commands.js
@@ -68,25 +68,27 @@ add_task(async function test_commands_po
   // Local state.
   const pushIndexReceived = 11;
   const accountState = {
     data: {
       device: {
         lastCommandIndex: 10,
       },
     },
+    getUserAccountData() {
+      return this.data;
+    },
+    updateUserAccountData(data) {
+      this.data = data;
+    },
   };
 
   const fxAccounts = {
-    async _withCurrentAccountState(cb) {
-      const get = () => accountState.data;
-      const set = val => {
-        accountState.data = val;
-      };
-      await cb(get, set);
+    async withCurrentAccountState(cb) {
+      await cb(accountState);
     },
   };
   const commands = new FxAccountsCommands(fxAccounts);
   const mockCommands = sinon.mock(commands);
   mockCommands
     .expects("_fetchDeviceCommands")
     .once()
     .withArgs(11)
@@ -109,25 +111,27 @@ add_task(
     // Local state.
     const pushIndexReceived = 12;
     const accountState = {
       data: {
         device: {
           lastCommandIndex: 12,
         },
       },
+      getUserAccountData() {
+        return this.data;
+      },
+      updateUserAccountData(data) {
+        this.data = data;
+      },
     };
 
     const fxAccounts = {
-      async _withCurrentAccountState(cb) {
-        const get = () => accountState.data;
-        const set = val => {
-          accountState.data = val;
-        };
-        await cb(get, set);
+      async withCurrentAccountState(cb) {
+        await cb(accountState);
       },
     };
     const commands = new FxAccountsCommands(fxAccounts);
     const mockCommands = sinon.mock(commands);
     mockCommands.expects("_fetchDeviceCommands").never();
     mockCommands.expects("_handleCommands").never();
     await commands.pollDeviceCommands(pushIndexReceived);
 
@@ -152,25 +156,27 @@ add_task(
     const remoteIndex = 12;
 
     // Local state.
     const pushIndexReceived = 11;
     const accountState = {
       data: {
         device: {},
       },
+      getUserAccountData() {
+        return this.data;
+      },
+      updateUserAccountData(data) {
+        this.data = data;
+      },
     };
 
     const fxAccounts = {
-      async _withCurrentAccountState(cb) {
-        const get = () => accountState.data;
-        const set = val => {
-          accountState.data = val;
-        };
-        await cb(get, set);
+      async withCurrentAccountState(cb) {
+        await cb(accountState);
       },
     };
     const commands = new FxAccountsCommands(fxAccounts);
     const mockCommands = sinon.mock(commands);
     mockCommands
       .expects("_fetchDeviceCommands")
       .once()
       .withArgs(11)
@@ -205,25 +211,27 @@ add_task(async function test_commands_po
 
   // Local state.
   const accountState = {
     data: {
       device: {
         lastCommandIndex: 10,
       },
     },
+    getUserAccountData() {
+      return this.data;
+    },
+    updateUserAccountData(data) {
+      this.data = data;
+    },
   };
 
   const fxAccounts = {
-    async _withCurrentAccountState(cb) {
-      const get = () => accountState.data;
-      const set = val => {
-        accountState.data = val;
-      };
-      await cb(get, set);
+    async withCurrentAccountState(cb) {
+      await cb(accountState);
     },
   };
   const commands = new FxAccountsCommands(fxAccounts);
   const mockCommands = sinon.mock(commands);
   mockCommands
     .expects("_fetchDeviceCommands")
     .once()
     .withArgs(11)
@@ -256,25 +264,27 @@ add_task(
     ];
     const remoteIndex = 12;
 
     // Local state.
     const accountState = {
       data: {
         device: {},
       },
+      getUserAccountData() {
+        return this.data;
+      },
+      updateUserAccountData(data) {
+        this.data = data;
+      },
     };
 
     const fxAccounts = {
-      async _withCurrentAccountState(cb) {
-        const get = () => accountState.data;
-        const set = val => {
-          accountState.data = val;
-        };
-        await cb(get, set);
+      async withCurrentAccountState(cb) {
+        await cb(accountState);
       },
     };
     const commands = new FxAccountsCommands(fxAccounts);
     const mockCommands = sinon.mock(commands);
     mockCommands
       .expects("_fetchDeviceCommands")
       .once()
       .withArgs(0)
--- a/services/fxaccounts/tests/xpcshell/test_device.js
+++ b/services/fxaccounts/tests/xpcshell/test_device.js
@@ -77,16 +77,16 @@ add_task(async function test_reset() {
     assertion: "foobar",
     sessionToken: "dead",
     kSync: "beef",
     kXCS: "cafe",
     kExtSync: "bacon",
     kExtKbHash: "cheese",
     verified: true,
   };
-  await fxAccounts.setSignedInUser(credentials);
+  await fxAccounts._internal.setSignedInUser(credentials);
   ok(!Services.prefs.prefHasUserValue(testPref));
   // signing the user out should reset the name pref.
   const namePref = PREF_ACCOUNT_ROOT + "device.name";
   ok(Services.prefs.prefHasUserValue(namePref));
-  await fxAccounts.signOut(/* remoteOnly = */ true);
+  await fxAccounts.signOut(/* localOnly = */ true);
   ok(!Services.prefs.prefHasUserValue(namePref));
 });
--- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
@@ -84,17 +84,17 @@ add_task(async function test_simple() {
     email: "test@example.com",
     sessionToken: "sessionToken",
     kSync: "the kSync value",
     kXCS: "the kXCS value",
     kExtSync: "the kExtSync value",
     kExtKbHash: "the kExtKbHash value",
     verified: true,
   };
-  await fxa.setSignedInUser(creds);
+  await fxa._internal.setSignedInUser(creds);
 
   // This should have stored stuff in both the .json file in the profile
   // dir, and the login dir.
   let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
   let data = await CommonUtils.readJSON(path);
 
   Assert.strictEqual(
     data.accountData.email,
@@ -182,17 +182,17 @@ add_task(async function test_MPLocked() 
     kExtSync: "the kExtSync value",
     kExtKbHash: "the kExtKbHash value",
     verified: true,
   };
 
   Assert.strictEqual(getLoginMgrData(), null, "no login mgr at the start");
   // tell the storage that the MP is locked.
   setLoginMgrLoggedInState(false);
-  await fxa.setSignedInUser(creds);
+  await fxa._internal.setSignedInUser(creds);
 
   // This should have stored stuff in the .json, and the login manager stuff
   // will not exist.
   let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
   let data = await CommonUtils.readJSON(path);
 
   Assert.strictEqual(
     data.accountData.email,
@@ -248,24 +248,24 @@ add_task(async function test_consistentW
     kSync: "the kSync value2",
     kXCS: "the kXCS value2",
     kExtSync: "the kExtSync value2",
     kExtKbHash: "the kExtKbHash value2",
     verified: false,
   };
 
   // Log a user in while MP is unlocked.
-  await fxa.setSignedInUser(creds1);
+  await fxa._internal.setSignedInUser(creds1);
 
   // tell the storage that the MP is locked - this will prevent logout from
   // being able to clear the data.
   setLoginMgrLoggedInState(false);
 
   // now set the second credentials.
-  await fxa.setSignedInUser(creds2);
+  await fxa._internal.setSignedInUser(creds2);
 
   // We should still have creds1 data in the login manager.
   let login = getLoginMgrData();
   Assert.strictEqual(login.username, creds1.uid);
   // and that we do have the first kSync in the login manager.
   Assert.strictEqual(
     JSON.parse(login.password).accountData.kSync,
     creds1.kSync,
--- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
@@ -125,27 +125,27 @@ async function createMockFxA() {
     assertion: "foobar",
     sessionToken: "dead",
     kSync: "beef",
     kXCS: "cafe",
     kExtSync: "bacon",
     kExtKbHash: "cheese",
     verified: true,
   };
-  await fxa.setSignedInUser(credentials);
+  await fxa._internal.setSignedInUser(credentials);
   return fxa;
 }
 
 // The tests.
 
 add_task(async function testCacheStorage() {
   let fxa = await createMockFxA();
 
   // Hook what the impl calls to save to disk.
-  let cas = fxa.internal.currentAccountState;
+  let cas = fxa._internal.currentAccountState;
   let origPersistCached = cas._persistCachedTokens.bind(cas);
   cas._persistCachedTokens = function() {
     return origPersistCached().then(() => {
       Services.obs.notifyObservers(null, "testhelper-fxa-cache-persist-done");
     });
   };
 
   let promiseWritten = promiseNotification("testhelper-fxa-cache-persist-done");
--- a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
@@ -130,17 +130,17 @@ async function createMockFxA(mockGrantCl
     sessionToken: "dead",
     kSync: "beef",
     kXCS: "cafe",
     kExtSync: "bacon",
     kExtKbHash: "cheese",
     verified: true,
   };
 
-  await fxa.setSignedInUser(credentials);
+  await fxa._internal.setSignedInUser(credentials);
   return fxa;
 }
 
 // The tests.
 
 function MockFxAccountsOAuthGrantClient() {
   this.activeTokens = new Set();
 }
@@ -248,34 +248,8 @@ add_task(async function testTokenRaces()
     promiseNotification("testhelper-fxa-revoke-complete"),
   ]);
   await fxa.removeCachedOAuthToken({ token: results[0] });
   equal(client.activeTokens.size, 1);
   await fxa.removeCachedOAuthToken({ token: results[2] });
   equal(client.activeTokens.size, 0);
   await notifications;
 });
-
-add_task(async function testSignOutDuringFetch() {
-  let client = new MockFxAccountsOAuthGrantClient();
-  let fxa = await createMockFxA(client);
-
-  // We need a couple of promises to ensure things happen at the right time...
-  let resolveSignedOut;
-  let promiseSignedOut = new Promise(resolve => {
-    resolveSignedOut = resolve;
-  });
-  let resolveInGetTokenFromAssertion;
-  let promiseInGetTokenFromAssertion = new Promise(resolve => {
-    resolveInGetTokenFromAssertion = resolve;
-  });
-  client.getTokenFromAssertion = async function(assertion, scope) {
-    resolveInGetTokenFromAssertion();
-    await promiseSignedOut;
-    return { access_token: "token" };
-  };
-
-  let getTokenPromise = fxa.getOAuthToken({ scope: "test-scope", client });
-  await promiseInGetTokenFromAssertion;
-  await fxa.signOut();
-  resolveSignedOut();
-  await Assert.rejects(getTokenPromise, /Another user has signed in/);
-});
--- a/services/fxaccounts/tests/xpcshell/test_pairing.js
+++ b/services/fxaccounts/tests/xpcshell/test_pairing.js
@@ -47,24 +47,26 @@ const fxaConfig = {
   promisePairingURI() {
     return PAIR_URI;
   },
   promiseOAuthURI() {
     return OAUTH_URI;
   },
 };
 const fxAccounts = {
-  getScopedKeys(scope) {
-    return {
-      [scope]: {
-        kid: "123456",
-        k: KSYNC,
-        kty: "oct",
-      },
-    };
+  keys: {
+    getScopedKeys(scope) {
+      return {
+        [scope]: {
+          kid: "123456",
+          k: KSYNC,
+          kty: "oct",
+        },
+      };
+    },
   },
   authorizeOAuthCode() {
     return { code: "mycode", state: "mystate" };
   },
   getSignedInUserProfile() {
     return {
       uid: UID,
       email: EMAIL,
--- a/services/fxaccounts/tests/xpcshell/test_profile.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile.js
@@ -27,64 +27,77 @@ let mockClient = function(fxa) {
 
 const ACCOUNT_UID = "abc123";
 const ACCOUNT_EMAIL = "foo@bar.com";
 const ACCOUNT_DATA = {
   uid: ACCOUNT_UID,
   email: ACCOUNT_EMAIL,
 };
 
-function FxaMock() {}
-FxaMock.prototype = {
-  currentAccountState: {
-    profile: null,
-    get isCurrent() {
-      return true;
+let mockFxa = function() {
+  let fxa = {
+    // helpers to make the tests using this mock less verbose...
+    set _testProfileCache(profileCache) {
+      this._internal.currentAccountState._data.profileCache = profileCache;
+    },
+    get _testProfileCache() {
+      return this._internal.currentAccountState._data.profileCache;
     },
-  },
-
-  getSignedInUser() {
-    return Promise.resolve(ACCOUNT_DATA);
-  },
+  };
+  fxa._internal = Object.assign(
+    {},
+    {
+      currentAccountState: Object.assign(
+        {},
+        {
+          _data: Object.assign({}, ACCOUNT_DATA),
 
-  getProfileCache() {
-    return Promise.resolve(this.profileCache);
-  },
+          get isCurrent() {
+            return true;
+          },
+
+          async getUserAccountData() {
+            return this._data;
+          },
 
-  setProfileCache(profileCache) {
-    this.profileCache = profileCache;
-    return Promise.resolve();
-  },
-};
+          async updateUserAccountData(data) {
+            this._data = Object.assign(this._data, data);
+          },
+        }
+      ),
 
-let mockFxa = function() {
-  return new FxaMock();
+      withCurrentAccountState(cb) {
+        return cb(this.currentAccountState);
+      },
+    }
+  );
+  return fxa;
 };
 
 function CreateFxAccountsProfile(fxa = null, client = null) {
   if (!fxa) {
     fxa = mockFxa();
   }
   let options = {
-    fxa,
+    fxai: fxa._internal,
     profileServerUrl: "http://127.0.0.1:1111/v1",
   };
   if (client) {
     options.profileClient = client;
   }
   return new FxAccountsProfile(options);
 }
 
 add_test(function cacheProfile_change() {
   let setProfileCacheCalled = false;
   let fxa = mockFxa();
-  fxa.setProfileCache = data => {
+  fxa._internal.currentAccountState.updateUserAccountData = data => {
     setProfileCacheCalled = true;
-    Assert.equal(data.profile.avatar, "myurl");
-    Assert.equal(data.etag, "bogusetag");
+    Assert.equal(data.profileCache.profile.avatar, "myurl");
+    Assert.equal(data.profileCache.etag, "bogusetag");
     return Promise.resolve();
   };
   let profile = CreateFxAccountsProfile(fxa);
 
   makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function(subject, topic, data) {
     Assert.equal(data, ACCOUNT_DATA.uid);
     Assert.ok(setProfileCacheCalled);
     run_next_test();
@@ -132,17 +145,17 @@ add_test(function fetchAndCacheProfile_a
       Assert.notEqual(profile._cachedAt, 12345, "cachedAt has been bumped");
       run_next_test();
     }
   );
 });
 
 add_test(function fetchAndCacheProfile_sendsETag() {
   let fxa = mockFxa();
-  fxa.profileCache = { profile: {}, etag: "bogusETag" };
+  fxa._testProfileCache = { profile: {}, etag: "bogusETag" };
   let client = mockClient(fxa);
   client.fetchProfile = function(etag) {
     Assert.equal(etag, "bogusETag");
     return Promise.resolve({
       body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" },
     });
   };
   let profile = CreateFxAccountsProfile(fxa, client);
@@ -249,34 +262,34 @@ add_task(async function fetchAndCachePro
 
   let got = await profile._fetchAndCacheProfile();
   Assert.equal(got.avatar, "myimg");
 });
 
 add_test(function fetchAndCacheProfile_alreadyCached() {
   let cachedUrl = "cachedurl";
   let fxa = mockFxa();
-  fxa.profileCache = {
+  fxa._testProfileCache = {
     profile: { uid: ACCOUNT_UID, avatar: cachedUrl },
     etag: "bogusETag",
   };
   let client = mockClient(fxa);
   client.fetchProfile = function(etag) {
     Assert.equal(etag, "bogusETag");
     return Promise.resolve(null);
   };
 
   let profile = CreateFxAccountsProfile(fxa, client);
   profile._cacheProfile = function(toCache) {
     do_throw("This method should not be called.");
   };
 
   return profile._fetchAndCacheProfile().then(result => {
     Assert.equal(result, null);
-    Assert.equal(fxa.profileCache.profile.avatar, cachedUrl);
+    Assert.equal(fxa._testProfileCache.profile.avatar, cachedUrl);
     run_next_test();
   });
 });
 
 // Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the
 // last one doesn't kick off a new request to check the cached copy is fresh.
 add_task(async function fetchAndCacheProfileAfterThreshold() {
   /*
@@ -366,48 +379,47 @@ add_task(async function fetchAndCachePro
   await backgroundFetchDone.promise;
   Assert.equal(numFetches, 2);
 });
 
 add_test(function tearDown_ok() {
   let profile = CreateFxAccountsProfile();
 
   Assert.ok(!!profile.client);
-  Assert.ok(!!profile.fxa);
+  Assert.ok(!!profile.fxai);
 
   profile.tearDown();
-  Assert.equal(null, profile.fxa);
+  Assert.equal(null, profile.fxai);
   Assert.equal(null, profile.client);
 
   run_next_test();
 });
 
 add_task(async function getProfile_ok() {
   let cachedUrl = "myurl";
   let didFetch = false;
 
   let fxa = mockFxa();
-  fxa.profileCache = { profile: { uid: ACCOUNT_UID, avatar: cachedUrl } };
+  fxa._testProfileCache = { profile: { uid: ACCOUNT_UID, avatar: cachedUrl } };
   let profile = CreateFxAccountsProfile(fxa);
 
   profile._fetchAndCacheProfile = function() {
     didFetch = true;
     return Promise.resolve();
   };
 
   let result = await profile.getProfile();
 
   Assert.equal(result.avatar, cachedUrl);
   Assert.ok(didFetch);
 });
 
 add_task(async function getProfile_no_cache() {
   let fetchedUrl = "newUrl";
   let fxa = mockFxa();
-  fxa.profileCache = null;
   let profile = CreateFxAccountsProfile(fxa);
 
   profile._fetchAndCacheProfileInternal = function() {
     return Promise.resolve({ uid: ACCOUNT_UID, avatar: fetchedUrl });
   };
 
   await profile.getProfile(); // returns null.
   let result = await profile._currentFetchPromise;
@@ -421,17 +433,17 @@ add_test(function getProfile_has_cached_
   let client = mockClient(fxa);
   client.fetchProfile = function() {
     return Promise.resolve({
       body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: null },
     });
   };
 
   let profile = CreateFxAccountsProfile(fxa, client);
-  fxa.profileCache = {
+  fxa._testProfileCache = {
     profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: cachedUrl },
   };
 
   // instead of checking this in a mocked "save" function, just check after the
   // observer
   makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function(subject, topic, data) {
     profile.getProfile().then(profileData => {
       Assert.equal(null, profileData.avatar);
@@ -441,17 +453,17 @@ add_test(function getProfile_has_cached_
 
   return profile.getProfile().then(result => {
     Assert.equal(result.avatar, "myurl");
   });
 });
 
 add_test(function getProfile_fetchAndCacheProfile_throws() {
   let fxa = mockFxa();
-  fxa.profileCache = {
+  fxa._testProfileCache = {
     profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" },
   };
   let profile = CreateFxAccountsProfile(fxa);
 
   profile._fetchAndCacheProfile = () => Promise.reject(new Error());
 
   return profile.getProfile().then(result => {
     Assert.equal(result.avatar, "myimg");
@@ -462,17 +474,17 @@ add_test(function getProfile_fetchAndCac
 add_test(function getProfile_email_changed() {
   let fxa = mockFxa();
   let client = mockClient(fxa);
   client.fetchProfile = function() {
     return Promise.resolve({
       body: { uid: ACCOUNT_UID, email: "newemail@bar.com" },
     });
   };
-  fxa.handleEmailUpdated = email => {
+  fxa._internal._handleEmailUpdated = email => {
     Assert.equal(email, "newemail@bar.com");
     run_next_test();
   };
 
   let profile = CreateFxAccountsProfile(fxa, client);
   return profile._fetchAndCacheProfile();
 });
 
--- a/services/fxaccounts/tests/xpcshell/test_profile_client.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile_client.js
@@ -45,26 +45,26 @@ let mockResponse = function(response) {
   };
 
   return Request;
 };
 
 // A simple mock FxA that hands out tokens without checking them and doesn't
 // expect tokens to be revoked. We have specific token tests further down that
 // has more checks here.
-let mockFxa = {
+let mockFxaInternal = {
   getOAuthToken(options) {
     Assert.equal(options.scope, "profile");
     return "token";
   },
 };
 
 const PROFILE_OPTIONS = {
   serverURL: "http://127.0.0.1:1111/v1",
-  fxa: mockFxa,
+  fxai: mockFxaInternal,
 };
 
 /**
  * Mock request error responder
  * @param {Error} error
  *        Error object
  * @returns {Function}
  */
@@ -196,17 +196,17 @@ add_test(function server401ResponseThenS
       // This test never has more than 1 token alive at once, so the token
       // being revoked must always be the last token we handed out.
       Assert.equal(parseInt(options.token), lastToken);
       ++numTokensRemoved;
     },
   };
   let profileOptions = {
     serverURL: "http://127.0.0.1:1111/v1",
-    fxa: mockFxaWithRemove,
+    fxai: mockFxaWithRemove,
   };
   let client = new FxAccountsProfileClient(profileOptions);
 
   // 2 responses - first one implying the token has expired, second works.
   let responses = [
     {
       status: 401,
       body:
@@ -270,17 +270,17 @@ add_test(function server401ResponsePersi
       // This test never has more than 1 token alive at once, so the token
       // being revoked must always be the last token we handed out.
       Assert.equal(parseInt(options.token), lastToken);
       ++numTokensRemoved;
     },
   };
   let profileOptions = {
     serverURL: "http://127.0.0.1:1111/v1",
-    fxa: mockFxaWithRemove,
+    fxai: mockFxaWithRemove,
   };
   let client = new FxAccountsProfileClient(profileOptions);
 
   let response = {
     status: 401,
     body:
       '{ "code": 401, "errno": 100, "error": "It\'s not your token, it\'s you!", "message": "I don\'t like you", "reason": "Because security" }',
   };
@@ -315,17 +315,17 @@ add_test(function server401ResponsePersi
     Assert.equal(numTokensRemoved, 2);
     run_next_test();
   });
 });
 
 add_test(function networkErrorResponse() {
   let client = new FxAccountsProfileClient({
     serverURL: "http://domain.dummy",
-    fxa: mockFxa,
+    fxai: mockFxaInternal,
   });
   client.fetchProfile().catch(function(e) {
     Assert.equal(e.name, "FxAccountsProfileClientError");
     Assert.equal(e.code, null);
     Assert.equal(e.errno, ERRNO_NETWORK);
     Assert.equal(e.error, ERROR_NETWORK);
     run_next_test();
   });
@@ -408,19 +408,19 @@ add_test(function errorTests() {
  *
  * @param {Object} options
  *        FxAccountsProfileClient constructor options
  * @param {String} expected
  *        Expected error message
  * @returns {*}
  */
 function validationHelper(options, expected) {
-  // add fxa to options - that missing isn't what we are testing here.
+  // add fxai to options - that missing isn't what we are testing here.
   if (options) {
-    options.fxa = mockFxa;
+    options.fxai = mockFxaInternal;
   }
   try {
     new FxAccountsProfileClient(options);
   } catch (e) {
     return Assert.equal(e.toString(), expected);
   }
   throw new Error("Validation helper error");
 }
--- a/services/fxaccounts/tests/xpcshell/test_push_service.js
+++ b/services/fxaccounts/tests/xpcshell/test_push_service.js
@@ -70,59 +70,59 @@ let mockLog = {
 add_task(async function initialize() {
   let pushService = new FxAccountsPushService();
   equal(pushService.initialize(), false);
 });
 
 add_task(async function registerPushEndpointSuccess() {
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: mockFxAccounts,
+    fxai: mockFxAccounts,
   });
 
   let subscription = await pushService.registerPushEndpoint();
   equal(subscription.endpoint, MOCK_ENDPOINT);
 });
 
 add_task(async function registerPushEndpointFailure() {
   let failPushService = Object.assign(mockPushService, {
     subscribe(scope, principal, cb) {
       cb(Cr.NS_ERROR_ABORT);
     },
   });
 
   let pushService = new FxAccountsPushService({
     pushService: failPushService,
-    fxAccounts: mockFxAccounts,
+    fxai: mockFxAccounts,
   });
 
   let subscription = await pushService.registerPushEndpoint();
   equal(subscription, null);
 });
 
 add_task(async function unsubscribeSuccess() {
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: mockFxAccounts,
+    fxai: mockFxAccounts,
   });
 
   let result = await pushService.unsubscribe();
   equal(result, true);
 });
 
 add_task(async function unsubscribeFailure() {
   let failPushService = Object.assign(mockPushService, {
     unsubscribe(scope, principal, cb) {
       cb(Cr.NS_ERROR_ABORT);
     },
   });
 
   let pushService = new FxAccountsPushService({
     pushService: failPushService,
-    fxAccounts: mockFxAccounts,
+    fxai: mockFxAccounts,
   });
 
   let result = await pushService.unsubscribe();
   equal(result, null);
 });
 
 add_test(function observeLogout() {
   let customLog = Object.assign(mockLog, {
@@ -152,17 +152,17 @@ add_test(function observePushTopicVerify
     checkVerificationStatus() {
       // checking verification status on push messages without data
       run_next_test();
     },
   });
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: customAccounts,
+    fxai: customAccounts,
   });
 
   pushService.observe(
     emptyMsg,
     mockPushService.pushTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
 });
@@ -184,17 +184,17 @@ add_test(function observePushTopicDevice
   let obs = (subject, topic, data) => {
     Services.obs.removeObserver(obs, topic);
     run_next_test();
   };
   Services.obs.addObserver(obs, ON_DEVICE_CONNECTED_NOTIFICATION);
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: mockFxAccounts,
+    fxai: mockFxAccounts,
   });
 
   pushService.observe(
     msg,
     mockPushService.pushTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
 });
@@ -225,29 +225,29 @@ add_task(async function observePushTopic
         async getUserAccountData() {
           return { device: { id: deviceId } };
         },
       };
     },
     signOut() {
       signoutCalled = true;
     },
-  });
+  })._internal;
 
   const deviceDisconnectedNotificationObserved = new Promise(resolve => {
     Services.obs.addObserver(function obs(subject, topic, data) {
       Services.obs.removeObserver(obs, topic);
       equal(data, JSON.stringify({ isLocalDevice: true }));
       resolve();
     }, ON_DEVICE_DISCONNECTED_NOTIFICATION);
   });
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: fxAccountsMock,
+    fxai: fxAccountsMock,
   });
 
   pushService.observe(
     msg,
     mockPushService.pushTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
 
@@ -281,29 +281,29 @@ add_task(async function observePushTopic
         async getUserAccountData() {
           return { device: { id: "thelocaldevice" } };
         },
       };
     },
     signOut() {
       signoutCalled = true;
     },
-  });
+  })._internal;
 
   const deviceDisconnectedNotificationObserved = new Promise(resolve => {
     Services.obs.addObserver(function obs(subject, topic, data) {
       Services.obs.removeObserver(obs, topic);
       equal(data, JSON.stringify({ isLocalDevice: false }));
       resolve();
     }, ON_DEVICE_DISCONNECTED_NOTIFICATION);
   });
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: fxAccountsMock,
+    fxai: fxAccountsMock,
   });
 
   pushService.observe(
     msg,
     mockPushService.pushTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
 
@@ -322,25 +322,25 @@ add_test(function observePushTopicAccoun
         },
       }),
     },
     QueryInterface() {
       return this;
     },
   };
   let customAccounts = Object.assign(mockFxAccounts, {
-    handleAccountDestroyed() {
+    _handleAccountDestroyed() {
       // checking verification status on push messages without data
       run_next_test();
     },
   });
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: customAccounts,
+    fxai: customAccounts,
   });
 
   pushService.observe(
     msg,
     mockPushService.pushTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
 });
@@ -368,17 +368,17 @@ add_test(function observePushTopicVerify
     Services.obs.removeObserver(obs, topic);
     Assert.equal(data, JSON.stringify(msg.data.json().data));
     run_next_test();
   };
   Services.obs.addObserver(obs, ON_VERIFY_LOGIN_NOTIFICATION);
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: mockFxAccounts,
+    fxai: mockFxAccounts,
   });
 
   pushService.observe(
     msg,
     mockPushService.pushTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
 });
@@ -397,17 +397,17 @@ add_test(function observePushTopicProfil
   let obs = (subject, topic, data) => {
     Services.obs.removeObserver(obs, topic);
     run_next_test();
   };
   Services.obs.addObserver(obs, ON_PROFILE_CHANGE_NOTIFICATION);
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: mockFxAccounts,
+    fxai: mockFxAccounts,
   });
 
   pushService.observe(
     msg,
     mockPushService.pushTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
 });
@@ -488,17 +488,17 @@ add_task(async function commandReceived(
       pollDeviceCommands() {
         res();
       },
     };
   });
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: fxAccountsMock,
+    fxai: fxAccountsMock,
   });
 
   pushService.observe(
     msg,
     mockPushService.pushTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
   await promiseConsumeRemoteMessagesCalled;
@@ -509,17 +509,17 @@ add_test(function observeSubscriptionCha
     updateDeviceRegistration() {
       // subscription change means updating the device registration
       run_next_test();
     },
   });
 
   let pushService = new FxAccountsPushService({
     pushService: mockPushService,
-    fxAccounts: customAccounts,
+    fxai: customAccounts,
   });
 
   pushService.observe(
     null,
     mockPushService.subscriptionChangeTopic,
     FXA_PUSH_SCOPE_ACCOUNT_UPDATE
   );
 });
--- a/services/fxaccounts/tests/xpcshell/test_web_channel.js
+++ b/services/fxaccounts/tests/xpcshell/test_web_channel.js
@@ -364,32 +364,34 @@ add_test(function test_helpers_should_al
   Assert.ok(!helpers.shouldAllowRelink("not_allowed_to_relink@testuser.com"));
 
   run_next_test();
 });
 
 add_task(async function test_helpers_login_without_customize_sync() {
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
-      setSignedInUser(accountData) {
-        return new Promise(resolve => {
-          // ensure fxAccounts is informed of the new user being signed in.
-          Assert.equal(accountData.email, "testuser@testuser.com");
+      _internal: {
+        setSignedInUser(accountData) {
+          return new Promise(resolve => {
+            // ensure fxAccounts is informed of the new user being signed in.
+            Assert.equal(accountData.email, "testuser@testuser.com");
 
-          // verifiedCanLinkAccount should be stripped in the data.
-          Assert.equal(false, "verifiedCanLinkAccount" in accountData);
+            // verifiedCanLinkAccount should be stripped in the data.
+            Assert.equal(false, "verifiedCanLinkAccount" in accountData);
 
-          // previously signed in user preference is updated.
-          Assert.equal(
-            helpers.getPreviousAccountNameHashPref(),
-            CryptoUtils.sha256Base64("testuser@testuser.com")
-          );
+            // previously signed in user preference is updated.
+            Assert.equal(
+              helpers.getPreviousAccountNameHashPref(),
+              CryptoUtils.sha256Base64("testuser@testuser.com")
+            );
 
-          resolve();
-        });
+            resolve();
+          });
+        },
       },
     },
   });
 
   // ensure the previous account pref is overwritten.
   helpers.setPreviousAccountNameHashPref("lastuser@testuser.com");
 
   await helpers.login({
@@ -397,76 +399,79 @@ add_task(async function test_helpers_log
     verifiedCanLinkAccount: true,
     customizeSync: false,
   });
 });
 
 add_task(async function test_helpers_login_with_customize_sync() {
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
-      setSignedInUser(accountData) {
-        return new Promise(resolve => {
-          // ensure fxAccounts is informed of the new user being signed in.
-          Assert.equal(accountData.email, "testuser@testuser.com");
+      _internal: {
+        setSignedInUser(accountData) {
+          return new Promise(resolve => {
+            // ensure fxAccounts is informed of the new user being signed in.
+            Assert.equal(accountData.email, "testuser@testuser.com");
 
-          // customizeSync should be stripped in the data.
-          Assert.equal(false, "customizeSync" in accountData);
+            // customizeSync should be stripped in the data.
+            Assert.equal(false, "customizeSync" in accountData);
 
-          resolve();
-        });
+            resolve();
+          });
+        },
       },
     },
   });
 
   await helpers.login({
     email: "testuser@testuser.com",
     verifiedCanLinkAccount: true,
     customizeSync: true,
   });
 });
 
 add_task(
   async function test_helpers_login_with_customize_sync_and_declined_engines() {
     let helpers = new FxAccountsWebChannelHelpers({
       fxAccounts: {
-        setSignedInUser(accountData) {
-          return new Promise(resolve => {
-            // ensure fxAccounts is informed of the new user being signed in.
-            Assert.equal(accountData.email, "testuser@testuser.com");
+        _internal: {
+          setSignedInUser(accountData) {
+            return new Promise(resolve => {
+              // ensure fxAccounts is informed of the new user being signed in.
+              Assert.equal(accountData.email, "testuser@testuser.com");
 
-            // customizeSync should be stripped in the data.
-            Assert.equal(false, "customizeSync" in accountData);
-            Assert.equal(false, "declinedSyncEngines" in accountData);
-            Assert.equal(
-              Services.prefs.getBoolPref("services.sync.engine.addons"),
-              false
-            );
-            Assert.equal(
-              Services.prefs.getBoolPref("services.sync.engine.bookmarks"),
-              true
-            );
-            Assert.equal(
-              Services.prefs.getBoolPref("services.sync.engine.history"),
-              true
-            );
-            Assert.equal(
-              Services.prefs.getBoolPref("services.sync.engine.passwords"),
-              true
-            );
-            Assert.equal(
-              Services.prefs.getBoolPref("services.sync.engine.prefs"),
-              false
-            );
-            Assert.equal(
-              Services.prefs.getBoolPref("services.sync.engine.tabs"),
-              true
-            );
-
-            resolve();
-          });
+              // customizeSync should be stripped in the data.
+              Assert.equal(false, "customizeSync" in accountData);
+              Assert.equal(false, "declinedSyncEngines" in accountData);
+              Assert.equal(
+                Services.prefs.getBoolPref("services.sync.engine.addons"),
+                false
+              );
+              Assert.equal(
+                Services.prefs.getBoolPref("services.sync.engine.bookmarks"),
+                true
+              );
+              Assert.equal(
+                Services.prefs.getBoolPref("services.sync.engine.history"),
+                true
+              );
+              Assert.equal(
+                Services.prefs.getBoolPref("services.sync.engine.passwords"),
+                true
+              );
+              Assert.equal(
+                Services.prefs.getBoolPref("services.sync.engine.prefs"),
+                false
+              );
+              Assert.equal(
+                Services.prefs.getBoolPref("services.sync.engine.tabs"),
+                true
+              );
+              resolve();
+            });
+          },
         },
       },
     });
 
     Assert.equal(
       Services.prefs.getBoolPref("services.sync.engine.addons"),
       true
     );
@@ -496,18 +501,20 @@ add_task(
   }
 );
 
 add_task(async function test_helpers_login_with_offered_sync_engines() {
   let helpers;
   const setSignedInUserCalled = new Promise(resolve => {
     helpers = new FxAccountsWebChannelHelpers({
       fxAccounts: {
-        async setSignedInUser(accountData) {
-          resolve(accountData);
+        _internal: {
+          async setSignedInUser(accountData) {
+            resolve(accountData);
+          },
         },
       },
     });
   });
 
   Services.prefs.setBoolPref("services.sync.engine.creditcards", false);
   Services.prefs.setBoolPref("services.sync.engine.addresses", false);
 
@@ -902,35 +909,37 @@ add_task(async function test_helpers_isP
 
 add_task(async function test_helpers_change_password() {
   let wasCalled = {
     updateUserAccountData: false,
     updateDeviceRegistration: false,
   };
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
-      updateUserAccountData(credentials) {
-        return new Promise(resolve => {
-          Assert.ok(credentials.hasOwnProperty("email"));
-          Assert.ok(credentials.hasOwnProperty("uid"));
-          Assert.ok(credentials.hasOwnProperty("unwrapBKey"));
-          Assert.ok(credentials.hasOwnProperty("device"));
-          Assert.equal(null, credentials.device);
-          // "foo" isn't a field known by storage, so should be dropped.
-          Assert.ok(!credentials.hasOwnProperty("foo"));
-          wasCalled.updateUserAccountData = true;
+      _internal: {
+        updateUserAccountData(credentials) {
+          return new Promise(resolve => {
+            Assert.ok(credentials.hasOwnProperty("email"));
+            Assert.ok(credentials.hasOwnProperty("uid"));
+            Assert.ok(credentials.hasOwnProperty("unwrapBKey"));
+            Assert.ok(credentials.hasOwnProperty("device"));
+            Assert.equal(null, credentials.device);
+            // "foo" isn't a field known by storage, so should be dropped.
+            Assert.ok(!credentials.hasOwnProperty("foo"));
+            wasCalled.updateUserAccountData = true;
 
-          resolve();
-        });
-      },
+            resolve();
+          });
+        },
 
-      updateDeviceRegistration() {
-        Assert.equal(arguments.length, 0);
-        wasCalled.updateDeviceRegistration = true;
-        return Promise.resolve();
+        updateDeviceRegistration() {
+          Assert.equal(arguments.length, 0);
+          wasCalled.updateDeviceRegistration = true;
+          return Promise.resolve();
+        },
       },
     },
   });
   await helpers.changePassword({
     email: "email",
     uid: "uid",
     unwrapBKey: "unwrapBKey",
     foo: "foo",
@@ -941,24 +950,26 @@ add_task(async function test_helpers_cha
 
 add_task(async function test_helpers_change_password_with_error() {
   let wasCalled = {
     updateUserAccountData: false,
     updateDeviceRegistration: false,
   };
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
-      updateUserAccountData() {
-        wasCalled.updateUserAccountData = true;
-        return Promise.reject();
-      },
+      _internal: {
+        updateUserAccountData() {
+          wasCalled.updateUserAccountData = true;
+          return Promise.reject();
+        },
 
-      updateDeviceRegistration() {
-        wasCalled.updateDeviceRegistration = true;
-        return Promise.resolve();
+        updateDeviceRegistration() {
+          wasCalled.updateDeviceRegistration = true;
+          return Promise.resolve();
+        },
       },
     },
   });
   try {
     await helpers.changePassword({});
     Assert.equal(false, "changePassword should have rejected");
   } catch (_) {
     Assert.ok(wasCalled.updateUserAccountData);
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -201,17 +201,17 @@ var configureFxAccountIdentity = functio
   };
   MockFxAccountsClient.prototype = {
     __proto__: FxAccountsClient.prototype,
     accountStatus() {
       return Promise.resolve(true);
     },
   };
   let mockFxAClient = new MockFxAccountsClient();
-  fxa.internal._fxAccountsClient = mockFxAClient;
+  fxa._internal._fxAccountsClient = mockFxAClient;
 
   let mockTSC = {
     // TokenServerClient
     async getTokenFromBrowserIDAssertion(uri, assertion) {
       Assert.equal(
         uri,
         Services.prefs.getStringPref("identity.sync.tokenserver.uri")
       );
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -319,21 +319,21 @@ this.BrowserIDManager.prototype = {
         break;
     }
   },
 
   /**
    * Provide override point for testing token expiration.
    */
   _now() {
-    return this._fxaService.now();
+    return this._fxaService._internal.now();
   },
 
   get _localtimeOffsetMsec() {
-    return this._fxaService.localtimeOffsetMsec;
+    return this._fxaService._internal.localtimeOffsetMsec;
   },
 
   get syncKeyBundle() {
     return this._syncKeyBundle;
   },
 
   get username() {
     return this._username;
@@ -359,17 +359,19 @@ this.BrowserIDManager.prototype = {
 
     // If we change the username, we interpret this as a major change event
     // and wipe out the credentials.
     this._log.info("Username changed. Removing stored credentials.");
     this.resetCredentials();
   },
 
   /**
-   * Resets/Drops all credentials we hold for the current user.
+   * Resets all calculated credentials we hold for the current user. This will
+   * *not* force the user to reauthenticate, but instead will force us to
+   * calculate a new key bundle, fetch a new token, etc.
    */
   resetCredentials() {
     this._syncKeyBundle = null;
     this._token = null;
     this._hashedUID = null;
     // The cluster URL comes from the token, so resetting it to empty will
     // force Sync to not accidentally use a value from an earlier token.
     Weave.Service.clusterURL = null;
@@ -412,17 +414,17 @@ this.BrowserIDManager.prototype = {
     if (!data.verified) {
       // Treat not verified as if the user needs to re-auth, so the browser
       // UI reflects the state.
       log.debug("unlockAndVerifyAuthState has an unverified user");
       telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.NOTVERIFIED);
       return LOGIN_FAILED_LOGIN_REJECTED;
     }
     this._updateSignedInUser(data);
-    if (await this._fxaService.canGetKeys()) {
+    if (await this._fxaService.keys.canGetKeys()) {
       log.debug(
         "unlockAndVerifyAuthState already has (or can fetch) sync keys"
       );
       telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.SUCCESS);
       return STATUS_OK;
     }
     // so no keys - ensure MP unlocked.
     if (!Utils.ensureMPUnlocked()) {
@@ -434,17 +436,17 @@ this.BrowserIDManager.prototype = {
     }
     // now we are unlocked we must re-fetch the user data as we may now have
     // the details that were previously locked away.
     this._updateSignedInUser(await this._fxaService.getSignedInUser());
     // If we still can't get keys it probably means the user authenticated
     // without unlocking the MP or cleared the saved logins, so we've now
     // lost them - the user will need to reauth before continuing.
     let result;
-    if (await this._fxaService.canGetKeys()) {
+    if (await this._fxaService.keys.canGetKeys()) {
       telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.SUCCESS);
       result = STATUS_OK;
     } else {
       telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.REJECTED);
       result = LOGIN_FAILED_LOGIN_REJECTED;
     }
     log.debug(
       "unlockAndVerifyAuthState re-fetched credentials and is returning",
@@ -494,57 +496,57 @@ this.BrowserIDManager.prototype = {
     // gotta be verified to fetch a token.
     if (!this._signedInUser.verified) {
       throw new Error("User is not verified");
     }
 
     // We need keys for things to work.  If we don't have them, just
     // return null for the token - sync calling unlockAndVerifyAuthState()
     // before actually syncing will setup the error states if necessary.
-    if (!(await this._fxaService.canGetKeys())) {
+    if (!(await this._fxaService.keys.canGetKeys())) {
       this._log.info(
         "Unable to fetch keys (master-password locked?), so aborting token fetch"
       );
       throw new Error("Can't fetch a token as we can't get keys");
     }
 
     // Do the assertion/certificate/token dance, with a retry.
     let getToken = async () => {
       this._log.info("Getting an assertion from", this._tokenServerUrl);
       const audience = Services.io.newURI(this._tokenServerUrl).prePath;
-      const assertion = await this._fxaService.getAssertion(audience);
+      const assertion = await this._fxaService._internal.getAssertion(audience);
 
       this._log.debug("Getting a token");
       const headers = { "X-Client-State": this._signedInUser.kXCS };
       const token = await this._tokenServerClient.getTokenFromBrowserIDAssertion(
         this._tokenServerUrl,
         assertion,
         headers
       );
       this._log.trace("Successfully got a token");
       return token;
     };
 
     let token;
     try {
       try {
         this._log.info("Getting keys");
-        this._updateSignedInUser(await this._fxaService.getKeys()); // throws if the user changed.
+        this._updateSignedInUser(await this._fxaService.keys.getKeys()); // throws if the user changed.
 
         token = await getToken();
       } catch (err) {
         // If we get a 401 fetching the token it may be that our certificate
         // needs to be regenerated.
         if (!err.response || err.response.status !== 401) {
           throw err;
         }
         this._log.warn(
           "Token server returned 401, refreshing certificate and retrying token fetch"
         );
-        await this._fxaService.invalidateCertificate();
+        await this._fxaService.keys.invalidateCertificate();
         token = await getToken();
       }
       // TODO: Make it be only 80% of the duration, so refresh the token
       // before it actually expires. This is to avoid sync storage errors
       // otherwise, we may briefly enter a "needs reauthentication" state.
       // (XXX - the above may no longer be true - someone should check ;)
       token.expiration = this._now() + token.duration * 1000 * 0.8;
       if (!this._syncKeyBundle) {
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -58,23 +58,23 @@ function MockFxAccounts() {
     _now_is: Date.now(),
 
     now() {
       return this._now_is;
     },
 
     fxAccountsClient: new MockFxAccountsClient(),
   });
-  fxa.internal.currentAccountState.getCertificate = function(
+  fxa._internal.currentAccountState.getCertificate = function(
     data,
     keyPair,
     mustBeValidUntil
   ) {
     this.cert = {
-      validUntil: fxa.internal.now() + CERT_LIFETIME,
+      validUntil: fxa._internal.now() + CERT_LIFETIME,
       cert: "certificate",
     };
     return Promise.resolve(this.cert.cert);
   };
   return fxa;
 }
 
 add_test(function test_initial_state() {
@@ -99,17 +99,17 @@ add_task(async function test_initialiali
   var browseridManager = new BrowserIDManager();
 
   // Use the real `_getAssertion` method that calls
   // `mockFxAClient.signCertificate`.
   let fxaInternal = makeFxAccountsInternalMock(identityConfig);
   delete fxaInternal._getAssertion;
 
   configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
-  browseridManager._fxaService.internal.initialize();
+  browseridManager._fxaService._internal.initialize();
 
   let signCertificateCalled = false;
   let accountStatusCalled = false;
 
   let AuthErrorMockFxAClient = function() {
     FxAccountsClient.apply(this);
   };
   AuthErrorMockFxAClient.prototype = {
@@ -123,17 +123,17 @@ add_task(async function test_initialiali
     },
     accountStatus() {
       accountStatusCalled = true;
       return Promise.resolve(false);
     },
   };
 
   let mockFxAClient = new AuthErrorMockFxAClient();
-  browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
+  browseridManager._fxaService._internal._fxAccountsClient = mockFxAClient;
 
   await Assert.rejects(
     browseridManager._ensureValidToken(),
     AuthenticationError,
     "should reject due to an auth error"
   );
 
   Assert.ok(signCertificateCalled);
@@ -206,25 +206,25 @@ add_task(async function test_resourceAut
 
   // Mocks within mocks...
   configureFxAccountIdentity(
     browseridManager,
     globalIdentityConfig,
     fxaInternal
   );
 
-  Assert.equal(browseridManager._fxaService.internal.now(), now);
+  Assert.equal(browseridManager._fxaService._internal.now(), now);
   Assert.equal(
-    browseridManager._fxaService.internal.localtimeOffsetMsec,
+    browseridManager._fxaService._internal.localtimeOffsetMsec,
     localtimeOffsetMsec
   );
 
-  Assert.equal(browseridManager._fxaService.now(), now);
+  Assert.equal(browseridManager._fxaService._internal.now(), now);
   Assert.equal(
-    browseridManager._fxaService.localtimeOffsetMsec,
+    browseridManager._fxaService._internal.localtimeOffsetMsec,
     localtimeOffsetMsec
   );
 
   let request = new Resource("https://example.net/i/like/pie/");
   let authenticator = browseridManager.getResourceAuthenticator();
   let output = await authenticator(request, "GET");
   dump("output" + JSON.stringify(output));
   let authHeader = output.headers.authorization;
@@ -264,17 +264,17 @@ add_task(async function test_RESTResourc
   fxaInternal.fxAccountsClient = fxaClient;
 
   configureFxAccountIdentity(
     browseridManager,
     globalIdentityConfig,
     fxaInternal
   );
 
-  Assert.equal(browseridManager._fxaService.internal.now(), now);
+  Assert.equal(browseridManager._fxaService._internal.now(), now);
 
   let request = new Resource("https://example.net/i/like/pie/");
   let authenticator = browseridManager.getResourceAuthenticator();
   let output = await authenticator(request, "GET");
   dump("output" + JSON.stringify(output));
   let authHeader = output.headers.authorization;
   Assert.ok(authHeader.startsWith("Hawk"));
 
@@ -288,25 +288,25 @@ add_task(async function test_ensureLogge
   configureFxAccountIdentity(globalBrowseridManager);
   await globalBrowseridManager._ensureValidToken();
   Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
   Assert.ok(globalBrowseridManager._token);
 
   // arrange for no logged in user.
   let fxa = globalBrowseridManager._fxaService;
   let signedInUser =
-    fxa.internal.currentAccountState.storageManager.accountData;
-  fxa.internal.currentAccountState.storageManager.accountData = null;
+    fxa._internal.currentAccountState.storageManager.accountData;
+  fxa._internal.currentAccountState.storageManager.accountData = null;
   await Assert.rejects(
     globalBrowseridManager._ensureValidToken(true),
     /Can't possibly get keys; User is not signed in/,
     "expecting rejection due to no user"
   );
   // Restore the logged in user to what it was.
-  fxa.internal.currentAccountState.storageManager.accountData = signedInUser;
+  fxa._internal.currentAccountState.storageManager.accountData = signedInUser;
   Status.login = LOGIN_FAILED_LOGIN_REJECTED;
   await globalBrowseridManager._ensureValidToken();
   Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
 });
 
 add_task(async function test_syncState() {
   // Avoid polling for an unverified user.
   let identityConfig = makeIdentityConfig();
@@ -316,26 +316,26 @@ add_task(async function test_syncState()
     globalBrowseridManager,
     globalIdentityConfig,
     fxaInternal
   );
 
   // arrange for no logged in user.
   let fxa = globalBrowseridManager._fxaService;
   let signedInUser =
-    fxa.internal.currentAccountState.storageManager.accountData;
-  fxa.internal.currentAccountState.storageManager.accountData = null;
+    fxa._internal.currentAccountState.storageManager.accountData;
+  fxa._internal.currentAccountState.storageManager.accountData = null;
   await Assert.rejects(
     globalBrowseridManager._ensureValidToken(true),
     /Can't possibly get keys; User is not signed in/,
     "expecting rejection due to no user"
   );
   // Restore to an unverified user.
   signedInUser.verified = false;
-  fxa.internal.currentAccountState.storageManager.accountData = signedInUser;
+  fxa._internal.currentAccountState.storageManager.accountData = signedInUser;
   Status.login = LOGIN_FAILED_LOGIN_REJECTED;
   // The browserid_identity observers are async, so call them directly.
   await globalBrowseridManager.observe(null, ONLOGIN_NOTIFICATION, "");
   Assert.equal(
     Status.login,
     LOGIN_FAILED_LOGIN_REJECTED,
     "should not have changed the login state for an unverified user"
   );
@@ -423,32 +423,32 @@ add_task(async function test_refreshCert
   _("BrowserIDManager refreshes the FXA certificate after a 401.");
   var identityConfig = makeIdentityConfig();
   var browseridManager = new BrowserIDManager();
   // Use the real `_getAssertion` method that calls
   // `mockFxAClient.signCertificate`.
   let fxaInternal = makeFxAccountsInternalMock(identityConfig);
   delete fxaInternal._getAssertion;
   configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
-  browseridManager._fxaService.internal.initialize();
+  browseridManager._fxaService._internal.initialize();
 
   let getCertCount = 0;
 
   let CheckSignMockFxAClient = function() {
     FxAccountsClient.apply(this);
   };
   CheckSignMockFxAClient.prototype = {
     __proto__: FxAccountsClient.prototype,
     signCertificate() {
       ++getCertCount;
     },
   };
 
   let mockFxAClient = new CheckSignMockFxAClient();
-  browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
+  browseridManager._fxaService._internal._fxAccountsClient = mockFxAClient;
 
   let didReturn401 = false;
   let didReturn200 = false;
   let mockTSC = mockTokenServer(() => {
     if (getCertCount <= 1) {
       didReturn401 = true;
       return {
         status: 401,
@@ -728,42 +728,45 @@ add_task(async function test_getKeysMiss
   delete identityConfig.fxaccount.user.kSync;
   delete identityConfig.fxaccount.user.kXCS;
   delete identityConfig.fxaccount.user.kExtSync;
   delete identityConfig.fxaccount.user.kExtKbHash;
   identityConfig.fxaccount.user.keyFetchToken = "keyFetchToken";
 
   configureFxAccountIdentity(browseridManager, identityConfig);
 
-  // Mock a fxAccounts object that returns no keys
+  // Mock a fxAccounts object
   let fxa = new FxAccounts({
-    fetchAndUnwrapKeys() {
-      return Promise.resolve({});
-    },
     fxAccountsClient: new MockFxAccountsClient(),
     newAccountState(credentials) {
       // We only expect this to be called with null indicating the (mock)
       // storage should be read.
       if (credentials) {
         throw new Error("Not expecting to have credentials passed");
       }
       let storageManager = new MockFxaStorageManager();
       storageManager.initialize(identityConfig.fxaccount.user);
       return new AccountState(storageManager);
     },
+    // And the keys object with a mock that returns no keys.
+    keys: {
+      fetchAndUnwrapKeys() {
+        return Promise.resolve({});
+      },
+    },
   });
 
   // Add a mock to the currentAccountState object.
-  fxa.internal.currentAccountState.getCertificate = function(
+  fxa._internal.currentAccountState.getCertificate = function(
     data,
     keyPair,
     mustBeValidUntil
   ) {
     this.cert = {
-      validUntil: fxa.internal.now() + CERT_LIFETIME,
+      validUntil: fxa._internal.now() + CERT_LIFETIME,
       cert: "certificate",
     };
     return Promise.resolve(this.cert.cert);
   };
 
   browseridManager._fxaService = fxa;
 
   await Assert.rejects(
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -561,24 +561,29 @@ add_task(async function test_autoconnect
 
   let origEnsureMPUnlocked = Utils.ensureMPUnlocked;
   Utils.ensureMPUnlocked = () => {
     _("Faking Master Password entry cancelation.");
     return false;
   };
   let origFxA = Service.identity._fxaService;
   Service.identity._fxaService = new FxAccounts({
-    canGetKeys() {
-      return false;
+    currentAccountState: {
+      getUserAccountData(...args) {
+        return origFxA._internal.currentAccountState.getUserAccountData(
+          ...args
+        );
+      },
     },
-    getSignedInUser() {
-      return origFxA.getSignedInUser();
+    keys: {
+      canGetKeys() {
+        return false;
+      },
     },
   });
-
   // A locked master password will still trigger a sync, but then we'll hit
   // MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL.
   let promiseObserved = promiseOneObserver("weave:service:login:error");
 
   scheduler.delayedAutoConnect(0);
   await promiseObserved;
 
   await Async.promiseYield();
--- a/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
+++ b/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
@@ -202,17 +202,17 @@ var Authentication = {
       await FxAccountsConfig.ensureConfigured();
 
       let client = new FxAccountsClient();
       let credentials = await client.signIn(
         account.username,
         account.password,
         true
       );
-      await fxAccounts.setSignedInUser(credentials);
+      await fxAccounts._internal.setSignedInUser(credentials);
       if (!credentials.verified) {
         await this._completeVerification(account.username);
       }
 
       return true;
     } catch (error) {
       throw new Error("signIn() failed with: " + error.message);
     }