Bug 971379 - FxA should accept synthetic events from certified apps. r=ferjm

This commit is contained in:
Jed Parsons 2014-03-11 19:35:24 -07:00
parent 8129654c83
commit dfd9d50b6c
5 changed files with 304 additions and 14 deletions

View File

@ -167,6 +167,18 @@ this.DOMIdentity = {
_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.
*/
@ -199,7 +211,7 @@ this.DOMIdentity = {
}
log("WARNING: Firefox Accounts is not enabled; Defaulting to BrowserID");
}
return IdentityService;
return this.IdentityService;
},
/*

View File

@ -44,6 +44,8 @@ const ERRORS = {
"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) {
@ -178,17 +180,6 @@ nsDOMIdentity.prototype = {
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) {
@ -198,8 +189,32 @@ nsDOMIdentity.prototype = {
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);
@ -620,7 +635,8 @@ nsDOMIdentity.prototype = {
// 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");
}
@ -648,7 +664,7 @@ nsDOMIdentity.prototype = {
// 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;
},

View File

@ -2,5 +2,8 @@
support-files=
file_declareAudience.html
file_syntheticEvents.html
[test_declareAudience.html]
[test_syntheticEvents.html]

View File

@ -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>

View File

@ -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>