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 177448 cdfd1aa2a0e8b310ddd639a95cc288396a4e3c2d
parent 177447 50dd2514c44b1d0508bffcec2b25af78e21458c3
child 177449 e73c642fdb2befe3e73048d62768806cb5f6584e
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgavin, gps
bugs909967
milestone27.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 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']