summaryrefslogtreecommitdiffstats
path: root/b2g/components/SignInToWebsite.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'b2g/components/SignInToWebsite.jsm')
-rw-r--r--b2g/components/SignInToWebsite.jsm444
1 files changed, 444 insertions, 0 deletions
diff --git a/b2g/components/SignInToWebsite.jsm b/b2g/components/SignInToWebsite.jsm
new file mode 100644
index 000000000..fd1349d46
--- /dev/null
+++ b/b2g/components/SignInToWebsite.jsm
@@ -0,0 +1,444 @@
+/* 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/. */
+
+/*
+ * SignInToWebsite.jsm - UX Controller and means for accessing identity
+ * cookies on behalf of relying parties.
+ *
+ * Currently, the b2g security architecture isolates web applications
+ * so that each window has access only to a local cookie jar:
+ *
+ * To prevent Web apps from interfering with one another, each one is
+ * hosted on a separate domain, and therefore may only access the
+ * resources associated with its domain. These resources include
+ * things such as IndexedDB databases, cookies, offline storage,
+ * and so forth.
+ *
+ * -- https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS/Security/Security_model
+ *
+ * As a result, an authentication system like Persona cannot share its
+ * cookie jar with multiple relying parties, and so would require a
+ * fresh login request in every window. This would not be a good
+ * experience.
+ *
+ *
+ * In order for navigator.id.request() to maintain state in a single
+ * cookie jar, we cause all Persona interactions to take place in a
+ * content context that is launched by the system application, with the
+ * result that Persona has a single cookie jar that all Relying
+ * Parties can use. Since of course those Relying Parties cannot
+ * reach into the system cookie jar, the Controller in this module
+ * provides a way to get messages and data to and fro between the
+ * Relying Party in its window context, and the Persona internal api
+ * in its context.
+ *
+ * On the Relying Party's side, say a web page invokes
+ * navigator.id.watch(), to register callbacks, and then
+ * navigator.id.request() to request an assertion. The navigator.id
+ * calls are provided by nsDOMIdentity. nsDOMIdentity messages down
+ * to the privileged DOMIdentity code (using cpmm and ppmm message
+ * managers). DOMIdentity stores the state of Relying Party flows
+ * using an Identity service (MinimalIdentity.jsm), and emits messages
+ * requesting Persona functions (doWatch, doReady, doLogout).
+ *
+ * The Identity service sends these observer messages to the
+ * Controller in this module, which in turn triggers content to open a
+ * window to host the Persona js. If user interaction is required,
+ * content will open the trusty UI. If user interaction is not required,
+ * and we only need to get to Persona functions, content will open a
+ * hidden iframe. In either case, a window is opened into which the
+ * controller causes the script identity.js to be injected. This
+ * script provides the glue between the in-page javascript and the
+ * pipe back down to the Controller, translating navigator.internal
+ * function callbacks into messages sent back to the Controller.
+ *
+ * As a result, a navigator.internal function in the hosted popup or
+ * iframe can call back to the injected identity.js (doReady, doLogin,
+ * or doLogout). identity.js callbacks send messages back through the
+ * pipe to the Controller. The controller invokes the corresponding
+ * function on the Identity Service (doReady, doLogin, or doLogout).
+ * The IdentityService calls the corresponding callback for the
+ * correct Relying Party, which causes DOMIdentity to send a message
+ * up to the Relying Party through nsDOMIdentity
+ * (Identity:RP:Watch:OnLogin etc.), and finally, nsDOMIdentity
+ * receives these messages and calls the original callback that the
+ * Relying Party registered (navigator.id.watch(),
+ * navigator.id.request(), or navigator.id.logout()).
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SignInToWebsiteController"];
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "getRandomId",
+ "resource://gre/modules/identity/IdentityUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
+ "resource://gre/modules/identity/MinimalIdentity.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Logger",
+ "resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+ "resource://gre/modules/SystemAppProxy.jsm");
+
+// The default persona uri; can be overwritten with toolkit.identity.uri pref.
+// Do this if you want to repoint to a different service for testing.
+// There's no point in setting up an observer to monitor the pref, as b2g prefs
+// can only be overwritten when the profie is recreated. So just get the value
+// on start-up.
+var kPersonaUri = "https://firefoxos.persona.org";
+try {
+ kPersonaUri = Services.prefs.getCharPref("toolkit.identity.uri");
+} catch(noSuchPref) {
+ // stick with the default value
+}
+
+// JS shim that contains the callback functions that
+// live within the identity UI provisioning frame.
+const kIdentityShimFile = "chrome://b2g/content/identity.js";
+
+// Type of MozChromeEvents to handle id dialogs.
+const kOpenIdentityDialog = "id-dialog-open";
+const kDoneIdentityDialog = "id-dialog-done";
+const kCloseIdentityDialog = "id-dialog-close-iframe";
+
+// Observer messages to communicate to shim
+const kIdentityDelegateWatch = "identity-delegate-watch";
+const kIdentityDelegateRequest = "identity-delegate-request";
+const kIdentityDelegateLogout = "identity-delegate-logout";
+const kIdentityDelegateFinished = "identity-delegate-finished";
+const kIdentityDelegateReady = "identity-delegate-ready";
+
+const kIdentityControllerDoMethod = "identity-controller-doMethod";
+
+function log(...aMessageArgs) {
+ Logger.log.apply(Logger, ["SignInToWebsiteController"].concat(aMessageArgs));
+}
+
+log("persona uri =", kPersonaUri);
+
+function sendChromeEvent(details) {
+ details.uri = kPersonaUri;
+ SystemAppProxy.dispatchEvent(details);
+}
+
+function Pipe() {
+ this._watchers = [];
+}
+
+Pipe.prototype = {
+ init: function pipe_init() {
+ Services.obs.addObserver(this, "identity-child-process-shutdown", false);
+ Services.obs.addObserver(this, "identity-controller-unwatch", false);
+ },
+
+ uninit: function pipe_uninit() {
+ Services.obs.removeObserver(this, "identity-child-process-shutdown");
+ Services.obs.removeObserver(this, "identity-controller-unwatch");
+ },
+
+ observe: function Pipe_observe(aSubject, aTopic, aData) {
+ let options = {};
+ if (aSubject) {
+ options = aSubject.wrappedJSObject;
+ }
+ switch (aTopic) {
+ case "identity-child-process-shutdown":
+ log("pipe removing watchers by message manager");
+ this._removeWatchers(null, options.messageManager);
+ break;
+
+ case "identity-controller-unwatch":
+ log("unwatching", options.id);
+ this._removeWatchers(options.id, options.messageManager);
+ break;
+ }
+ },
+
+ _addWatcher: function Pipe__addWatcher(aId, aMm) {
+ log("Adding watcher with id", aId);
+ for (let i = 0; i < this._watchers.length; ++i) {
+ let watcher = this._watchers[i];
+ if (this._watcher.id === aId) {
+ watcher.count++;
+ return;
+ }
+ }
+ this._watchers.push({id: aId, count: 1, mm: aMm});
+ },
+
+ _removeWatchers: function Pipe__removeWatcher(aId, aMm) {
+ let checkId = aId !== null;
+ let index = -1;
+ for (let i = 0; i < this._watchers.length; ++i) {
+ let watcher = this._watchers[i];
+ if (watcher.mm === aMm &&
+ (!checkId || (checkId && watcher.id === aId))) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ if (checkId) {
+ if (--(this._watchers[index].count) === 0) {
+ this._watchers.splice(index, 1);
+ }
+ } else {
+ this._watchers.splice(index, 1);
+ }
+ }
+
+ if (this._watchers.length === 0) {
+ log("No more watchers; clean up persona host iframe");
+ let detail = {
+ type: kCloseIdentityDialog
+ };
+ log('telling content to close the dialog');
+ // tell content to close the dialog
+ sendChromeEvent(detail);
+ }
+ },
+
+ communicate: function(aRpOptions, aContentOptions, aMessageCallback) {
+ let rpID = aRpOptions.id;
+ let rpMM = aRpOptions.mm;
+ if (rpMM) {
+ this._addWatcher(rpID, rpMM);
+ }
+
+ log("RP options:", aRpOptions, "\n content options:", aContentOptions);
+
+ // This content variable is injected into the scope of
+ // kIdentityShimFile, where it is used to access the BrowserID object
+ // and its internal API.
+ let mm = null;
+ let uuid = getRandomId();
+ let self = this;
+
+ function removeMessageListeners() {
+ if (mm) {
+ mm.removeMessageListener(kIdentityDelegateFinished, identityDelegateFinished);
+ mm.removeMessageListener(kIdentityControllerDoMethod, aMessageCallback);
+ }
+ }
+
+ function identityDelegateFinished() {
+ removeMessageListeners();
+
+ let detail = {
+ type: kDoneIdentityDialog,
+ showUI: aContentOptions.showUI || false,
+ id: kDoneIdentityDialog + "-" + uuid,
+ requestId: aRpOptions.id
+ };
+ log('received delegate finished; telling content to close the dialog');
+ sendChromeEvent(detail);
+ self._removeWatchers(rpID, rpMM);
+ }
+
+ SystemAppProxy.addEventListener("mozContentEvent", function getAssertion(evt) {
+ let msg = evt.detail;
+ if (!msg.id.match(uuid)) {
+ return;
+ }
+
+ switch (msg.id) {
+ case kOpenIdentityDialog + '-' + uuid:
+ if (msg.type === 'cancel') {
+ // The user closed the dialog. Clean up and call cancel.
+ SystemAppProxy.removeEventListener("mozContentEvent", getAssertion);
+ removeMessageListeners();
+ aMessageCallback({json: {method: "cancel"}});
+ } else {
+ // The window has opened. Inject the identity shim file containing
+ // the callbacks in the content script. This could be either the
+ // visible popup that the user interacts with, or it could be an
+ // invisible frame.
+ let frame = evt.detail.frame;
+ let frameLoader = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ mm = frameLoader.messageManager;
+ try {
+ mm.loadFrameScript(kIdentityShimFile, true, true);
+ log("Loaded shim", kIdentityShimFile);
+ } catch (e) {
+ log("Error loading", kIdentityShimFile, "as a frame script:", e);
+ }
+
+ // There are two messages that the delegate can send back: a "do
+ // method" event, and a "finished" event. We pass the do-method
+ // events straight to the caller for interpretation and handling.
+ // If we receive a "finished" event, then the delegate is done, so
+ // we shut down the pipe and clean up.
+ mm.addMessageListener(kIdentityControllerDoMethod, aMessageCallback);
+ mm.addMessageListener(kIdentityDelegateFinished, identityDelegateFinished);
+
+ mm.sendAsyncMessage(aContentOptions.message, aRpOptions);
+ }
+ break;
+
+ case kDoneIdentityDialog + '-' + uuid:
+ // Received our assertion. The message manager callbacks will handle
+ // communicating back to the IDService. All we have to do is remove
+ // this listener.
+ SystemAppProxy.removeEventListener("mozContentEvent", getAssertion);
+ break;
+
+ default:
+ log("ERROR - Unexpected message: id=" + msg.id + ", type=" + msg.type + ", errorMsg=" + msg.errorMsg);
+ break;
+ }
+
+ });
+
+ // Tell content to open the identity iframe or trusty popup. The parameter
+ // showUI signals whether user interaction is needed. If it is, content will
+ // open a dialog; if not, a hidden iframe. In each case, BrowserID is
+ // available in the context.
+ let detail = {
+ type: kOpenIdentityDialog,
+ showUI: aContentOptions.showUI || false,
+ id: kOpenIdentityDialog + "-" + uuid,
+ requestId: aRpOptions.id
+ };
+
+ sendChromeEvent(detail);
+ }
+
+};
+
+/*
+ * The controller sits between the IdentityService used by DOMIdentity
+ * and a content process launches an (invisible) iframe or (visible)
+ * trusty UI. Using an injected js script (identity.js), the
+ * controller enables the content window to access the persona identity
+ * storage in the system cookie jar and send events back via the
+ * controller into IdentityService and DOM, and ultimately up to the
+ * Relying Party, which is open in a different window context.
+ */
+this.SignInToWebsiteController = {
+
+ /*
+ * Initialize the controller. To use a different content communication pipe,
+ * such as when mocking it in tests, pass aOptions.pipe.
+ */
+ init: function SignInToWebsiteController_init(aOptions) {
+ aOptions = aOptions || {};
+ this.pipe = aOptions.pipe || new Pipe();
+ Services.obs.addObserver(this, "identity-controller-watch", false);
+ Services.obs.addObserver(this, "identity-controller-request", false);
+ Services.obs.addObserver(this, "identity-controller-logout", false);
+ },
+
+ uninit: function SignInToWebsiteController_uninit() {
+ Services.obs.removeObserver(this, "identity-controller-watch");
+ Services.obs.removeObserver(this, "identity-controller-request");
+ Services.obs.removeObserver(this, "identity-controller-logout");
+ },
+
+ observe: function SignInToWebsiteController_observe(aSubject, aTopic, aData) {
+ log("observe: received", aTopic, "with", aData, "for", aSubject);
+ let options = null;
+ if (aSubject) {
+ options = aSubject.wrappedJSObject;
+ }
+ switch (aTopic) {
+ case "identity-controller-watch":
+ this.doWatch(options);
+ break;
+ case "identity-controller-request":
+ this.doRequest(options);
+ break;
+ case "identity-controller-logout":
+ this.doLogout(options);
+ break;
+ default:
+ Logger.reportError("SignInToWebsiteController", "Unknown observer notification:", aTopic);
+ break;
+ }
+ },
+
+ /*
+ * options: method required - name of method to invoke
+ * assertion optional
+ */
+ _makeDoMethodCallback: function SignInToWebsiteController__makeDoMethodCallback(aRpId) {
+ return function SignInToWebsiteController_methodCallback(aOptions) {
+ let message = aOptions.json;
+ if (typeof message === 'string') {
+ message = JSON.parse(message);
+ }
+
+ switch (message.method) {
+ case "ready":
+ IdentityService.doReady(aRpId);
+ break;
+
+ case "login":
+ if (message._internalParams) {
+ IdentityService.doLogin(aRpId, message.assertion, message._internalParams);
+ } else {
+ IdentityService.doLogin(aRpId, message.assertion);
+ }
+ break;
+
+ case "logout":
+ IdentityService.doLogout(aRpId);
+ break;
+
+ case "cancel":
+ IdentityService.doCancel(aRpId);
+ break;
+
+ default:
+ log("WARNING: wonky method call:", message.method);
+ break;
+ }
+ };
+ },
+
+ doWatch: function SignInToWebsiteController_doWatch(aRpOptions) {
+ // dom prevents watch from being called twice
+ let contentOptions = {
+ message: kIdentityDelegateWatch,
+ showUI: false
+ };
+ this.pipe.communicate(aRpOptions, contentOptions,
+ this._makeDoMethodCallback(aRpOptions.id));
+ },
+
+ /**
+ * The website is requesting login so the user must choose an identity to use.
+ */
+ doRequest: function SignInToWebsiteController_doRequest(aRpOptions) {
+ log("doRequest", aRpOptions);
+ let contentOptions = {
+ message: kIdentityDelegateRequest,
+ showUI: true
+ };
+ this.pipe.communicate(aRpOptions, contentOptions,
+ this._makeDoMethodCallback(aRpOptions.id));
+ },
+
+ /*
+ *
+ */
+ doLogout: function SignInToWebsiteController_doLogout(aRpOptions) {
+ log("doLogout", aRpOptions);
+ let contentOptions = {
+ message: kIdentityDelegateLogout,
+ showUI: false
+ };
+ this.pipe.communicate(aRpOptions, contentOptions,
+ this._makeDoMethodCallback(aRpOptions.id));
+ }
+
+};