Bug 949526 - FxAccountsManager and B2G implementation. Part 1: FxAccountsManager. r=markh
authorFernando Jiménez <ferjmoreno@gmail.com>
Fri, 13 Dec 2013 12:25:26 +0100
changeset 160321 bcc13b8db10c9a3ec2ea3153ba737af7d4af149b
parent 160320 5207b6d4938c6837a9eb71cef0467b17a9739395
child 160322 cc7892e78f65ac2c56cb642b8ac65a9b76808f03
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmarkh
bugs949526
milestone29.0a1
Bug 949526 - FxAccountsManager and B2G implementation. Part 1: FxAccountsManager. r=markh
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsConsts.js
services/fxaccounts/FxAccountsManager.jsm
services/fxaccounts/moz.build
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -11,28 +11,21 @@ Cu.import("resource://gre/modules/Promis
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://gre/modules/FxAccountsConsts.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
                                   "resource://gre/modules/identity/jwcrypto.jsm");
 
-const DATA_FORMAT_VERSION = 1;
-const DEFAULT_STORAGE_FILENAME = "signedInUser.json";
-const ASSERTION_LIFETIME = 1000 * 60 * 5; // 5 minutes
-const KEY_LIFETIME = 1000 * 3600 * 12;    // 12 hours
-const CERT_LIFETIME = 1000 * 3600 * 6;    // 6 hours
-const POLL_SESSION = 1000 * 60 * 5;       // 5 minutes
-const POLL_STEP = 1000 * 3;               // 3 seconds
-
 // loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
 // "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
 // default.
 const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
 try {
   this.LOG_LEVEL =
     Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
     && Services.prefs.getCharPref(PREF_LOG_LEVEL);
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsConsts.js
@@ -0,0 +1,85 @@
+/* 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.DATA_FORMAT_VERSION = 1;
+this.DEFAULT_STORAGE_FILENAME = "signedInUser.json";
+
+// Token life times.
+this.ASSERTION_LIFETIME = 1000 * 60 * 5;    // 5 minutes
+this.CERT_LIFETIME      = 1000 * 3600 * 6;  // 6 hours
+this.KEY_LIFETIME       = 1000 * 3600 * 12; // 12 hours
+
+// Polling timings.
+this.POLL_SESSION       = 1000 * 60 * 5;    // 5 minutes
+this.POLL_STEP          = 1000 * 3;         // 3 seconds
+
+// Server errno.
+// From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format
+this.ERRNO_ACCOUNT_ALREADY_EXISTS     = 101;
+this.ERRNO_ACCOUNT_DOES_NOT_EXISTS    = 102;
+this.ERRNO_INCORRECT_PASSWORD         = 103;
+this.ERRNO_UNVERIFIED_ACCOUNT         = 104;
+this.ERRNO_INVALID_VERIFICATION_CODE  = 105;
+this.ERRNO_NOT_VALID_JSON_BODY        = 106;
+this.ERRNO_INVALID_BODY_PARAMETERS    = 107;
+this.ERRNO_MISSING_BODY_PARAMETERS    = 108;
+this.ERRNO_INVALID_REQUEST_SIGNATURE  = 109;
+this.ERRNO_INVALID_AUTH_TOKEN         = 110;
+this.ERRNO_INVALID_AUTH_TIMESTAMP     = 111;
+this.ERRNO_MISSING_CONTENT_LENGTH     = 112;
+this.ERRNO_REQUEST_BODY_TOO_LARGE     = 113;
+this.ERRNO_TOO_MANY_CLIENT_REQUESTS   = 114;
+this.ERRNO_INVALID_AUTH_NONCE         = 115;
+this.ERRNO_SERVICE_TEMP_UNAVAILABLE   = 201;
+this.ERRNO_UNKNOWN_ERROR              = 999;
+
+// Errors.
+this.ERROR_ACCOUNT_ALREADY_EXISTS     = "ACCOUNT_ALREADY_EXISTS";
+this.ERROR_ACCOUNT_DOES_NOT_EXISTS    = "ACCOUNT_DOES_NOT_EXISTS";
+this.ERROR_ALREADY_SIGNED_IN_USER     = "ALREADY_SIGNED_IN_USER";
+this.ERROR_INVALID_ACCOUNTID          = "INVALID_ACCOUNTID";
+this.ERROR_INVALID_AUDIENCE           = "INVALID_AUDIENCE";
+this.ERROR_INVALID_AUTH_TOKEN         = "INVALID_AUTH_TOKEN";
+this.ERROR_INVALID_AUTH_TIMESTAMP     = "INVALID_AUTH_TIMESTAMP";
+this.ERROR_INVALID_AUTH_NONCE         = "INVALID_AUTH_NONCE";
+this.ERROR_INVALID_BODY_PARAMETERS    = "INVALID_BODY_PARAMETERS";
+this.ERROR_INVALID_PASSWORD           = "INVALID_PASSWORD";
+this.ERROR_INVALID_VERIFICATION_CODE  = "INVALID_VERIFICATION_CODE";
+this.ERROR_INVALID_REQUEST_SIGNATURE  = "INVALID_REQUEST_SIGNATURE";
+this.ERROR_INTERNAL_INVALID_USER      = "INTERNAL_ERROR_INVALID_USER";
+this.ERROR_MISSING_BODY_PARAMETERS    = "MISSING_BODY_PARAMETERS";
+this.ERROR_MISSING_CONTENT_LENGTH     = "MISSING_CONTENT_LENGTH";
+this.ERROR_NO_TOKEN_SESSION           = "NO_TOKEN_SESSION";
+this.ERROR_NOT_VALID_JSON_BODY        = "NOT_VALID_JSON_BODY";
+this.ERROR_OFFLINE                    = "OFFLINE";
+this.ERROR_REQUEST_BODY_TOO_LARGE     = "REQUEST_BODY_TOO_LARGE";
+this.ERROR_SERVER_ERROR               = "SERVER_ERROR";
+this.ERROR_TOO_MANY_CLIENT_REQUESTS   = "TOO_MANY_CLIENT_REQUESTS";
+this.ERROR_SERVICE_TEMP_UNAVAILABLE   = "SERVICE_TEMPORARY_UNAVAILABLE";
+this.ERROR_UI_ERROR                   = "UI_ERROR";
+this.ERROR_UNKNOWN                    = "UNKNOWN_ERROR";
+this.ERROR_UNVERIFIED_ACCOUNT         = "UNVERIFIED_ACCOUNT";
+
+// Error matching.
+this.SERVER_ERRNO_TO_ERROR = {};
+SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_ALREADY_EXISTS]     = ERROR_ACCOUNT_ALREADY_EXISTS;
+SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_DOES_NOT_EXISTS]    = ERROR_ACCOUNT_DOES_NOT_EXISTS;
+SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_PASSWORD]         = ERROR_INVALID_PASSWORD;
+SERVER_ERRNO_TO_ERROR[ERRNO_UNVERIFIED_ACCOUNT]         = ERROR_UNVERIFIED_ACCOUNT;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_VERIFICATION_CODE]  = ERROR_INVALID_VERIFICATION_CODE;
+SERVER_ERRNO_TO_ERROR[ERRNO_NOT_VALID_JSON_BODY]        = ERROR_NOT_VALID_JSON_BODY;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_PARAMETERS]    = ERROR_INVALID_BODY_PARAMETERS;
+SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_BODY_PARAMETERS]    = ERROR_MISSING_BODY_PARAMETERS;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_SIGNATURE]  = ERROR_INVALID_REQUEST_SIGNATURE;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TOKEN]         = ERROR_INVALID_AUTH_TOKEN;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TIMESTAMP]     = ERROR_INVALID_AUTH_TIMESTAMP;
+SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_CONTENT_LENGTH]     = ERROR_MISSING_CONTENT_LENGTH;
+SERVER_ERRNO_TO_ERROR[ERRNO_REQUEST_BODY_TOO_LARGE]     = ERROR_REQUEST_BODY_TOO_LARGE;
+SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_CLIENT_REQUESTS]   = ERROR_TOO_MANY_CLIENT_REQUESTS;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_NONCE]         = ERROR_INVALID_AUTH_NONCE;
+SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMP_UNAVAILABLE]   = ERROR_SERVICE_TEMP_UNAVAILABLE;
+SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR]              = ERROR_UNKNOWN;
+
+// Allow this file to be imported via Components.utils.import().
+this.EXPORTED_SYMBOLS = Object.keys(this);
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsManager.jsm
@@ -0,0 +1,382 @@
+/* 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/. */
+
+/**
+ * Temporary abstraction layer for common Fx Accounts operations.
+ * For now, we will be using this module only from B2G but in the end we might
+ * want this to be merged with FxAccounts.jsm and let other products also use
+ * it.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FxAccountsManager"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/FxAccountsConsts.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient",
+  "resource://gre/modules/FxAccountsClient.jsm");
+
+this.FxAccountsManager = {
+
+  // We don't really need to save fxAccounts instance but this way we allow
+  // to mock FxAccounts from tests.
+  _fxAccounts: fxAccounts,
+
+  // We keep the session details here so consumers don't need to deal with
+  // session tokens and are only required to handle the email.
+  _activeSession: null,
+
+  // We only expose the email and the verified status so far.
+  get _user() {
+    if (!this._activeSession || !this._activeSession.email) {
+      return null;
+    }
+
+    return {
+      accountId: this._activeSession.email,
+      verified: this._activeSession.verified
+    }
+  },
+
+  _getError: function(aServerResponse) {
+    if (!aServerResponse || !aServerResponse.error || !aServerResponse.error.errno) {
+      return;
+    }
+    return SERVER_ERRNO_TO_ERROR[aServerResponse.error.errno];
+  },
+
+  _serverError: function(aServerResponse) {
+    let error = this._getError({ error: aServerResponse });
+    return Promise.reject({
+      error: error ? error : ERROR_SERVER_ERROR,
+      details: aServerResponse
+    });
+  },
+
+  // As we do with _fxAccounts, we don't really need this factory, but this way
+  // we allow tests to mock FxAccountsClient.
+  _createFxAccountsClient: function() {
+    return new FxAccountsClient();
+  },
+
+  _signInSignUp: function(aMethod, aAccountId, aPassword) {
+    if (Services.io.offline) {
+      return Promise.reject({
+        error: ERROR_OFFLINE
+      });
+    }
+
+    if (!aAccountId) {
+      return Promise.reject({
+        error: ERROR_INVALID_ACCOUNTID
+      });
+    }
+
+    if (!aPassword) {
+      return Promise.reject({
+        error: ERROR_INVALID_PASSWORD
+      });
+    }
+
+    // Check that there is no signed in account first.
+    if (this._activeSession) {
+      return Promise.reject({
+        error: ERROR_ALREADY_SIGNED_IN_USER,
+        details: {
+          user: this._user
+        }
+      });
+    }
+
+    let client = this._createFxAccountsClient();
+    return this._fxAccounts.getSignedInUser().then(
+      user => {
+        if (user) {
+          return Promise.reject({
+            error: ERROR_ALREADY_SIGNED_IN_USER,
+            details: {
+              user: user
+            }
+          });
+        }
+        return client[aMethod](aAccountId, aPassword);
+      }
+    ).then(
+      user => {
+        let error = this._getError(user);
+        if (!user || !user.uid || !user.sessionToken || error) {
+          return Promise.reject({
+            error: error ? error : ERROR_INTERNAL_INVALID_USER,
+            details: {
+              user: user
+            }
+          });
+        }
+
+        // Save the credentials of the signed in user.
+        user.email = aAccountId;
+        return this._fxAccounts.setSignedInUser(user, false).then(
+          () => {
+            this._activeSession = user;
+            return Promise.resolve({
+              accountCreated: aMethod === "signUp",
+              user: this._user
+            });
+          }
+        );
+      },
+      reason => { return this._serverError(reason); }
+    );
+  },
+
+  _getAssertion: function(aAudience) {
+    return this._fxAccounts.getAssertion(aAudience);
+  },
+
+  _signOut: function() {
+    if (!this._activeSession) {
+      return Promise.resolve();
+    }
+
+    return this._fxAccounts.signOut(this._activeSession.sessionToken).then(
+      () => {
+        // If there is no connection, removing the local session should be
+        // enough. The client can create new sessions up to the limit (100?).
+        // Orphaned tokens on the server will eventually be garbage collected.
+        if (Services.io.offline) {
+          this._activeSession = null;
+          return Promise.resolve();
+        }
+        // Otherwise, we try to remove the remote session.
+        let client = this._createFxAccountsClient();
+        return client.signOut(this._activeSession.sessionToken).then(
+          result => {
+            // Even if there is a remote server error, we remove the local
+            // session.
+            this._activeSession = null;
+            let error = this._getError(result);
+            if (error) {
+              return Promise.reject({
+                error: error,
+                details: result
+              });
+            }
+            return Promise.resolve();
+          },
+          reason => {
+            // Even if there is a remote server error, we remove the local
+            // session.
+            this._activeSession = null;
+            return this._serverError(reason);
+          }
+        );
+      }
+    );
+  },
+
+  // -- API --
+
+  signIn: function(aAccountId, aPassword) {
+    return this._signInSignUp("signIn", aAccountId, aPassword);
+  },
+
+  signUp: function(aAccountId, aPassword) {
+    return this._signInSignUp("signUp", aAccountId, aPassword);
+  },
+
+  signOut: function() {
+    if (!this._activeSession) {
+      // If there is no cached active session, we try to get it from the
+      // account storage.
+      return this.getAccount().then(
+        result => {
+          if (!result) {
+            return Promise.resolve();
+          }
+          return this._signOut();
+        }
+      );
+    }
+    return this._signOut();
+  },
+
+  getAccount: function() {
+    // We check first if we have session details cached.
+    if (this._activeSession) {
+      // If our cache says that the account is not yet verified, we check that
+      // this information is correct, and update the cached data if not.
+      if (this._activeSession && !this._activeSession.verified &&
+          !Services.io.offline) {
+        return this.verificationStatus(this._activeSession);
+      }
+
+      return Promise.resolve(this._user);
+    }
+
+    // If no cached information, we try to get it from the persistent storage.
+    return this._fxAccounts.getSignedInUser().then(
+      user => {
+        if (!user || !user.email) {
+          return Promise.resolve(null);
+        }
+
+        this._activeSession = user;
+        // If we get a stored information of a not yet verified account,
+        // we check this information with the server, update the stored
+        // data if needed and finally return the account details.
+        if (!user.verified && !Services.io.offline) {
+          return this.verificationStatus(user);
+        }
+
+        return Promise.resolve(this._user);
+      }
+    );
+  },
+
+  queryAccount: function(aAccountId) {
+    if (Services.io.offline) {
+      return Promise.reject({
+        error: ERROR_OFFLINE
+      });
+    }
+
+    let deferred = Promise.defer();
+
+    if (!aAccountId) {
+      return Promise.reject({
+        error: ERROR_INVALID_ACCOUNTID
+      });
+    }
+
+    let client = this._createFxAccountsClient();
+    return client.accountExists(aAccountId).then(
+      result => {
+        let error = this._getError(result);
+        if (error) {
+          return Promise.reject({
+            error: error,
+            details: result
+          });
+        }
+
+        return Promise.resolve({
+          registered: result
+        });
+      },
+      reason => { this._serverError(reason); }
+    );
+  },
+
+  verificationStatus: function() {
+    if (!this._activeSession || !this._activeSession.sessionToken) {
+      return Promise.reject({
+        error: ERROR_NO_TOKEN_SESSION
+      });
+    }
+
+    // There is no way to unverify an already verified account, so we just
+    // return the account details of a verified account
+    if (this._activeSession.verified) {
+      return Promise.resolve(this._user);
+    }
+
+    if (Services.io.offline) {
+      return Promise.reject({
+        error: ERROR_OFFLINE
+      });
+    }
+
+    let client = this._createFxAccountsClient();
+    return client.recoveryEmailStatus(this._activeSession.sessionToken).then(
+      data => {
+        let error = this._getError(data);
+        if (error) {
+          return Promise.reject({
+            error: error,
+            details: data
+          });
+        }
+
+        // If the verification status is different from the one that we have
+        // stored, we update it and return the session data. If not, we simply
+        // return the session data.
+        if (this._activeSession.verified != data.verified) {
+          this._activeSession.verified = data.verified;
+          return this._fxAccounts.setSignedInUser(this._activeSession).then(
+            () => {
+              return Promise.resolve(this._user);
+            }
+          );
+        }
+        return Promise.resolve(this._user);
+      },
+      reason => { return this._serverError(reason); }
+    );
+  },
+
+  getAssertion: function(aAudience) {
+    if (!aAudience) {
+      return Promise.reject({
+        error: ERROR_INVALID_AUDIENCE
+      });
+    }
+
+    if (Services.io.offline) {
+      return Promise.reject({
+        error: ERROR_OFFLINE
+      });
+    }
+
+    return this.getAccount().then(
+      user => {
+        if (user) {
+          // We cannot get assertions for unverified accounts.
+          if (user.verified) {
+            return this._getAssertion(aAudience);
+          }
+
+          return Promise.reject({
+            error: ERROR_UNVERIFIED_ACCOUNT,
+            details: {
+              user: user
+            }
+          });
+        }
+
+        // If there is no currently signed in user, we trigger the signIn UI
+        // flow.
+        let ui = Cc["@mozilla.org/fxaccounts/fxaccounts-ui-glue;1"]
+                   .createInstance(Ci.nsIFxAccountsUIGlue);
+        return ui.signInFlow().then(
+          result => {
+            // Even if we get a successful result from the UI, the account will
+            // most likely be unverified, so we cannot get an assertion.
+            if (result && result.verified) {
+              return this._getAssertion(aAudience);
+            }
+            return Promise.reject({
+              error: ERROR_UNVERIFIED_ACCOUNT,
+              details: {
+                user: result
+              }
+            });
+          },
+          error => {
+            return Promise.reject({
+              error: ERROR_UI_ERROR,
+              details: error
+            });
+          }
+        );
+      }
+    );
+  }
+};
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -1,11 +1,17 @@
 # -*- 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',
-  'FxAccountsClient.jsm'
+  'FxAccountsClient.jsm',
+  'FxAccountsConsts.js'
 ]
+
+# For now, we will only be using the FxA manager in B2G.
+if CONFIG['MOZ_B2G']:
+  EXTRA_JS_MODULES += ['FxAccountsManager.jsm']