|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 * http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 Cu.import("resource://gre/modules/FxAccounts.jsm"); |
|
5 Cu.import("resource://services-sync/browserid_identity.js"); |
|
6 Cu.import("resource://services-sync/rest.js"); |
|
7 Cu.import("resource://services-sync/util.js"); |
|
8 Cu.import("resource://services-common/utils.js"); |
|
9 Cu.import("resource://services-crypto/utils.js"); |
|
10 Cu.import("resource://testing-common/services/sync/utils.js"); |
|
11 Cu.import("resource://testing-common/services/sync/fxa_utils.js"); |
|
12 Cu.import("resource://services-common/hawkclient.js"); |
|
13 Cu.import("resource://gre/modules/FxAccounts.jsm"); |
|
14 Cu.import("resource://gre/modules/FxAccountsClient.jsm"); |
|
15 Cu.import("resource://gre/modules/FxAccountsCommon.js"); |
|
16 Cu.import("resource://services-sync/service.js"); |
|
17 Cu.import("resource://services-sync/status.js"); |
|
18 Cu.import("resource://services-sync/constants.js"); |
|
19 |
|
20 const SECOND_MS = 1000; |
|
21 const MINUTE_MS = SECOND_MS * 60; |
|
22 const HOUR_MS = MINUTE_MS * 60; |
|
23 |
|
24 let identityConfig = makeIdentityConfig(); |
|
25 let browseridManager = new BrowserIDManager(); |
|
26 configureFxAccountIdentity(browseridManager, identityConfig); |
|
27 |
|
28 /** |
|
29 * Mock client clock and skew vs server in FxAccounts signed-in user module and |
|
30 * API client. browserid_identity.js queries these values to construct HAWK |
|
31 * headers. We will use this to test clock skew compensation in these headers |
|
32 * below. |
|
33 */ |
|
34 let MockFxAccountsClient = function() { |
|
35 FxAccountsClient.apply(this); |
|
36 }; |
|
37 MockFxAccountsClient.prototype = { |
|
38 __proto__: FxAccountsClient.prototype |
|
39 }; |
|
40 |
|
41 function MockFxAccounts() { |
|
42 let fxa = new FxAccounts({ |
|
43 _now_is: Date.now(), |
|
44 |
|
45 now: function () { |
|
46 return this._now_is; |
|
47 }, |
|
48 |
|
49 fxAccountsClient: new MockFxAccountsClient() |
|
50 }); |
|
51 fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { |
|
52 this.cert = { |
|
53 validUntil: fxa.internal.now() + CERT_LIFETIME, |
|
54 cert: "certificate", |
|
55 }; |
|
56 return Promise.resolve(this.cert.cert); |
|
57 }; |
|
58 return fxa; |
|
59 } |
|
60 |
|
61 function run_test() { |
|
62 initTestLogging("Trace"); |
|
63 Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace; |
|
64 Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace; |
|
65 run_next_test(); |
|
66 }; |
|
67 |
|
68 add_test(function test_initial_state() { |
|
69 _("Verify initial state"); |
|
70 do_check_false(!!browseridManager._token); |
|
71 do_check_false(browseridManager.hasValidToken()); |
|
72 run_next_test(); |
|
73 } |
|
74 ); |
|
75 |
|
76 add_task(function test_initialializeWithCurrentIdentity() { |
|
77 _("Verify start after initializeWithCurrentIdentity"); |
|
78 browseridManager.initializeWithCurrentIdentity(); |
|
79 yield browseridManager.whenReadyToAuthenticate.promise; |
|
80 do_check_true(!!browseridManager._token); |
|
81 do_check_true(browseridManager.hasValidToken()); |
|
82 do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email); |
|
83 } |
|
84 ); |
|
85 |
|
86 |
|
87 add_test(function test_getResourceAuthenticator() { |
|
88 _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header."); |
|
89 let authenticator = browseridManager.getResourceAuthenticator(); |
|
90 do_check_true(!!authenticator); |
|
91 let req = {uri: CommonUtils.makeURI( |
|
92 "https://example.net/somewhere/over/the/rainbow"), |
|
93 method: 'GET'}; |
|
94 let output = authenticator(req, 'GET'); |
|
95 do_check_true('headers' in output); |
|
96 do_check_true('authorization' in output.headers); |
|
97 do_check_true(output.headers.authorization.startsWith('Hawk')); |
|
98 _("Expected internal state after successful call."); |
|
99 do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid); |
|
100 run_next_test(); |
|
101 } |
|
102 ); |
|
103 |
|
104 add_test(function test_getRESTRequestAuthenticator() { |
|
105 _("BrowserIDManager supplies a REST Request Authenticator callback which sets a Hawk header on a request object."); |
|
106 let request = new SyncStorageRequest( |
|
107 "https://example.net/somewhere/over/the/rainbow"); |
|
108 let authenticator = browseridManager.getRESTRequestAuthenticator(); |
|
109 do_check_true(!!authenticator); |
|
110 let output = authenticator(request, 'GET'); |
|
111 do_check_eq(request.uri, output.uri); |
|
112 do_check_true(output._headers.authorization.startsWith('Hawk')); |
|
113 do_check_true(output._headers.authorization.contains('nonce')); |
|
114 do_check_true(browseridManager.hasValidToken()); |
|
115 run_next_test(); |
|
116 } |
|
117 ); |
|
118 |
|
119 add_test(function test_resourceAuthenticatorSkew() { |
|
120 _("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header."); |
|
121 |
|
122 // Clock is skewed 12 hours into the future |
|
123 // We pick a date in the past so we don't risk concealing bugs in code that |
|
124 // uses new Date() instead of our given date. |
|
125 let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; |
|
126 let browseridManager = new BrowserIDManager(); |
|
127 let hawkClient = new HawkClient("https://example.net/v1", "/foo"); |
|
128 |
|
129 // mock fxa hawk client skew |
|
130 hawkClient.now = function() { |
|
131 dump("mocked client now: " + now + '\n'); |
|
132 return now; |
|
133 } |
|
134 // Imagine there's already been one fxa request and the hawk client has |
|
135 // already detected skew vs the fxa auth server. |
|
136 let localtimeOffsetMsec = -1 * 12 * HOUR_MS; |
|
137 hawkClient._localtimeOffsetMsec = localtimeOffsetMsec; |
|
138 |
|
139 let fxaClient = new MockFxAccountsClient(); |
|
140 fxaClient.hawk = hawkClient; |
|
141 |
|
142 // Sanity check |
|
143 do_check_eq(hawkClient.now(), now); |
|
144 do_check_eq(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec); |
|
145 |
|
146 // Properly picked up by the client |
|
147 do_check_eq(fxaClient.now(), now); |
|
148 do_check_eq(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec); |
|
149 |
|
150 let fxa = new MockFxAccounts(); |
|
151 fxa.internal._now_is = now; |
|
152 fxa.internal.fxAccountsClient = fxaClient; |
|
153 |
|
154 // Picked up by the signed-in user module |
|
155 do_check_eq(fxa.internal.now(), now); |
|
156 do_check_eq(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec); |
|
157 |
|
158 do_check_eq(fxa.now(), now); |
|
159 do_check_eq(fxa.localtimeOffsetMsec, localtimeOffsetMsec); |
|
160 |
|
161 // Mocks within mocks... |
|
162 configureFxAccountIdentity(browseridManager, identityConfig); |
|
163 |
|
164 // Ensure the new FxAccounts mock has a signed-in user. |
|
165 fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; |
|
166 |
|
167 browseridManager._fxaService = fxa; |
|
168 |
|
169 do_check_eq(browseridManager._fxaService.internal.now(), now); |
|
170 do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec, |
|
171 localtimeOffsetMsec); |
|
172 |
|
173 do_check_eq(browseridManager._fxaService.now(), now); |
|
174 do_check_eq(browseridManager._fxaService.localtimeOffsetMsec, |
|
175 localtimeOffsetMsec); |
|
176 |
|
177 let request = new SyncStorageRequest("https://example.net/i/like/pie/"); |
|
178 let authenticator = browseridManager.getResourceAuthenticator(); |
|
179 let output = authenticator(request, 'GET'); |
|
180 dump("output" + JSON.stringify(output)); |
|
181 let authHeader = output.headers.authorization; |
|
182 do_check_true(authHeader.startsWith('Hawk')); |
|
183 |
|
184 // Skew correction is applied in the header and we're within the two-minute |
|
185 // window. |
|
186 do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); |
|
187 do_check_true( |
|
188 (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); |
|
189 |
|
190 run_next_test(); |
|
191 }); |
|
192 |
|
193 add_test(function test_RESTResourceAuthenticatorSkew() { |
|
194 _("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header."); |
|
195 |
|
196 // Clock is skewed 12 hours into the future from our arbitary date |
|
197 let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; |
|
198 let browseridManager = new BrowserIDManager(); |
|
199 let hawkClient = new HawkClient("https://example.net/v1", "/foo"); |
|
200 |
|
201 // mock fxa hawk client skew |
|
202 hawkClient.now = function() { |
|
203 return now; |
|
204 } |
|
205 // Imagine there's already been one fxa request and the hawk client has |
|
206 // already detected skew vs the fxa auth server. |
|
207 hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS; |
|
208 |
|
209 let fxaClient = new MockFxAccountsClient(); |
|
210 fxaClient.hawk = hawkClient; |
|
211 let fxa = new MockFxAccounts(); |
|
212 fxa.internal._now_is = now; |
|
213 fxa.internal.fxAccountsClient = fxaClient; |
|
214 |
|
215 configureFxAccountIdentity(browseridManager, identityConfig); |
|
216 |
|
217 // Ensure the new FxAccounts mock has a signed-in user. |
|
218 fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; |
|
219 |
|
220 browseridManager._fxaService = fxa; |
|
221 |
|
222 do_check_eq(browseridManager._fxaService.internal.now(), now); |
|
223 |
|
224 let request = new SyncStorageRequest("https://example.net/i/like/pie/"); |
|
225 let authenticator = browseridManager.getResourceAuthenticator(); |
|
226 let output = authenticator(request, 'GET'); |
|
227 dump("output" + JSON.stringify(output)); |
|
228 let authHeader = output.headers.authorization; |
|
229 do_check_true(authHeader.startsWith('Hawk')); |
|
230 |
|
231 // Skew correction is applied in the header and we're within the two-minute |
|
232 // window. |
|
233 do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); |
|
234 do_check_true( |
|
235 (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); |
|
236 |
|
237 run_next_test(); |
|
238 }); |
|
239 |
|
240 add_task(function test_ensureLoggedIn() { |
|
241 configureFxAccountIdentity(browseridManager); |
|
242 yield browseridManager.initializeWithCurrentIdentity(); |
|
243 Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked"); |
|
244 yield browseridManager.ensureLoggedIn(); |
|
245 Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked"); |
|
246 Assert.ok(browseridManager._shouldHaveSyncKeyBundle, |
|
247 "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); |
|
248 |
|
249 // arrange for no logged in user. |
|
250 let fxa = browseridManager._fxaService |
|
251 let signedInUser = fxa.internal.currentAccountState.signedInUser; |
|
252 fxa.internal.currentAccountState.signedInUser = null; |
|
253 browseridManager.initializeWithCurrentIdentity(); |
|
254 Assert.ok(!browseridManager._shouldHaveSyncKeyBundle, |
|
255 "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are."); |
|
256 Status.login = LOGIN_FAILED_NO_USERNAME; |
|
257 yield Assert_rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user"); |
|
258 Assert.ok(browseridManager._shouldHaveSyncKeyBundle, |
|
259 "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); |
|
260 fxa.internal.currentAccountState.signedInUser = signedInUser; |
|
261 Status.login = LOGIN_FAILED_LOGIN_REJECTED; |
|
262 yield Assert_rejects(browseridManager.ensureLoggedIn(), |
|
263 "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection"); |
|
264 Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, |
|
265 "status should remain LOGIN_FAILED_LOGIN_REJECTED"); |
|
266 Status.login = LOGIN_FAILED_NETWORK_ERROR; |
|
267 yield browseridManager.ensureLoggedIn(); |
|
268 Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked"); |
|
269 }); |
|
270 |
|
271 add_test(function test_tokenExpiration() { |
|
272 _("BrowserIDManager notices token expiration:"); |
|
273 let bimExp = new BrowserIDManager(); |
|
274 configureFxAccountIdentity(bimExp, identityConfig); |
|
275 |
|
276 let authenticator = bimExp.getResourceAuthenticator(); |
|
277 do_check_true(!!authenticator); |
|
278 let req = {uri: CommonUtils.makeURI( |
|
279 "https://example.net/somewhere/over/the/rainbow"), |
|
280 method: 'GET'}; |
|
281 authenticator(req, 'GET'); |
|
282 |
|
283 // Mock the clock. |
|
284 _("Forcing the token to expire ..."); |
|
285 Object.defineProperty(bimExp, "_now", { |
|
286 value: function customNow() { |
|
287 return (Date.now() + 3000001); |
|
288 }, |
|
289 writable: true, |
|
290 }); |
|
291 do_check_true(bimExp._token.expiration < bimExp._now()); |
|
292 _("... means BrowserIDManager knows to re-fetch it on the next call."); |
|
293 do_check_false(bimExp.hasValidToken()); |
|
294 run_next_test(); |
|
295 } |
|
296 ); |
|
297 |
|
298 add_test(function test_sha256() { |
|
299 // Test vectors from http://www.bichlmeier.info/sha256test.html |
|
300 let vectors = [ |
|
301 ["", |
|
302 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"], |
|
303 ["abc", |
|
304 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"], |
|
305 ["message digest", |
|
306 "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"], |
|
307 ["secure hash algorithm", |
|
308 "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"], |
|
309 ["SHA256 is considered to be safe", |
|
310 "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"], |
|
311 ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", |
|
312 "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"], |
|
313 ["For this sample, this 63-byte string will be used as input data", |
|
314 "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"], |
|
315 ["This is exactly 64 bytes long, not counting the terminating byte", |
|
316 "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"] |
|
317 ]; |
|
318 let bidUser = new BrowserIDManager(); |
|
319 for (let [input,output] of vectors) { |
|
320 do_check_eq(CommonUtils.bytesAsHex(bidUser._sha256(input)), output); |
|
321 } |
|
322 run_next_test(); |
|
323 }); |
|
324 |
|
325 add_test(function test_computeXClientStateHeader() { |
|
326 let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d"; |
|
327 let kB = CommonUtils.hexToBytes(kBhex); |
|
328 |
|
329 let bidUser = new BrowserIDManager(); |
|
330 let header = bidUser._computeXClientState(kB); |
|
331 |
|
332 do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f"); |
|
333 run_next_test(); |
|
334 }); |
|
335 |
|
336 add_task(function test_getTokenErrors() { |
|
337 _("BrowserIDManager correctly handles various failures to get a token."); |
|
338 |
|
339 _("Arrange for a 401 - Sync should reflect an auth error."); |
|
340 initializeIdentityWithTokenServerResponse({ |
|
341 status: 401, |
|
342 headers: {"content-type": "application/json"}, |
|
343 body: JSON.stringify({}), |
|
344 }); |
|
345 let browseridManager = Service.identity; |
|
346 |
|
347 yield browseridManager.initializeWithCurrentIdentity(); |
|
348 yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, |
|
349 "should reject due to 401"); |
|
350 Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); |
|
351 |
|
352 // XXX - other interesting responses to return? |
|
353 |
|
354 // And for good measure, some totally "unexpected" errors - we generally |
|
355 // assume these problems are going to magically go away at some point. |
|
356 _("Arrange for an empty body with a 200 response - should reflect a network error."); |
|
357 initializeIdentityWithTokenServerResponse({ |
|
358 status: 200, |
|
359 headers: [], |
|
360 body: "", |
|
361 }); |
|
362 browseridManager = Service.identity; |
|
363 yield browseridManager.initializeWithCurrentIdentity(); |
|
364 yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, |
|
365 "should reject due to non-JSON response"); |
|
366 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); |
|
367 }); |
|
368 |
|
369 add_task(function test_getTokenErrorWithRetry() { |
|
370 _("tokenserver sends an observer notification on various backoff headers."); |
|
371 |
|
372 // Set Sync's backoffInterval to zero - after we simulated the backoff header |
|
373 // it should reflect the value we sent. |
|
374 Status.backoffInterval = 0; |
|
375 _("Arrange for a 503 with a Retry-After header."); |
|
376 initializeIdentityWithTokenServerResponse({ |
|
377 status: 503, |
|
378 headers: {"content-type": "application/json", |
|
379 "retry-after": "100"}, |
|
380 body: JSON.stringify({}), |
|
381 }); |
|
382 let browseridManager = Service.identity; |
|
383 |
|
384 yield browseridManager.initializeWithCurrentIdentity(); |
|
385 yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, |
|
386 "should reject due to 503"); |
|
387 |
|
388 // The observer should have fired - check it got the value in the response. |
|
389 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); |
|
390 // Sync will have the value in ms with some slop - so check it is at least that. |
|
391 Assert.ok(Status.backoffInterval >= 100000); |
|
392 |
|
393 _("Arrange for a 200 with an X-Backoff header."); |
|
394 Status.backoffInterval = 0; |
|
395 initializeIdentityWithTokenServerResponse({ |
|
396 status: 503, |
|
397 headers: {"content-type": "application/json", |
|
398 "x-backoff": "200"}, |
|
399 body: JSON.stringify({}), |
|
400 }); |
|
401 browseridManager = Service.identity; |
|
402 |
|
403 yield browseridManager.initializeWithCurrentIdentity(); |
|
404 yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, |
|
405 "should reject due to no token in response"); |
|
406 |
|
407 // The observer should have fired - check it got the value in the response. |
|
408 Assert.ok(Status.backoffInterval >= 200000); |
|
409 }); |
|
410 |
|
411 add_task(function test_getKeysErrorWithBackoff() { |
|
412 _("Auth server (via hawk) sends an observer notification on backoff headers."); |
|
413 |
|
414 // Set Sync's backoffInterval to zero - after we simulated the backoff header |
|
415 // it should reflect the value we sent. |
|
416 Status.backoffInterval = 0; |
|
417 _("Arrange for a 503 with a X-Backoff header."); |
|
418 |
|
419 let config = makeIdentityConfig(); |
|
420 // We want no kA or kB so we attempt to fetch them. |
|
421 delete config.fxaccount.user.kA; |
|
422 delete config.fxaccount.user.kB; |
|
423 config.fxaccount.user.keyFetchToken = "keyfetchtoken"; |
|
424 yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { |
|
425 Assert.equal(method, "get"); |
|
426 Assert.equal(uri, "http://mockedserver:9999/account/keys") |
|
427 return { |
|
428 status: 503, |
|
429 headers: {"content-type": "application/json", |
|
430 "x-backoff": "100"}, |
|
431 body: "{}", |
|
432 } |
|
433 }); |
|
434 |
|
435 let browseridManager = Service.identity; |
|
436 yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, |
|
437 "should reject due to 503"); |
|
438 |
|
439 // The observer should have fired - check it got the value in the response. |
|
440 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); |
|
441 // Sync will have the value in ms with some slop - so check it is at least that. |
|
442 Assert.ok(Status.backoffInterval >= 100000); |
|
443 }); |
|
444 |
|
445 add_task(function test_getKeysErrorWithRetry() { |
|
446 _("Auth server (via hawk) sends an observer notification on retry headers."); |
|
447 |
|
448 // Set Sync's backoffInterval to zero - after we simulated the backoff header |
|
449 // it should reflect the value we sent. |
|
450 Status.backoffInterval = 0; |
|
451 _("Arrange for a 503 with a Retry-After header."); |
|
452 |
|
453 let config = makeIdentityConfig(); |
|
454 // We want no kA or kB so we attempt to fetch them. |
|
455 delete config.fxaccount.user.kA; |
|
456 delete config.fxaccount.user.kB; |
|
457 config.fxaccount.user.keyFetchToken = "keyfetchtoken"; |
|
458 yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { |
|
459 Assert.equal(method, "get"); |
|
460 Assert.equal(uri, "http://mockedserver:9999/account/keys") |
|
461 return { |
|
462 status: 503, |
|
463 headers: {"content-type": "application/json", |
|
464 "retry-after": "100"}, |
|
465 body: "{}", |
|
466 } |
|
467 }); |
|
468 |
|
469 let browseridManager = Service.identity; |
|
470 yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, |
|
471 "should reject due to 503"); |
|
472 |
|
473 // The observer should have fired - check it got the value in the response. |
|
474 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); |
|
475 // Sync will have the value in ms with some slop - so check it is at least that. |
|
476 Assert.ok(Status.backoffInterval >= 100000); |
|
477 }); |
|
478 |
|
479 add_task(function test_getHAWKErrors() { |
|
480 _("BrowserIDManager correctly handles various HAWK failures."); |
|
481 |
|
482 _("Arrange for a 401 - Sync should reflect an auth error."); |
|
483 let config = makeIdentityConfig(); |
|
484 yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { |
|
485 Assert.equal(method, "post"); |
|
486 Assert.equal(uri, "http://mockedserver:9999/certificate/sign") |
|
487 return { |
|
488 status: 401, |
|
489 headers: {"content-type": "application/json"}, |
|
490 body: JSON.stringify({}), |
|
491 } |
|
492 }); |
|
493 Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); |
|
494 |
|
495 // XXX - other interesting responses to return? |
|
496 |
|
497 // And for good measure, some totally "unexpected" errors - we generally |
|
498 // assume these problems are going to magically go away at some point. |
|
499 _("Arrange for an empty body with a 200 response - should reflect a network error."); |
|
500 yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { |
|
501 Assert.equal(method, "post"); |
|
502 Assert.equal(uri, "http://mockedserver:9999/certificate/sign") |
|
503 return { |
|
504 status: 200, |
|
505 headers: [], |
|
506 body: "", |
|
507 } |
|
508 }); |
|
509 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); |
|
510 }); |
|
511 |
|
512 add_task(function test_getGetKeysFailing401() { |
|
513 _("BrowserIDManager correctly handles 401 responses fetching keys."); |
|
514 |
|
515 _("Arrange for a 401 - Sync should reflect an auth error."); |
|
516 let config = makeIdentityConfig(); |
|
517 // We want no kA or kB so we attempt to fetch them. |
|
518 delete config.fxaccount.user.kA; |
|
519 delete config.fxaccount.user.kB; |
|
520 config.fxaccount.user.keyFetchToken = "keyfetchtoken"; |
|
521 yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { |
|
522 Assert.equal(method, "get"); |
|
523 Assert.equal(uri, "http://mockedserver:9999/account/keys") |
|
524 return { |
|
525 status: 401, |
|
526 headers: {"content-type": "application/json"}, |
|
527 body: "{}", |
|
528 } |
|
529 }); |
|
530 Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); |
|
531 }); |
|
532 |
|
533 add_task(function test_getGetKeysFailing503() { |
|
534 _("BrowserIDManager correctly handles 5XX responses fetching keys."); |
|
535 |
|
536 _("Arrange for a 503 - Sync should reflect a network error."); |
|
537 let config = makeIdentityConfig(); |
|
538 // We want no kA or kB so we attempt to fetch them. |
|
539 delete config.fxaccount.user.kA; |
|
540 delete config.fxaccount.user.kB; |
|
541 config.fxaccount.user.keyFetchToken = "keyfetchtoken"; |
|
542 yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { |
|
543 Assert.equal(method, "get"); |
|
544 Assert.equal(uri, "http://mockedserver:9999/account/keys") |
|
545 return { |
|
546 status: 503, |
|
547 headers: {"content-type": "application/json"}, |
|
548 body: "{}", |
|
549 } |
|
550 }); |
|
551 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error"); |
|
552 }); |
|
553 |
|
554 add_task(function test_getKeysMissing() { |
|
555 _("BrowserIDManager correctly handles getKeys succeeding but not returning keys."); |
|
556 |
|
557 let browseridManager = new BrowserIDManager(); |
|
558 let identityConfig = makeIdentityConfig(); |
|
559 // our mock identity config already has kA and kB - remove them or we never |
|
560 // try and fetch them. |
|
561 delete identityConfig.fxaccount.user.kA; |
|
562 delete identityConfig.fxaccount.user.kB; |
|
563 identityConfig.fxaccount.user.keyFetchToken = 'keyFetchToken'; |
|
564 |
|
565 configureFxAccountIdentity(browseridManager, identityConfig); |
|
566 |
|
567 // Mock a fxAccounts object that returns no keys |
|
568 let fxa = new FxAccounts({ |
|
569 fetchAndUnwrapKeys: function () { |
|
570 return Promise.resolve({}); |
|
571 }, |
|
572 fxAccountsClient: new MockFxAccountsClient() |
|
573 }); |
|
574 |
|
575 // Add a mock to the currentAccountState object. |
|
576 fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { |
|
577 this.cert = { |
|
578 validUntil: fxa.internal.now() + CERT_LIFETIME, |
|
579 cert: "certificate", |
|
580 }; |
|
581 return Promise.resolve(this.cert.cert); |
|
582 }; |
|
583 |
|
584 // Ensure the new FxAccounts mock has a signed-in user. |
|
585 fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; |
|
586 |
|
587 browseridManager._fxaService = fxa; |
|
588 |
|
589 yield browseridManager.initializeWithCurrentIdentity(); |
|
590 |
|
591 let ex; |
|
592 try { |
|
593 yield browseridManager.whenReadyToAuthenticate.promise; |
|
594 } catch (e) { |
|
595 ex = e; |
|
596 } |
|
597 |
|
598 Assert.ok(ex.message.indexOf("missing kA or kB") >= 0); |
|
599 }); |
|
600 |
|
601 // End of tests |
|
602 // Utility functions follow |
|
603 |
|
604 // Create a new browserid_identity object and initialize it with a |
|
605 // hawk mock that simulates HTTP responses. |
|
606 // The callback function will be called each time the mocked hawk server wants |
|
607 // to make a request. The result of the callback should be the mock response |
|
608 // object that will be returned to hawk. |
|
609 // A token server mock will be used that doesn't hit a server, so we move |
|
610 // directly to a hawk request. |
|
611 function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) { |
|
612 // A mock request object. |
|
613 function MockRESTRequest(uri, credentials, extra) { |
|
614 this._uri = uri; |
|
615 this._credentials = credentials; |
|
616 this._extra = extra; |
|
617 }; |
|
618 MockRESTRequest.prototype = { |
|
619 setHeader: function() {}, |
|
620 post: function(data, callback) { |
|
621 this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra); |
|
622 callback.call(this); |
|
623 }, |
|
624 get: function(callback) { |
|
625 this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra); |
|
626 callback.call(this); |
|
627 } |
|
628 } |
|
629 |
|
630 // The hawk client. |
|
631 function MockedHawkClient() {} |
|
632 MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999"); |
|
633 MockedHawkClient.prototype.constructor = MockedHawkClient; |
|
634 MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) { |
|
635 return new MockRESTRequest(uri, credentials, extra); |
|
636 } |
|
637 // Arrange for the same observerPrefix as FxAccountsClient uses |
|
638 MockedHawkClient.prototype.observerPrefix = "FxA:hawk"; |
|
639 |
|
640 // tie it all together - configureFxAccountIdentity isn't useful here :( |
|
641 let fxaClient = new MockFxAccountsClient(); |
|
642 fxaClient.hawk = new MockedHawkClient(); |
|
643 let internal = { |
|
644 fxAccountsClient: fxaClient, |
|
645 } |
|
646 let fxa = new FxAccounts(internal); |
|
647 fxa.internal.currentAccountState.signedInUser = { |
|
648 accountData: config.fxaccount.user, |
|
649 }; |
|
650 |
|
651 browseridManager._fxaService = fxa; |
|
652 browseridManager._signedInUser = null; |
|
653 yield browseridManager.initializeWithCurrentIdentity(); |
|
654 yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, |
|
655 "expecting rejection due to hawk error"); |
|
656 } |
|
657 |
|
658 |
|
659 function getTimestamp(hawkAuthHeader) { |
|
660 return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS; |
|
661 } |
|
662 |
|
663 function getTimestampDelta(hawkAuthHeader, now=Date.now()) { |
|
664 return Math.abs(getTimestamp(hawkAuthHeader) - now); |
|
665 } |
|
666 |