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