Bug 971379 - FxA should accept synthetic events from certified apps. r=ferjm
authorJed Parsons <jedp@mozilla.com>
Tue, 11 Mar 2014 19:35:24 -0700
changeset 173373 7a6be642b7291adc7af6498f2255198eb754e610
parent 173372 ae9df661d3a4770db82371356363eadba2e64524
child 173374 8b13d02000f4d049127de79e959b9add338df38b
push id40985
push userryanvm@gmail.com
push dateThu, 13 Mar 2014 13:17:59 +0000
treeherdermozilla-inbound@5b84220fd962 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersferjm
bugs971379
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 971379 - FxA should accept synthetic events from certified apps. r=ferjm
dom/identity/DOMIdentity.jsm
dom/identity/nsDOMIdentity.js
dom/identity/tests/mochitest/chrome.ini
dom/identity/tests/mochitest/file_syntheticEvents.html
dom/identity/tests/mochitest/test_syntheticEvents.html
--- a/dom/identity/DOMIdentity.jsm
+++ b/dom/identity/DOMIdentity.jsm
@@ -163,16 +163,28 @@ this.DOMIdentity = {
    * window ID.  We use the mmContexts map when child-process-shutdown is
    * observed, and all we have is a message manager to identify the window in
    * question.
    */
   _serviceContexts: new Map(),
   _mmContexts: new Map(),
 
   /*
+   * Mockable, for testing
+   */
+  _mockIdentityService: null,
+  get IdentityService() {
+    if (this._mockIdentityService) {
+      log("Using a mocked identity service");
+      return this._mockIdentityService;
+    }
+    return IdentityService;
+  },
+
+  /*
    * Create a new RPWatchContext, and update the context maps.
    */
   newContext: function(message, targetMM) {
     let context = new RPWatchContext(message, targetMM);
     this._serviceContexts.set(message.id, context);
     this._mmContexts.set(targetMM, message.id);
     return context;
   },
@@ -194,17 +206,17 @@ this.DOMIdentity = {
     let context = this._serviceContexts.get(message.id);
     if (context.wantIssuer == "firefox-accounts") {
       if (Services.prefs.getPrefType(PREF_FXA_ENABLED) === Ci.nsIPrefBranch.PREF_BOOL
           && Services.prefs.getBoolPref(PREF_FXA_ENABLED)) {
         return FirefoxAccounts;
       }
       log("WARNING: Firefox Accounts is not enabled; Defaulting to BrowserID");
     }
-    return IdentityService;
+    return this.IdentityService;
   },
 
   /*
    * Get the RPWatchContext object for a given message manager.
    */
   getContextForMM: function(targetMM) {
     return this._serviceContexts.get(this._mmContexts.get(targetMM));
   },
--- a/dom/identity/nsDOMIdentity.js
+++ b/dom/identity/nsDOMIdentity.js
@@ -39,16 +39,18 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "nsIMessageSender");
 
 
 const ERRORS = {
   "ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS":
     "Only privileged and certified apps may use Firefox Accounts",
   "ERROR_INVALID_ASSERTION_AUDIENCE":
     "Assertion audience may not differ from origin",
+  "ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT":
+    "The request() method may only be invoked when handling user input",
 };
 
 function nsDOMIdentity(aIdentityInternal) {
   this._identityInternal = aIdentityInternal;
 }
 nsDOMIdentity.prototype = {
   __exposedProps__: {
     // Relying Party (RP)
@@ -173,38 +175,51 @@ nsDOMIdentity.prototype = {
       // broken client to be able to call watch() any more.  It's broken.
       return;
     }
     this._identityInternal._mm.sendAsyncMessage("Identity:RP:Watch", message);
   },
 
   request: function nsDOMIdentity_request(aOptions = {}) {
     this._log("request: " + JSON.stringify(aOptions));
-    let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils);
-
-    // The only time we permit calling of request() outside of a user
-    // input handler is when we are handling the (deprecated) get() or
-    // getVerifiedEmail() calls, which make use of an RP context
-    // marked as _internal.
-    if (this.nativeEventsRequired && !util.isHandlingUserInput && !aOptions._internal) {
-      this._log("request: rejecting non-native event");
-      return;
-    }
 
     // Has the caller called watch() before this?
     if (!this._rpWatcher) {
       throw new Error("navigator.id.request called before navigator.id.watch");
     }
     if (this._rpCalls > MAX_RP_CALLS) {
       throw new Error("navigator.id.request called too many times");
     }
 
+    let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindowUtils);
+
     let message = this.DOMIdentityMessage(aOptions);
 
