Bug 909967 - Introduce a Firefox Accounts module for storing/retrieving user credentials. r=gavin,gps
authorZach Carter <zcarter@mozilla.com>
Mon, 23 Sep 2013 18:35:37 -0700
changeset 161438 cdfd1aa2a0e8b310ddd639a95cc288396a4e3c2d
parent 161437 50dd2514c44b1d0508bffcec2b25af78e21458c3
child 161439 e73c642fdb2befe3e73048d62768806cb5f6584e
push idunknown
push userunknown
push dateunknown
reviewersgavin, gps
bugs909967
milestone27.0a1
Bug 909967 - Introduce a Firefox Accounts module for storing/retrieving user credentials. r=gavin,gps
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/moz.build
services/fxaccounts/tests/moz.build
services/fxaccounts/tests/xpcshell/head.js
services/fxaccounts/tests/xpcshell/test_accounts.js
services/fxaccounts/tests/xpcshell/xpcshell.ini
services/moz.build
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -0,0 +1,144 @@
+/* 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/. */
+
+
+this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/osfile.jsm")
+Cu.import("resource://services-common/utils.js");
+
+const defaultBaseDir = OS.Path.join(OS.Constants.Path.profileDir);
+const defaultStorageOptions = {
+  filename: 'signedInUser.json',
+  baseDir: defaultBaseDir,
+};
+
+/**
+ * FxAccounts constructor
+ *
+ * @param signedInUserStorage is a storage instance for getting/setting
+ *                            the signedInUser. Uses JSONStorage by default.
+ * @return instance
+ */
+function FxAccounts(signedInUserStorage = new JSONStorage(defaultStorageOptions)) {
+  this._signedInUserStorage = signedInUserStorage;
+}
+
+FxAccounts.prototype = Object.freeze({
+  // data format version
+  version: 1,
+
+  /**
+   * Set the current user signed in to Firefox Accounts (FxA)
+   *
+   * @param credentials
+   *        The credentials object obtained by logging in or creating
+   *        an account on the FxA server:
+   *
+   *        {
+   *          email: The users email address
+   *          uid: The user's unique id
+   *          sessionToken: Session for the FxA server
+   *          assertion: A Persona assertion used to enable Sync
+   *          kA: An encryption key from the FxA server
+   *          kB: An encryption key derived from the user's FxA password
+   *        }
+   *
+   * @return Promise
+   *         The promise resolves to null on success or is rejected on error
+   */
+  setSignedInUser: function setSignedInUser(credentials) {
+    let record = { version: this.version, accountData: credentials };
+    // cache a clone of the credentials object
+    this._signedInUser = JSON.parse(JSON.stringify(record));
+
+    return this._signedInUserStorage.set(record);
+  },
+
+  /**
+   * Get the user currently signed in to Firefox Accounts (FxA)
+   *
+   * @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
+   *          assertion: A Persona assertion used to enable Sync
+   *          kA: An encryption key from the FxA server
+   *          kB: An encryption key derived from the user's FxA password
+   *        }
+   *
+   *        or null if no user is signed in or the user data is an
+   *        unrecognized version.
+   *
+   */
+  getSignedInUser: function getSignedInUser() {
+    // skip disk if user is cached
+    if (typeof this._signedInUser !== 'undefined') {
+      let deferred = Promise.defer();
+      let result = this._signedInUser ? this._signedInUser.accountData : undefined;
+      deferred.resolve(result);
+      return deferred.promise;
+    }
+
+    return this._signedInUserStorage.get()
+      .then((user) => {
+          if (user.version === this.version) {
+            this._signedInUser = user;
+            return user.accountData;
+          }
+        },
+        (err) => undefined);
+  },
+
+  /**
+   * Sign the current user out
+   *
+   * @return Promise
+   *         The promise is rejected if a storage error occurs
+   */
+  signOut: function signOut() {
+    this._signedInUser = {};
+    return this._signedInUserStorage.set(null);
+  },
+});
+
+
+
+/**
+ * JSONStorage constructor that creates instances that may set/get
+ * to a specified file, in a directory that will be created if it
+ * doesn't exist.
+ *
+ * @param options {
+ *                  filename: of the file to write to
+ *                  baseDir: directory where the file resides
+ *                }
+ * @return instance
+ */
+function JSONStorage(options) {
+  this.baseDir = options.baseDir;
+  this.path = OS.Path.join(options.baseDir, options.filename);
+}
+
+JSONStorage.prototype = Object.freeze({
+  set: function (contents) {
+    return OS.File.makeDir(this.baseDir, {ignoreExisting: true})
+      .then(CommonUtils.writeJSON.bind(null, contents, this.path));
+  },
+
+  get: function () {
+    return CommonUtils.readJSON(this.path);
+  },
+});
+
+
+// create an instance to export
+this.fxAccounts = new FxAccounts();
+
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += ['tests']
+EXTRA_JS_MODULES += ['FxAccounts.jsm']
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['xpcshell/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/head.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+(function initFxAccountsTestingInfrastructure() {
+  do_get_profile();
+
+  let ns = {};
+  Components.utils.import("resource://testing-common/services-common/logging.js",
+                          ns);
+
+  ns.initTestLogging("Trace");
+}).call(this);
+
+
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+
+function run_test() {
+  run_next_test();
+}
+
+let credentials = {
+  email: "foo@example.com",
+  uid: "1234@lcip.org",
+  assertion: "foobar",
+  sessionToken: "dead",
+  kA: "beef",
+  kB: "cafe"
+};
+
+add_task(function test_get_signed_in_user_initially_unset() {
+  // user is initially undefined
+  let result = yield fxAccounts.getSignedInUser();
+  do_check_eq(result, undefined);
+
+  // set user
+  yield fxAccounts.setSignedInUser(credentials);
+
+  // get user
+  let result = yield fxAccounts.getSignedInUser();
+  do_check_eq(result.email, credentials.email);
+  do_check_eq(result.assertion, credentials.assertion);
+  do_check_eq(result.kB, credentials.kB);
+
+  // Delete the memory cache and force the user
+  // to be read and parsed from storage (e.g. disk via JSONStorage)
+  delete fxAccounts._signedInUser;
+  let result = yield fxAccounts.getSignedInUser();
+  do_check_eq(result.email, credentials.email);
+  do_check_eq(result.assertion, credentials.assertion);
+  do_check_eq(result.kB, credentials.kB);
+
+  // sign out
+  yield fxAccounts.signOut();
+
+  // user should be undefined after sign out
+  let result = yield fxAccounts.getSignedInUser();
+  do_check_eq(result, undefined);
+
+  run_next_test();
+});
+
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = head.js
+tail =
+
+[test_accounts.js]
+
--- a/services/moz.build
+++ b/services/moz.build
@@ -2,16 +2,17 @@
 # vim: set filetype=python:
 # 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/.
 
 PARALLEL_DIRS += [
     'common',
     'crypto',
+    'fxaccounts'
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
     # MOZ_SERVICES_HEALTHREPORT and therefore MOZ_DATA_REPORTING are
     # defined on Android, but these features are implemented using Java.
     if CONFIG['MOZ_SERVICES_HEALTHREPORT']:
         PARALLEL_DIRS += ['healthreport']