Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | <!DOCTYPE HTML> |
michael@0 | 2 | <html> |
michael@0 | 3 | <!-- |
michael@0 | 4 | https://bugzilla.mozilla.org/show_bug.cgi?id=947374 |
michael@0 | 5 | --> |
michael@0 | 6 | <head> |
michael@0 | 7 | <meta charset="utf-8"> |
michael@0 | 8 | <title>Certified apps can changed the default audience of an assertion -- Bug 947374</title> |
michael@0 | 9 | <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> |
michael@0 | 10 | <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> |
michael@0 | 11 | </head> |
michael@0 | 12 | <body> |
michael@0 | 13 | <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=947374">Mozilla Bug 947374</a> |
michael@0 | 14 | <p id="display"></p> |
michael@0 | 15 | <div id="content"> |
michael@0 | 16 | |
michael@0 | 17 | </div> |
michael@0 | 18 | <pre id="test"> |
michael@0 | 19 | <script type="application/javascript;version=1.8"> |
michael@0 | 20 | |
michael@0 | 21 | SimpleTest.waitForExplicitFinish(); |
michael@0 | 22 | |
michael@0 | 23 | Components.utils.import("resource://gre/modules/Promise.jsm"); |
michael@0 | 24 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 25 | Components.utils.import("resource://gre/modules/identity/jwcrypto.jsm"); |
michael@0 | 26 | Components.utils.import("resource://gre/modules/identity/FirefoxAccounts.jsm"); |
michael@0 | 27 | |
michael@0 | 28 | // quick check to make sure we can test apps: |
michael@0 | 29 | is("appStatus" in document.nodePrincipal, true, |
michael@0 | 30 | "appStatus should be present in nsIPrincipal, if not the rest of this test will fail"); |
michael@0 | 31 | |
michael@0 | 32 | // Mock the Firefox Accounts manager to generate a keypair and provide a fake |
michael@0 | 33 | // cert for the caller on each getAssertion request. |
michael@0 | 34 | function MockFXAManager() {} |
michael@0 | 35 | |
michael@0 | 36 | MockFXAManager.prototype = { |
michael@0 | 37 | getAssertion: function(audience, options) { |
michael@0 | 38 | // Always reject a request for a silent assertion, simulating the |
michael@0 | 39 | // scenario in which there is no signed-in user to begin with. |
michael@0 | 40 | if (options.silent) { |
michael@0 | 41 | return Promise.resolve(null); |
michael@0 | 42 | } |
michael@0 | 43 | |
michael@0 | 44 | let deferred = Promise.defer(); |
michael@0 | 45 | jwcrypto.generateKeyPair("DS160", (err, kp) => { |
michael@0 | 46 | if (err) { |
michael@0 | 47 | return deferred.reject(err); |
michael@0 | 48 | } |
michael@0 | 49 | jwcrypto.generateAssertion("fake-cert", kp, audience, (err, assertion) => { |
michael@0 | 50 | if (err) { |
michael@0 | 51 | return deferred.reject(err); |
michael@0 | 52 | } |
michael@0 | 53 | return deferred.resolve(assertion); |
michael@0 | 54 | }); |
michael@0 | 55 | }); |
michael@0 | 56 | return deferred.promise; |
michael@0 | 57 | } |
michael@0 | 58 | }; |
michael@0 | 59 | |
michael@0 | 60 | let originalManager = FirefoxAccounts.fxAccountsManager; |
michael@0 | 61 | FirefoxAccounts.fxAccountsManager = new MockFXAManager(); |
michael@0 | 62 | |
michael@0 | 63 | // The manifests for these apps are all declared in |
michael@0 | 64 | // /testing/profiles/webapps_mochitest.json. They are injected into the profile |
michael@0 | 65 | // by /testing/mochitest/runtests.py with the appropriate appStatus. So we don't |
michael@0 | 66 | // have to manually install any apps. |
michael@0 | 67 | // |
michael@0 | 68 | // For each app, we will use the file_declareAudience.html content to populate an |
michael@0 | 69 | // iframe. The iframe will request() a firefox accounts assertion. It will then |
michael@0 | 70 | // postMessage the results of this experiment back down to us, and we will |
michael@0 | 71 | // compare it with the expected results. |
michael@0 | 72 | let apps = [ |
michael@0 | 73 | { |
michael@0 | 74 | title: "an installed app, which should neither be able to use FxA, nor change audience", |
michael@0 | 75 | manifest: "https://example.com/manifest.webapp", |
michael@0 | 76 | appStatus: Components.interfaces.nsIPrincipal.APP_STATUS_INSTALLED, |
michael@0 | 77 | origin: "https://example.com", |
michael@0 | 78 | wantAudience: "https://i-cant-have-this.com", |
michael@0 | 79 | uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_declareAudience.html", |
michael@0 | 80 | expected: { |
michael@0 | 81 | success: false, |
michael@0 | 82 | underprivileged: true, |
michael@0 | 83 | }, |
michael@0 | 84 | }, |
michael@0 | 85 | { |
michael@0 | 86 | title: "an app's assertion audience should be its origin by default", |
michael@0 | 87 | manifest: "https://example.com/manifest_priv.webapp", |
michael@0 | 88 | appStatus: Components.interfaces.nsIPrincipal.APP_STATUS_PRIVILEGED, |
michael@0 | 89 | origin: "https://example.com", |
michael@0 | 90 | uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_declareAudience.html", |
michael@0 | 91 | expected: { |
michael@0 | 92 | success: true, |
michael@0 | 93 | underprivileged: false, |
michael@0 | 94 | }, |
michael@0 | 95 | }, |
michael@0 | 96 | { |
michael@0 | 97 | title: "a privileged app, which may not have an audience other than its origin", |
michael@0 | 98 | manifest: "https://example.com/manifest_priv.webapp", |
michael@0 | 99 | appStatus: Components.interfaces.nsIPrincipal.APP_STATUS_PRIVILEGED, |
michael@0 | 100 | origin: "https://example.com", |
michael@0 | 101 | wantAudience: "https://i-like-pie.com", |
michael@0 | 102 | uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_declareAudience.html", |
michael@0 | 103 | expected: { |
michael@0 | 104 | success: false, |
michael@0 | 105 | underprivileged: false, |
michael@0 | 106 | }, |
michael@0 | 107 | }, |
michael@0 | 108 | { |
michael@0 | 109 | title: "a privileged app, which may declare an audience the same as its origin", |
michael@0 | 110 | manifest: "https://example.com/manifest_priv.webapp", |
michael@0 | 111 | appStatus: Components.interfaces.nsIPrincipal.APP_STATUS_PRIVILEGED, |
michael@0 | 112 | origin: "https://example.com", |
michael@0 | 113 | wantAudience: "https://example.com", |
michael@0 | 114 | uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_declareAudience.html", |
michael@0 | 115 | expected: { |
michael@0 | 116 | success: true, |
michael@0 | 117 | }, |
michael@0 | 118 | }, |
michael@0 | 119 | { |
michael@0 | 120 | title: "a certified app, which may do whatever it damn well pleases", |
michael@0 | 121 | manifest: "https://example.com/manifest_cert.webapp", |
michael@0 | 122 | appStatus: Components.interfaces.nsIPrincipal.APP_STATUS_CERTIFIED, |
michael@0 | 123 | origin: "https://example.com", |
michael@0 | 124 | wantAudience: "https://whatever-i-want.com", |
michael@0 | 125 | uri: "https://example.com/chrome/dom/identity/tests/mochitest/file_declareAudience.html", |
michael@0 | 126 | expected: { |
michael@0 | 127 | success: true, |
michael@0 | 128 | }, |
michael@0 | 129 | }, |
michael@0 | 130 | ]; |
michael@0 | 131 | |
michael@0 | 132 | let appIndex = 0; |
michael@0 | 133 | let expectedErrors = 0; |
michael@0 | 134 | let receivedErrors = []; |
michael@0 | 135 | let testRunner = runTest(); |
michael@0 | 136 | |
michael@0 | 137 | // Successful tests will send exactly one message. But for error tests, we may |
michael@0 | 138 | // have more than one message from the onerror handler in the client. So we keep |
michael@0 | 139 | // track of received errors; once they reach the expected count, we are done. |
michael@0 | 140 | function receiveMessage(event) { |
michael@0 | 141 | let result = JSON.parse(event.data); |
michael@0 | 142 | let app = apps[appIndex]; |
michael@0 | 143 | let expected = app.expected; |
michael@0 | 144 | |
michael@0 | 145 | is(result.success, expected.success, |
michael@0 | 146 | "Assertion request succeeds"); |
michael@0 | 147 | |
michael@0 | 148 | if (expected.success) { |
michael@0 | 149 | // Confirm that the assertion audience and origin are as expected |
michael@0 | 150 | let components = extractAssertionComponents(result.backedAssertion); |
michael@0 | 151 | is(components.payload.aud, app.wantAudience || app.origin, |
michael@0 | 152 | "Got desired assertion audience"); |
michael@0 | 153 | |
michael@0 | 154 | } else { |
michael@0 | 155 | receivedErrors.push(result.error); |
michael@0 | 156 | } |
michael@0 | 157 | |
michael@0 | 158 | if (receivedErrors.length === expectedErrors) { |
michael@0 | 159 | |
michael@0 | 160 | if (expected.underprivileged) { |
michael@0 | 161 | ok(receivedErrors.indexOf("ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS") > -1, |
michael@0 | 162 | "Expect a complaint that this app cannot use FxA."); |
michael@0 | 163 | } |
michael@0 | 164 | if (!expected.success) { |
michael@0 | 165 | ok(receivedErrors.indexOf("ERROR_INVALID_ASSERTION_AUDIENCE") > -1, |
michael@0 | 166 | "Expect an error getting an assertion"); |
michael@0 | 167 | } |
michael@0 | 168 | |
michael@0 | 169 | appIndex += 1; |
michael@0 | 170 | |
michael@0 | 171 | if (appIndex === apps.length) { |
michael@0 | 172 | window.removeEventListener("message", receiveMessage); |
michael@0 | 173 | |
michael@0 | 174 | FirefoxAccounts.fxAccountsManager = originalManager; |
michael@0 | 175 | |
michael@0 | 176 | SimpleTest.finish(); |
michael@0 | 177 | return; |
michael@0 | 178 | } |
michael@0 | 179 | |
michael@0 | 180 | testRunner.next(); |
michael@0 | 181 | } |
michael@0 | 182 | } |
michael@0 | 183 | |
michael@0 | 184 | window.addEventListener("message", receiveMessage, false, true); |
michael@0 | 185 | |
michael@0 | 186 | function runTest() { |
michael@0 | 187 | for (let app of apps) { |
michael@0 | 188 | dump("** Testing " + app.title + "\n"); |
michael@0 | 189 | // Set up state for message handler |
michael@0 | 190 | expectedErrors = 0; |
michael@0 | 191 | receivedErrors = []; |
michael@0 | 192 | if (!app.expected.success) { |
michael@0 | 193 | expectedErrors += 1; |
michael@0 | 194 | } |
michael@0 | 195 | if (app.expected.underprivileged) { |
michael@0 | 196 | expectedErrors += 1; |
michael@0 | 197 | } |
michael@0 | 198 | |
michael@0 | 199 | let iframe = document.createElement("iframe"); |
michael@0 | 200 | |
michael@0 | 201 | iframe.setAttribute("mozapp", app.manifest); |
michael@0 | 202 | iframe.setAttribute("mozbrowser", "true"); |
michael@0 | 203 | iframe.src = app.uri; |
michael@0 | 204 | |
michael@0 | 205 | document.getElementById("content").appendChild(iframe); |
michael@0 | 206 | |
michael@0 | 207 | iframe.addEventListener("load", function onLoad() { |
michael@0 | 208 | iframe.removeEventListener("load", onLoad); |
michael@0 | 209 | |
michael@0 | 210 | let principal = iframe.contentDocument.nodePrincipal; |
michael@0 | 211 | is(principal.appStatus, app.appStatus, |
michael@0 | 212 | "Iframe's document.nodePrincipal has expected appStatus"); |
michael@0 | 213 | |
michael@0 | 214 | // Because the <iframe mozapp> can't parent its way back to us, we |
michael@0 | 215 | // provide this handle to our window so it can postMessage to us. |
michael@0 | 216 | iframe.contentWindow.wrappedJSObject.realParent = window; |
michael@0 | 217 | |
michael@0 | 218 | // Test what we want to test, viz. whether or not the app can request |
michael@0 | 219 | // an assertion with an audience the same as or different from its |
michael@0 | 220 | // origin. The client will post back its success or failure in procuring |
michael@0 | 221 | // an identity assertion from Firefox Accounts. |
michael@0 | 222 | iframe.contentWindow.postMessage({audience: app.wantAudience}, "*"); |
michael@0 | 223 | }, false); |
michael@0 | 224 | |
michael@0 | 225 | yield undefined; |
michael@0 | 226 | } |
michael@0 | 227 | } |
michael@0 | 228 | |
michael@0 | 229 | function extractAssertionComponents(backedAssertion) { |
michael@0 | 230 | let [_, signedObject] = backedAssertion.split("~"); |
michael@0 | 231 | let parts = signedObject.split("."); |
michael@0 | 232 | |
michael@0 | 233 | let headerSegment = parts[0]; |
michael@0 | 234 | let payloadSegment = parts[1]; |
michael@0 | 235 | let cryptoSegment = parts[2]; |
michael@0 | 236 | |
michael@0 | 237 | let header = JSON.parse(base64UrlDecode(headerSegment)); |
michael@0 | 238 | let payload = JSON.parse(base64UrlDecode(payloadSegment)); |
michael@0 | 239 | |
michael@0 | 240 | return {header: header, |
michael@0 | 241 | payload: payload, |
michael@0 | 242 | headerSegment: headerSegment, |
michael@0 | 243 | payloadSegment: payloadSegment, |
michael@0 | 244 | cryptoSegment: cryptoSegment}; |
michael@0 | 245 | }; |
michael@0 | 246 | |
michael@0 | 247 | function base64UrlDecode(s) { |
michael@0 | 248 | s = s.replace(/-/g, "+"); |
michael@0 | 249 | s = s.replace(/_/g, "/"); |
michael@0 | 250 | // Don't need to worry about reintroducing padding ('=='), since |
michael@0 | 251 | // jwcrypto provides that. |
michael@0 | 252 | return atob(s); |
michael@0 | 253 | } |
michael@0 | 254 | |
michael@0 | 255 | SpecialPowers.pushPrefEnv({"set": |
michael@0 | 256 | [ |
michael@0 | 257 | ["dom.mozBrowserFramesEnabled", true], |
michael@0 | 258 | ["dom.identity.enabled", true], |
michael@0 | 259 | ["identity.fxaccounts.enabled", true], |
michael@0 | 260 | ["toolkit.identity.debug", true], |
michael@0 | 261 | ["dom.identity.syntheticEventsOk", true], |
michael@0 | 262 | |
michael@0 | 263 | ["security.apps.privileged.CSP.default", ""], |
michael@0 | 264 | ["security.apps.certified.CSP.default", ""], |
michael@0 | 265 | ]}, |
michael@0 | 266 | function() { |
michael@0 | 267 | testRunner.next(); |
michael@0 | 268 | } |
michael@0 | 269 | ); |
michael@0 | 270 | |
michael@0 | 271 | |
michael@0 | 272 | </script> |
michael@0 | 273 | </pre> |
michael@0 | 274 | </body> |
michael@0 | 275 | </html> |