diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /b2g/components/SignInToWebsite.jsm | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'b2g/components/SignInToWebsite.jsm')
-rw-r--r-- | b2g/components/SignInToWebsite.jsm | 444 |
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)); + } + +}; |