b2g/components/SignInToWebsite.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 /*
     6  * SignInToWebsite.jsm - UX Controller and means for accessing identity
     7  * cookies on behalf of relying parties.
     8  *
     9  * Currently, the b2g security architecture isolates web applications
    10  * so that each window has access only to a local cookie jar:
    11  *
    12  *     To prevent Web apps from interfering with one another, each one is
    13  *     hosted on a separate domain, and therefore may only access the
    14  *     resources associated with its domain. These resources include
    15  *     things such as IndexedDB databases, cookies, offline storage,
    16  *     and so forth.
    17  *
    18  *     -- https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS/Security/Security_model
    19  *
    20  * As a result, an authentication system like Persona cannot share its
    21  * cookie jar with multiple relying parties, and so would require a
    22  * fresh login request in every window.  This would not be a good
    23  * experience.
    24  *
    25  *
    26  * In order for navigator.id.request() to maintain state in a single
    27  * cookie jar, we cause all Persona interactions to take place in a
    28  * content context that is launched by the system application, with the
    29  * result that Persona has a single cookie jar that all Relying
    30  * Parties can use.  Since of course those Relying Parties cannot
    31  * reach into the system cookie jar, the Controller in this module
    32  * provides a way to get messages and data to and fro between the
    33  * Relying Party in its window context, and the Persona internal api
    34  * in its context.
    35  *
    36  * On the Relying Party's side, say a web page invokes
    37  * navigator.id.watch(), to register callbacks, and then
    38  * navigator.id.request() to request an assertion.  The navigator.id
    39  * calls are provided by nsDOMIdentity.  nsDOMIdentity messages down
    40  * to the privileged DOMIdentity code (using cpmm and ppmm message
    41  * managers).  DOMIdentity stores the state of Relying Party flows
    42  * using an Identity service (MinimalIdentity.jsm), and emits messages
    43  * requesting Persona functions (doWatch, doReady, doLogout).
    44  *
    45  * The Identity service sends these observer messages to the
    46  * Controller in this module, which in turn triggers content to open a
    47  * window to host the Persona js.  If user interaction is required,
    48  * content will open the trusty UI.  If user interaction is not required,
    49  * and we only need to get to Persona functions, content will open a
    50  * hidden iframe.  In either case, a window is opened into which the
    51  * controller causes the script identity.js to be injected.  This
    52  * script provides the glue between the in-page javascript and the
    53  * pipe back down to the Controller, translating navigator.internal
    54  * function callbacks into messages sent back to the Controller.
    55  *
    56  * As a result, a navigator.internal function in the hosted popup or
    57  * iframe can call back to the injected identity.js (doReady, doLogin,
    58  * or doLogout).  identity.js callbacks send messages back through the
    59  * pipe to the Controller.  The controller invokes the corresponding
    60  * function on the Identity Service (doReady, doLogin, or doLogout).
    61  * The IdentityService calls the corresponding callback for the
    62  * correct Relying Party, which causes DOMIdentity to send a message
    63  * up to the Relying Party through nsDOMIdentity
    64  * (Identity:RP:Watch:OnLogin etc.), and finally, nsDOMIdentity
    65  * receives these messages and calls the original callback that the
    66  * Relying Party registered (navigator.id.watch(),
    67  * navigator.id.request(), or navigator.id.logout()).
    68  */
    70 "use strict";
    72 this.EXPORTED_SYMBOLS = ["SignInToWebsiteController"];
    74 const Ci = Components.interfaces;
    75 const Cu = Components.utils;
    77 Cu.import("resource://gre/modules/Services.jsm");
    78 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    80 XPCOMUtils.defineLazyModuleGetter(this, "getRandomId",
    81                                   "resource://gre/modules/identity/IdentityUtils.jsm");
    83 XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
    84                                   "resource://gre/modules/identity/MinimalIdentity.jsm");
    86 XPCOMUtils.defineLazyModuleGetter(this, "Logger",
    87                                   "resource://gre/modules/identity/LogUtils.jsm");
    89 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
    90                                   "resource://gre/modules/SystemAppProxy.jsm");
    92 // The default persona uri; can be overwritten with toolkit.identity.uri pref.
    93 // Do this if you want to repoint to a different service for testing.
    94 // There's no point in setting up an observer to monitor the pref, as b2g prefs
    95 // can only be overwritten when the profie is recreated.  So just get the value
    96 // on start-up.
    97 let kPersonaUri = "https://firefoxos.persona.org";
    98 try {
    99   kPersonaUri = Services.prefs.getCharPref("toolkit.identity.uri");
   100 } catch(noSuchPref) {
   101   // stick with the default value
   102 }
   104 // JS shim that contains the callback functions that
   105 // live within the identity UI provisioning frame.
   106 const kIdentityShimFile = "chrome://b2g/content/identity.js";
   108 // Type of MozChromeEvents to handle id dialogs.
   109 const kOpenIdentityDialog = "id-dialog-open";
   110 const kDoneIdentityDialog = "id-dialog-done";
   111 const kCloseIdentityDialog = "id-dialog-close-iframe";
   113 // Observer messages to communicate to shim
   114 const kIdentityDelegateWatch = "identity-delegate-watch";
   115 const kIdentityDelegateRequest = "identity-delegate-request";
   116 const kIdentityDelegateLogout = "identity-delegate-logout";
   117 const kIdentityDelegateFinished = "identity-delegate-finished";
   118 const kIdentityDelegateReady = "identity-delegate-ready";
   120 const kIdentityControllerDoMethod = "identity-controller-doMethod";
   122 function log(...aMessageArgs) {
   123   Logger.log.apply(Logger, ["SignInToWebsiteController"].concat(aMessageArgs));
   124 }
   126 log("persona uri =", kPersonaUri);
   128 function sendChromeEvent(details) {
   129   details.uri = kPersonaUri;
   130   SystemAppProxy.dispatchEvent(details);
   131 }
   133 function Pipe() {
   134   this._watchers = [];
   135 }
   137 Pipe.prototype = {
   138   init: function pipe_init() {
   139     Services.obs.addObserver(this, "identity-child-process-shutdown", false);
   140     Services.obs.addObserver(this, "identity-controller-unwatch", false);
   141   },
   143   uninit: function pipe_uninit() {
   144     Services.obs.removeObserver(this, "identity-child-process-shutdown");
   145     Services.obs.removeObserver(this, "identity-controller-unwatch");
   146   },
   148   observe: function Pipe_observe(aSubject, aTopic, aData) {
   149     let options = {};
   150     if (aSubject) {
   151       options = aSubject.wrappedJSObject;
   152     }
   153     switch (aTopic) {
   154       case "identity-child-process-shutdown":
   155         log("pipe removing watchers by message manager");
   156         this._removeWatchers(null, options.messageManager);
   157         break;
   159       case "identity-controller-unwatch":
   160         log("unwatching", options.id);
   161         this._removeWatchers(options.id, options.messageManager);
   162         break;
   163     }
   164   },
   166   _addWatcher: function Pipe__addWatcher(aId, aMm) {
   167     log("Adding watcher with id", aId);
   168     for (let i = 0; i < this._watchers.length; ++i) {
   169       let watcher = this._watchers[i];
   170       if (this._watcher.id === aId) {
   171         watcher.count++;
   172         return;
   173       }
   174     }
   175     this._watchers.push({id: aId, count: 1, mm: aMm});
   176   },
   178   _removeWatchers: function Pipe__removeWatcher(aId, aMm) {
   179     let checkId = aId !== null;
   180     let index = -1;
   181     for (let i = 0; i < this._watchers.length; ++i) {
   182       let watcher = this._watchers[i];
   183       if (watcher.mm === aMm &&
   184           (!checkId || (checkId && watcher.id === aId))) {
   185         index = i;
   186         break;
   187       }
   188     }
   190     if (index !== -1) {
   191       if (checkId) {
   192         if (--(this._watchers[index].count) === 0) {
   193           this._watchers.splice(index, 1);
   194         }
   195       } else {
   196         this._watchers.splice(index, 1);
   197       }
   198     }
   200     if (this._watchers.length === 0) {
   201       log("No more watchers; clean up persona host iframe");
   202       let detail = {
   203         type: kCloseIdentityDialog
   204       };
   205       log('telling content to close the dialog');
   206       // tell content to close the dialog
   207       sendChromeEvent(detail);
   208     }
   209   },
   211   communicate: function(aRpOptions, aContentOptions, aMessageCallback) {
   212     let rpID = aRpOptions.id;
   213     let rpMM = aRpOptions.mm;
   214     if (rpMM) {
   215       this._addWatcher(rpID, rpMM);
   216     }
   218     log("RP options:", aRpOptions, "\n  content options:", aContentOptions);
   220     // This content variable is injected into the scope of
   221     // kIdentityShimFile, where it is used to access the BrowserID object
   222     // and its internal API.
   223     let mm = null;
   224     let uuid = getRandomId();
   225     let self = this;
   227     function removeMessageListeners() {
   228       if (mm) {
   229         mm.removeMessageListener(kIdentityDelegateFinished, identityDelegateFinished);
   230         mm.removeMessageListener(kIdentityControllerDoMethod, aMessageCallback);
   231       }
   232     }
   234     function identityDelegateFinished() {
   235       removeMessageListeners();
   237       let detail = {
   238         type: kDoneIdentityDialog,
   239         showUI: aContentOptions.showUI || false,
   240         id: kDoneIdentityDialog + "-" + uuid,
   241         requestId: aRpOptions.id
   242       };
   243       log('received delegate finished; telling content to close the dialog');
   244       sendChromeEvent(detail);
   245       self._removeWatchers(rpID, rpMM);
   246     }
   248     SystemAppProxy.addEventListener("mozContentEvent", function getAssertion(evt) {
   249       let msg = evt.detail;
   250       if (!msg.id.match(uuid)) {
   251         return;
   252       }
   254       switch (msg.id) {
   255         case kOpenIdentityDialog + '-' + uuid:
   256           if (msg.type === 'cancel') {
   257             // The user closed the dialog.  Clean up and call cancel.
   258             SystemAppProxy.removeEventListener("mozContentEvent", getAssertion);
   259             removeMessageListeners();
   260             aMessageCallback({json: {method: "cancel"}});
   261           } else {
   262             // The window has opened.  Inject the identity shim file containing
   263             // the callbacks in the content script.  This could be either the
   264             // visible popup that the user interacts with, or it could be an
   265             // invisible frame.
   266             let frame = evt.detail.frame;
   267             let frameLoader = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
   268             mm = frameLoader.messageManager;
   269             try {
   270               mm.loadFrameScript(kIdentityShimFile, true, true);
   271               log("Loaded shim", kIdentityShimFile);
   272             } catch (e) {
   273               log("Error loading", kIdentityShimFile, "as a frame script:", e);
   274             }
   276             // There are two messages that the delegate can send back: a "do
   277             // method" event, and a "finished" event.  We pass the do-method
   278             // events straight to the caller for interpretation and handling.
   279             // If we receive a "finished" event, then the delegate is done, so
   280             // we shut down the pipe and clean up.
   281             mm.addMessageListener(kIdentityControllerDoMethod, aMessageCallback);
   282             mm.addMessageListener(kIdentityDelegateFinished, identityDelegateFinished);
   284             mm.sendAsyncMessage(aContentOptions.message, aRpOptions);
   285           }
   286           break;
   288         case kDoneIdentityDialog + '-' + uuid:
   289           // Received our assertion.  The message manager callbacks will handle
   290           // communicating back to the IDService.  All we have to do is remove
   291           // this listener.
   292           SystemAppProxy.removeEventListener("mozContentEvent", getAssertion);
   293           break;
   295         default:
   296           log("ERROR - Unexpected message: id=" + msg.id + ", type=" + msg.type + ", errorMsg=" + msg.errorMsg);
   297           break;
   298       }
   300     });
   302     // Tell content to open the identity iframe or trusty popup. The parameter
   303     // showUI signals whether user interaction is needed.  If it is, content will
   304     // open a dialog; if not, a hidden iframe.  In each case, BrowserID is
   305     // available in the context.
   306     let detail = {
   307       type: kOpenIdentityDialog,
   308       showUI: aContentOptions.showUI || false,
   309       id: kOpenIdentityDialog + "-" + uuid,
   310       requestId: aRpOptions.id
   311     };
   313     sendChromeEvent(detail);
   314   }
   316 };
   318 /*
   319  * The controller sits between the IdentityService used by DOMIdentity
   320  * and a content process launches an (invisible) iframe or (visible)
   321  * trusty UI.  Using an injected js script (identity.js), the
   322  * controller enables the content window to access the persona identity
   323  * storage in the system cookie jar and send events back via the
   324  * controller into IdentityService and DOM, and ultimately up to the
   325  * Relying Party, which is open in a different window context.
   326  */
   327 this.SignInToWebsiteController = {
   329   /*
   330    * Initialize the controller.  To use a different content communication pipe,
   331    * such as when mocking it in tests, pass aOptions.pipe.
   332    */
   333   init: function SignInToWebsiteController_init(aOptions) {
   334     aOptions = aOptions || {};
   335     this.pipe = aOptions.pipe || new Pipe();
   336     Services.obs.addObserver(this, "identity-controller-watch", false);
   337     Services.obs.addObserver(this, "identity-controller-request", false);
   338     Services.obs.addObserver(this, "identity-controller-logout", false);
   339   },
   341   uninit: function SignInToWebsiteController_uninit() {
   342     Services.obs.removeObserver(this, "identity-controller-watch");
   343     Services.obs.removeObserver(this, "identity-controller-request");
   344     Services.obs.removeObserver(this, "identity-controller-logout");
   345   },
   347   observe: function SignInToWebsiteController_observe(aSubject, aTopic, aData) {
   348     log("observe: received", aTopic, "with", aData, "for", aSubject);
   349     let options = null;
   350     if (aSubject) {
   351       options = aSubject.wrappedJSObject;
   352     }
   353     switch (aTopic) {
   354       case "identity-controller-watch":
   355         this.doWatch(options);
   356         break;
   357       case "identity-controller-request":
   358         this.doRequest(options);
   359         break;
   360       case "identity-controller-logout":
   361         this.doLogout(options);
   362         break;
   363       default:
   364         Logger.reportError("SignInToWebsiteController", "Unknown observer notification:", aTopic);
   365         break;
   366     }
   367   },
   369   /*
   370    * options:    method          required - name of method to invoke
   371    *             assertion       optional
   372    */
   373   _makeDoMethodCallback: function SignInToWebsiteController__makeDoMethodCallback(aRpId) {
   374     return function SignInToWebsiteController_methodCallback(aOptions) {
   375       let message = aOptions.json;
   376       if (typeof message === 'string') {
   377         message = JSON.parse(message);
   378       }
   380       switch (message.method) {
   381         case "ready":
   382           IdentityService.doReady(aRpId);
   383           break;
   385         case "login":
   386            if (message._internalParams) {
   387              IdentityService.doLogin(aRpId, message.assertion, message._internalParams);
   388            } else {
   389              IdentityService.doLogin(aRpId, message.assertion);
   390            }
   391           break;
   393         case "logout":
   394           IdentityService.doLogout(aRpId);
   395           break;
   397         case "cancel":
   398           IdentityService.doCancel(aRpId);
   399           break;
   401         default:
   402           log("WARNING: wonky method call:", message.method);
   403           break;
   404       }
   405     };
   406   },
   408   doWatch: function SignInToWebsiteController_doWatch(aRpOptions) {
   409     // dom prevents watch from  being called twice
   410     let contentOptions = {
   411       message: kIdentityDelegateWatch,
   412       showUI: false
   413     };
   414     this.pipe.communicate(aRpOptions, contentOptions,
   415         this._makeDoMethodCallback(aRpOptions.id));
   416   },
   418   /**
   419    * The website is requesting login so the user must choose an identity to use.
   420    */
   421   doRequest: function SignInToWebsiteController_doRequest(aRpOptions) {
   422     log("doRequest", aRpOptions);
   423     let contentOptions = {
   424       message: kIdentityDelegateRequest,
   425       showUI: true
   426     };
   427     this.pipe.communicate(aRpOptions, contentOptions,
   428         this._makeDoMethodCallback(aRpOptions.id));
   429   },
   431   /*
   432    *
   433    */
   434   doLogout: function SignInToWebsiteController_doLogout(aRpOptions) {
   435     log("doLogout", aRpOptions);
   436     let contentOptions = {
   437       message: kIdentityDelegateLogout,
   438       showUI: false
   439     };
   440     this.pipe.communicate(aRpOptions, contentOptions,
   441         this._makeDoMethodCallback(aRpOptions.id));
   442   }
   444 };

mercurial