Bug 949526 - FxAccountsManager and B2G implementation. Part 1: FxAccountsManager. r=markh
--- 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']