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.

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

mercurial