summaryrefslogtreecommitdiffstats
path: root/toolkit/identity/FirefoxAccounts.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/identity/FirefoxAccounts.jsm')
-rw-r--r--toolkit/identity/FirefoxAccounts.jsm320
1 files changed, 320 insertions, 0 deletions
diff --git a/toolkit/identity/FirefoxAccounts.jsm b/toolkit/identity/FirefoxAccounts.jsm
new file mode 100644
index 000000000..54efaf3b4
--- /dev/null
+++ b/toolkit/identity/FirefoxAccounts.jsm
@@ -0,0 +1,320 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["FirefoxAccounts"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
+ "resource://gre/modules/identity/IdentityUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
+ "resource://gre/modules/identity/IdentityUtils.jsm");
+
+// 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);
+} catch (e) {
+ this.LOG_LEVEL = Log.Level.Error;
+}
+
+var log = Log.repository.getLogger("Identity.FxAccounts");
+log.level = LOG_LEVEL;
+log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+
+#ifdef MOZ_B2G
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsManager",
+ "resource://gre/modules/FxAccountsManager.jsm",
+ "FxAccountsManager");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+#else
+log.warn("The FxAccountsManager is only functional in B2G at this time.");
+var FxAccountsManager = null;
+var ONVERIFIED_NOTIFICATION = null;
+var ONLOGIN_NOTIFICATION = null;
+var ONLOGOUT_NOTIFICATION = null;
+#endif
+
+function FxAccountsService() {
+ Services.obs.addObserver(this, "quit-application-granted", false);
+ if (ONVERIFIED_NOTIFICATION) {
+ Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, false);
+ Services.obs.addObserver(this, ONLOGIN_NOTIFICATION, false);
+ Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
+ }
+
+ // Maintain interface parity with Identity.jsm and MinimalIdentity.jsm
+ this.RP = this;
+
+ this._rpFlows = new Map();
+
+ // Enable us to mock FxAccountsManager service in testing
+ this.fxAccountsManager = FxAccountsManager;
+}
+
+FxAccountsService.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case null:
+ // Guard against matching null ON*_NOTIFICATION
+ break;
+ case ONVERIFIED_NOTIFICATION:
+ log.debug("Received " + ONVERIFIED_NOTIFICATION + "; firing request()s");
+ for (let [rpId,] of this._rpFlows) {
+ this.request(rpId);
+ }
+ break;
+ case ONLOGIN_NOTIFICATION:
+ log.debug("Received " + ONLOGIN_NOTIFICATION + "; doLogin()s fired");
+ for (let [rpId,] of this._rpFlows) {
+ this.request(rpId);
+ }
+ break;
+ case ONLOGOUT_NOTIFICATION:
+ log.debug("Received " + ONLOGOUT_NOTIFICATION + "; doLogout()s fired");
+ for (let [rpId,] of this._rpFlows) {
+ this.doLogout(rpId);
+ }
+ break;
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ if (ONVERIFIED_NOTIFICATION) {
+ Services.obs.removeObserver(this, ONVERIFIED_NOTIFICATION);
+ Services.obs.removeObserver(this, ONLOGIN_NOTIFICATION);
+ Services.obs.removeObserver(this, ONLOGOUT_NOTIFICATION);
+ }
+ break;
+ }
+ },
+
+ cleanupRPRequest: function(aRp) {
+ aRp.pendingRequest = false;
+ this._rpFlows.set(aRp.id, aRp);
+ },
+
+ /**
+ * Register a listener for a given windowID as a result of a call to
+ * navigator.id.watch().
+ *
+ * @param aRPCaller
+ * (Object) an object that represents the caller document, and
+ * is expected to have properties:
+ * - id (unique, e.g. uuid)
+ * - origin (string)
+ *
+ * and a bunch of callbacks
+ * - doReady()
+ * - doLogin()
+ * - doLogout()
+ * - doError()
+ * - doCancel()
+ *
+ */
+ watch: function watch(aRpCaller) {
+ this._rpFlows.set(aRpCaller.id, aRpCaller);
+ log.debug("watch: " + aRpCaller.id);
+ log.debug("Current rp flows: " + this._rpFlows.size);
+
+ // Log the user in, if possible, and then call ready().
+ let runnable = {
+ run: () => {
+ this.fxAccountsManager.getAssertion(aRpCaller.audience,
+ aRpCaller.principal,
+ { silent:true }).then(
+ data => {
+ if (data) {
+ this.doLogin(aRpCaller.id, data);
+ } else {
+ this.doLogout(aRpCaller.id);
+ }
+ this.doReady(aRpCaller.id);
+ },
+ error => {
+ log.error("get silent assertion failed: " + JSON.stringify(error));
+ this.doError(aRpCaller.id, error);
+ }
+ );
+ }
+ };
+ Services.tm.currentThread.dispatch(runnable,
+ Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ /**
+ * Delete the flow when the screen is unloaded
+ */
+ unwatch: function(aRpCallerId, aTargetMM) {
+ log.debug("unwatching: " + aRpCallerId);
+ this._rpFlows.delete(aRpCallerId);
+ },
+
+ /**
+ * Initiate a login with user interaction as a result of a call to
+ * navigator.id.request().
+ *
+ * @param aRPId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ * @param aOptions
+ * (Object) options including privacyPolicy, termsOfService
+ */
+ request: function request(aRPId, aOptions) {
+ aOptions = aOptions || {};
+ let rp = this._rpFlows.get(aRPId);
+ if (!rp) {
+ log.error("request() called before watch()");
+ return;
+ }
+
+ // We check if we already have a pending request for this RP and in that
+ // case we just bail out. We don't want duplicated onlogin or oncancel
+ // events.
+ if (rp.pendingRequest) {
+ log.debug("request() already called");
+ return;
+ }
+
+ // Otherwise, we set the RP flow with the pending request flag.
+ rp.pendingRequest = true;
+ this._rpFlows.set(rp.id, rp);
+
+ let options = makeMessageObject(rp);
+ objectCopy(aOptions, options);
+
+ log.debug("get assertion for " + rp.audience);
+
+ this.fxAccountsManager.getAssertion(rp.audience, rp.principal, options)
+ .then(
+ data => {
+ log.debug("got assertion for " + rp.audience + ": " + data);
+ this.doLogin(aRPId, data);
+ },
+ error => {
+ log.debug("get assertion failed: " + JSON.stringify(error));
+ // Cancellation is passed through an error channel; here we reroute.
+ if ((error.error && (error.error.details == "DIALOG_CLOSED_BY_USER")) ||
+ (error.details == "DIALOG_CLOSED_BY_USER")) {
+ return this.doCancel(aRPId);
+ }
+ this.doError(aRPId, error);
+ }
+ )
+ .then(
+ () => {
+ this.cleanupRPRequest(rp);
+ }
+ )
+ .catch(
+ () => {
+ this.cleanupRPRequest(rp);
+ }
+ );
+ },
+
+ /**
+ * Invoked when a user wishes to logout of a site (for instance, when clicking
+ * on an in-content logout button).
+ *
+ * @param aRpCallerId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ */
+ logout: function logout(aRpCallerId) {
+ // XXX Bug 945363 - Resolve the SSO story for FXA and implement
+ // logout accordingly.
+ //
+ // For now, it makes no sense to logout from a specific RP in
+ // Firefox Accounts, so just directly call the logout callback.
+ if (!this._rpFlows.has(aRpCallerId)) {
+ log.error("logout() called before watch()");
+ return;
+ }
+
+ // Call logout() on the next tick
+ let runnable = {
+ run: () => {
+ this.fxAccountsManager.signOut().then(() => {
+ this.doLogout(aRpCallerId);
+ });
+ }
+ };
+ Services.tm.currentThread.dispatch(runnable,
+ Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ childProcessShutdown: function childProcessShutdown(messageManager) {
+ for (let [key,] of this._rpFlows) {
+ if (this._rpFlows.get(key)._mm === messageManager) {
+ this._rpFlows.delete(key);
+ }
+ }
+ },
+
+ doLogin: function doLogin(aRpCallerId, aAssertion) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doLogin found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doLogin(aAssertion);
+ },
+
+ doLogout: function doLogout(aRpCallerId) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doLogout found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doLogout();
+ },
+
+ doReady: function doReady(aRpCallerId) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doReady found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doReady();
+ },
+
+ doCancel: function doCancel(aRpCallerId) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doCancel found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doCancel();
+ },
+
+ doError: function doError(aRpCallerId, aError) {
+ let rp = this._rpFlows.get(aRpCallerId);
+ if (!rp) {
+ log.warn("doError found no rp to go with callerId " + aRpCallerId);
+ return;
+ }
+
+ rp.doError(aError);
+ }
+};
+
+this.FirefoxAccounts = new FxAccountsService();
+