+    // We permit calling of request() outside of a user input handler only when
+    // we are handling the (deprecated) get() or getVerifiedEmail() calls,
+    // which make use of an RP context marked as _internal, or when a certified
+    // app is calling.
+    //
+    // XXX Bug 982460 - grant the same privilege to packaged apps
+
+    if (!aOptions._internal &&
+        this._appStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
+
+      // If the caller is not special in one of those ways, see if the user has
+      // preffed on 'syntheticEventsOk' (useful for testing); otherwise, if
+      // this is a non-native event, reject it.
+      let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIDOMWindowUtils);
+
+      if (!util.isHandlingUserInput && this.nativeEventsRequired) {
+        message.errors.push("ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT");
+      }
+    }
+
     // Report and fail hard on any errors.
     if (message.errors.length) {
       this.reportErrors(message);
       return;
     }
 
     if (aOptions) {
       // Optional string properties
@@ -615,17 +630,18 @@ nsDOMIdentity.prototype = {
 
     // On b2g, an app's status can be NOT_INSTALLED, INSTALLED, PRIVILEGED, or
     // CERTIFIED.  Compare the appStatus value to the constants enumerated in
     // Ci.nsIPrincipal.APP_STATUS_*.
     message.appStatus = this._appStatus;
 
     // Currently, we only permit certified and privileged apps to use
     // Firefox Accounts.
-    if (this._appStatus !== principal.APP_STATUS_PRIVILEGED &&
+    if (aOptions.wantIssuer == "firefox-accounts" &&
+        this._appStatus !== principal.APP_STATUS_PRIVILEGED &&
         this._appStatus !== principal.APP_STATUS_CERTIFIED) {
       message.errors.push("ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS");
     }
 
     // Normally the window origin will be the audience in assertions.  On b2g,
     // certified apps have the power to override this and declare any audience
     // the want.  Privileged apps can also declare a different audience, as
     // long as it is the same as the origin specified in their manifest files.
@@ -643,17 +659,17 @@ nsDOMIdentity.prototype = {
       } else {
         message.errors.push("ERROR_INVALID_ASSERTION_AUDIENCE");
       }
     }
 
     // Replace any audience supplied by the RP with one that has been sanitised
     message.audience = _audience;
 
-    this._log("Generated message: " + JSON.stringify(message));
+    this._log("DOMIdentityMessage: " + JSON.stringify(message));
 
     return message;
   },
 
   uninit: function DOMIdentity_uninit() {
     this._log("nsDOMIdentity uninit()");
     this._identityInternal._mm.sendAsyncMessage(
       "Identity:RP:Unwatch",
--- a/dom/identity/tests/mochitest/chrome.ini
+++ b/dom/identity/tests/mochitest/chrome.ini
@@ -1,6 +1,9 @@
 [DEFAULT]
 
 support-files=
   file_declareAudience.html
+  file_syntheticEvents.html
 
 [test_declareAudience.html]
+[test_syntheticEvents.html]
+
new file mode 100644
--- /dev/null
+++ b/dom/identity/tests/mochitest/file_syntheticEvents.html
@@ -0,0 +1,50 @@
+<!--
+  * 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/. */
+ -->
+<!DOCTYPE html>
+<html>
+  <!--
+  Certified and privileged apps can call mozId outside an event handler
+  https://bugzilla.mozilla.org/show_bug.cgi?id=971379
+  -->
+<head>
+  <meta charset="utf-8">
+  <title>Test app for bug 971379</title>
+</head>
+
+<body>
+    <div id='test'>
+<script type="application/javascript;version=1.8">
+
+  function postResults(message) {
+    window.realParent.postMessage(JSON.stringify(message), "*");
+  }
+
+  function onready() {
+    navigator.mozId.request();
+  }
+
+  function onlogin(backedAssertion) {
+    postResults({success: true, backedAssertion: backedAssertion});
+  }
+
+  function onerror(error) {
+    postResults({success: false, error: error});
+  }
+
+  onmessage = function(message) {
+    navigator.mozId.watch({
+      wantIssuer: message.data.wantIssuer,
+      onready: onready,
+      onerror: onerror,
+      onlogin: onlogin,
+      onlogout: function() {},
+    });
+  };
+
+</script>
+</div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/identity/tests/mochitest/test_syntheticEvents.html
@@ -0,0 +1,209 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+  https://bugzilla.mozilla.org/show_bug.cgi?id=971379
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Certified/packaged apps may use synthetic events with FXA -- Bug 971379</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=971379">Mozilla Bug 971379</a>
+<p id="display"></p>
+<div id="content">
+
+</div>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+
+SimpleTest.waitForExplicitFinish();
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/DOMIdentity.jsm");
+Components.utils.import("resource://gre/modules/identity/jwcrypto.jsm");
+Components.utils.import("resource://gre/modules/identity/FirefoxAccounts.jsm");
+
+// Mock the Firefox Accounts manager to give a dummy assertion, just to confirm
+// that we're making the trip through the dom/identity and toolkit/identity
+// plumbing.
+function MockFXAManager() {}
+MockFXAManager.prototype = {
+  getAssertion: function() {
+    return Promise.resolve("here~you.go.dude");
+  }
+};
+
+let originalManager = FirefoxAccounts.fxAccountsManager;
+FirefoxAccounts.fxAccountsManager = new MockFXAManager();
+
+// Mock IdentityService (Persona) so we can test request() while not handling
+// user input on an installed app.  Since in this test suite, we have only this
+// one test for Persona, we additionally cause request() to throw if invoked, as
+// added security that nsDOMIdentity did not emit a request message.
+let MockIdentityService = function() {
+  this.RP = this;
+  this.contexts = {};
+}
+MockIdentityService.prototype = {
+  watch: function(context) {
+    this.contexts[context.id] = context;
+    context.doReady();
+  },
+
+  request: function(message) {
+    ok(false, "nsDOMIdentity should block Persona request() in this test suite");
+  },
+};
+DOMIdentity._mockIdentityService = new MockIdentityService();
+
+// The manifests for these apps are all declared in
+// /testing/profiles/webapps_mochitest.json.  They are injected into the profile
+// by /testing/mochitest/runtests.py with the appropriate appStatus.  So we don't
+// have to manually install any apps.
+let apps = [
+  {
+    title: "an installed app, which must request() in a native event",
+    manifest: "https://example.com/manifest.webapp",
+    origin: "https://example.com",
+    uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_syntheticEvents.html",
+    wantIssuer: "",  // default to persona
+    expected: {
+      success: false,
+      errors: [
+        "ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT",
+      ],
+    },
+  },
+  {
+    title: "an installed app, which must may not use firefox accounts",
+    manifest: "https://example.com/manifest.webapp",
+    origin: "https://example.com",
+    uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_syntheticEvents.html",
+    wantIssuer: "firefox-accounts",
+    expected: {
+      success: false,
+      errors: [
+        "ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS",
+      ],
+    },
+  },
+  {
+    title: "a privileged app, which may not use synthetic events (until bug 982460 lands)",
+    manifest: "https://example.com/manifest_priv.webapp",
+    origin: "https://example.com",
+    uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_syntheticEvents.html",
+    wantIssuer: "firefox-accounts",
+    expected: {
+      success: false,
+      errors: [
+        "ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT",
+      ],
+    },
+  },
+  {
+    title: "a certified app, which may use synthetic events",
+    manifest: "https://example.com/manifest_cert.webapp",
+    origin: "https://example.com",
+    uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_syntheticEvents.html",
+    wantIssuer: "firefox-accounts",
+    expected: {
+      success: true,
+    },
+  },
+];
+
+let appIndex = 0;
+let testRunner = runTest();
+let receivedErrors = [];
+
+function receiveMessage(event) {
+  dump("** Received response: " + event.data + "\n");
+  let result = JSON.parse(event.data);
+  let app = apps[appIndex];
+  let expected = app.expected;
+
+  is(result.success, expected.success,
+    "Assertion request " + (expected.success ? "succeeds" : "fails"));
+
+  if (result.error) {
+    receivedErrors.push(result.error);
+  }
+
+  if (receivedErrors.length === (expected.errors || []).length) {
+    receivedErrors.forEach((error) => {
+      ok(expected.errors.indexOf(error) > -1,
+         "Received " + error + ".  " +
+         "Expected errors are: " + JSON.stringify(expected.errors));
+    });
+
+    appIndex += 1;
+
+    if (appIndex === apps.length) {
+      window.removeEventListener("message", receiveMessage);
+
+      // Remove mock from DOMIdentity
+      DOMIdentity._mockIdentityService = null;
+
+      // Restore original fxa manager
+      FirefoxAccounts.fxAccountsManager = originalManager;
+
+      SimpleTest.finish();
+      return;
+    }
+
+    testRunner.next();
+  }
+}
+
+window.addEventListener("message", receiveMessage, false, true);
+
+function runTest() {
+  for (let app of apps) {
+    dump("** Testing " + app.title + "\n");
+
+    receivedErrors = [];
+
+    let iframe = document.createElement("iframe");
+
+    iframe.setAttribute("mozapp", app.manifest);
+    iframe.setAttribute("mozbrowser", "true");
+    iframe.src = app.uri;
+
+    document.getElementById("content").appendChild(iframe);
+
+    iframe.addEventListener("load", function onLoad() {
+      iframe.removeEventListener("load", onLoad);
+
+      // Because the <iframe mozapp> can't parent its way back to us, we
+      // provide this handle to our window so it can postMessage to us.
+      iframe.contentWindow.wrappedJSObject.realParent = window;
+      iframe.contentWindow.postMessage({wantIssuer: app.wantIssuer}, "*");
+    }, false);
+
+    yield undefined;
+  }
+}
+
+SpecialPowers.pushPrefEnv({"set":
+  [
+    ["dom.mozBrowserFramesEnabled", true],
+    ["dom.identity.enabled", true],
+    ["identity.fxaccounts.enabled", true],
+    ["toolkit.identity.debug", true],
+
+    ["security.apps.privileged.CSP.default", ""],
+    ["security.apps.certified.CSP.default", ""],
+  ]},
+  function() {
+    testRunner.next();
+  }
+);
+
+
+</script>
+</pre>
+</body>
+</html>