1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/tests/unit/test_browserid_identity.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,666 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +Cu.import("resource://gre/modules/FxAccounts.jsm"); 1.8 +Cu.import("resource://services-sync/browserid_identity.js"); 1.9 +Cu.import("resource://services-sync/rest.js"); 1.10 +Cu.import("resource://services-sync/util.js"); 1.11 +Cu.import("resource://services-common/utils.js"); 1.12 +Cu.import("resource://services-crypto/utils.js"); 1.13 +Cu.import("resource://testing-common/services/sync/utils.js"); 1.14 +Cu.import("resource://testing-common/services/sync/fxa_utils.js"); 1.15 +Cu.import("resource://services-common/hawkclient.js"); 1.16 +Cu.import("resource://gre/modules/FxAccounts.jsm"); 1.17 +Cu.import("resource://gre/modules/FxAccountsClient.jsm"); 1.18 +Cu.import("resource://gre/modules/FxAccountsCommon.js"); 1.19 +Cu.import("resource://services-sync/service.js"); 1.20 +Cu.import("resource://services-sync/status.js"); 1.21 +Cu.import("resource://services-sync/constants.js"); 1.22 + 1.23 +const SECOND_MS = 1000; 1.24 +const MINUTE_MS = SECOND_MS * 60; 1.25 +const HOUR_MS = MINUTE_MS * 60; 1.26 + 1.27 +let identityConfig = makeIdentityConfig(); 1.28 +let browseridManager = new BrowserIDManager(); 1.29 +configureFxAccountIdentity(browseridManager, identityConfig); 1.30 + 1.31 +/** 1.32 + * Mock client clock and skew vs server in FxAccounts signed-in user module and 1.33 + * API client. browserid_identity.js queries these values to construct HAWK 1.34 + * headers. We will use this to test clock skew compensation in these headers 1.35 + * below. 1.36 + */ 1.37 +let MockFxAccountsClient = function() { 1.38 + FxAccountsClient.apply(this); 1.39 +}; 1.40 +MockFxAccountsClient.prototype = { 1.41 + __proto__: FxAccountsClient.prototype 1.42 +}; 1.43 + 1.44 +function MockFxAccounts() { 1.45 + let fxa = new FxAccounts({ 1.46 + _now_is: Date.now(), 1.47 + 1.48 + now: function () { 1.49 + return this._now_is; 1.50 + }, 1.51 + 1.52 + fxAccountsClient: new MockFxAccountsClient() 1.53 + }); 1.54 + fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { 1.55 + this.cert = { 1.56 + validUntil: fxa.internal.now() + CERT_LIFETIME, 1.57 + cert: "certificate", 1.58 + }; 1.59 + return Promise.resolve(this.cert.cert); 1.60 + }; 1.61 + return fxa; 1.62 +} 1.63 + 1.64 +function run_test() { 1.65 + initTestLogging("Trace"); 1.66 + Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace; 1.67 + Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace; 1.68 + run_next_test(); 1.69 +}; 1.70 + 1.71 +add_test(function test_initial_state() { 1.72 + _("Verify initial state"); 1.73 + do_check_false(!!browseridManager._token); 1.74 + do_check_false(browseridManager.hasValidToken()); 1.75 + run_next_test(); 1.76 + } 1.77 +); 1.78 + 1.79 +add_task(function test_initialializeWithCurrentIdentity() { 1.80 + _("Verify start after initializeWithCurrentIdentity"); 1.81 + browseridManager.initializeWithCurrentIdentity(); 1.82 + yield browseridManager.whenReadyToAuthenticate.promise; 1.83 + do_check_true(!!browseridManager._token); 1.84 + do_check_true(browseridManager.hasValidToken()); 1.85 + do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email); 1.86 + } 1.87 +); 1.88 + 1.89 + 1.90 +add_test(function test_getResourceAuthenticator() { 1.91 + _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header."); 1.92 + let authenticator = browseridManager.getResourceAuthenticator(); 1.93 + do_check_true(!!authenticator); 1.94 + let req = {uri: CommonUtils.makeURI( 1.95 + "https://example.net/somewhere/over/the/rainbow"), 1.96 + method: 'GET'}; 1.97 + let output = authenticator(req, 'GET'); 1.98 + do_check_true('headers' in output); 1.99 + do_check_true('authorization' in output.headers); 1.100 + do_check_true(output.headers.authorization.startsWith('Hawk')); 1.101 + _("Expected internal state after successful call."); 1.102 + do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid); 1.103 + run_next_test(); 1.104 + } 1.105 +); 1.106 + 1.107 +add_test(function test_getRESTRequestAuthenticator() { 1.108 + _("BrowserIDManager supplies a REST Request Authenticator callback which sets a Hawk header on a request object."); 1.109 + let request = new SyncStorageRequest( 1.110 + "https://example.net/somewhere/over/the/rainbow"); 1.111 + let authenticator = browseridManager.getRESTRequestAuthenticator(); 1.112 + do_check_true(!!authenticator); 1.113 + let output = authenticator(request, 'GET'); 1.114 + do_check_eq(request.uri, output.uri); 1.115 + do_check_true(output._headers.authorization.startsWith('Hawk')); 1.116 + do_check_true(output._headers.authorization.contains('nonce')); 1.117 + do_check_true(browseridManager.hasValidToken()); 1.118 + run_next_test(); 1.119 + } 1.120 +); 1.121 + 1.122 +add_test(function test_resourceAuthenticatorSkew() { 1.123 + _("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header."); 1.124 + 1.125 + // Clock is skewed 12 hours into the future 1.126 + // We pick a date in the past so we don't risk concealing bugs in code that 1.127 + // uses new Date() instead of our given date. 1.128 + let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; 1.129 + let browseridManager = new BrowserIDManager(); 1.130 + let hawkClient = new HawkClient("https://example.net/v1", "/foo"); 1.131 + 1.132 + // mock fxa hawk client skew 1.133 + hawkClient.now = function() { 1.134 + dump("mocked client now: " + now + '\n'); 1.135 + return now; 1.136 + } 1.137 + // Imagine there's already been one fxa request and the hawk client has 1.138 + // already detected skew vs the fxa auth server. 1.139 + let localtimeOffsetMsec = -1 * 12 * HOUR_MS; 1.140 + hawkClient._localtimeOffsetMsec = localtimeOffsetMsec; 1.141 + 1.142 + let fxaClient = new MockFxAccountsClient(); 1.143 + fxaClient.hawk = hawkClient; 1.144 + 1.145 + // Sanity check 1.146 + do_check_eq(hawkClient.now(), now); 1.147 + do_check_eq(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec); 1.148 + 1.149 + // Properly picked up by the client 1.150 + do_check_eq(fxaClient.now(), now); 1.151 + do_check_eq(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec); 1.152 + 1.153 + let fxa = new MockFxAccounts(); 1.154 + fxa.internal._now_is = now; 1.155 + fxa.internal.fxAccountsClient = fxaClient; 1.156 + 1.157 + // Picked up by the signed-in user module 1.158 + do_check_eq(fxa.internal.now(), now); 1.159 + do_check_eq(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec); 1.160 + 1.161 + do_check_eq(fxa.now(), now); 1.162 + do_check_eq(fxa.localtimeOffsetMsec, localtimeOffsetMsec); 1.163 + 1.164 + // Mocks within mocks... 1.165 + configureFxAccountIdentity(browseridManager, identityConfig); 1.166 + 1.167 + // Ensure the new FxAccounts mock has a signed-in user. 1.168 + fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; 1.169 + 1.170 + browseridManager._fxaService = fxa; 1.171 + 1.172 + do_check_eq(browseridManager._fxaService.internal.now(), now); 1.173 + do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec, 1.174 + localtimeOffsetMsec); 1.175 + 1.176 + do_check_eq(browseridManager._fxaService.now(), now); 1.177 + do_check_eq(browseridManager._fxaService.localtimeOffsetMsec, 1.178 + localtimeOffsetMsec); 1.179 + 1.180 + let request = new SyncStorageRequest("https://example.net/i/like/pie/"); 1.181 + let authenticator = browseridManager.getResourceAuthenticator(); 1.182 + let output = authenticator(request, 'GET'); 1.183 + dump("output" + JSON.stringify(output)); 1.184 + let authHeader = output.headers.authorization; 1.185 + do_check_true(authHeader.startsWith('Hawk')); 1.186 + 1.187 + // Skew correction is applied in the header and we're within the two-minute 1.188 + // window. 1.189 + do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); 1.190 + do_check_true( 1.191 + (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); 1.192 + 1.193 + run_next_test(); 1.194 +}); 1.195 + 1.196 +add_test(function test_RESTResourceAuthenticatorSkew() { 1.197 + _("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header."); 1.198 + 1.199 + // Clock is skewed 12 hours into the future from our arbitary date 1.200 + let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; 1.201 + let browseridManager = new BrowserIDManager(); 1.202 + let hawkClient = new HawkClient("https://example.net/v1", "/foo"); 1.203 + 1.204 + // mock fxa hawk client skew 1.205 + hawkClient.now = function() { 1.206 + return now; 1.207 + } 1.208 + // Imagine there's already been one fxa request and the hawk client has 1.209 + // already detected skew vs the fxa auth server. 1.210 + hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS; 1.211 + 1.212 + let fxaClient = new MockFxAccountsClient(); 1.213 + fxaClient.hawk = hawkClient; 1.214 + let fxa = new MockFxAccounts(); 1.215 + fxa.internal._now_is = now; 1.216 + fxa.internal.fxAccountsClient = fxaClient; 1.217 + 1.218 + configureFxAccountIdentity(browseridManager, identityConfig); 1.219 + 1.220 + // Ensure the new FxAccounts mock has a signed-in user. 1.221 + fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; 1.222 + 1.223 + browseridManager._fxaService = fxa; 1.224 + 1.225 + do_check_eq(browseridManager._fxaService.internal.now(), now); 1.226 + 1.227 + let request = new SyncStorageRequest("https://example.net/i/like/pie/"); 1.228 + let authenticator = browseridManager.getResourceAuthenticator(); 1.229 + let output = authenticator(request, 'GET'); 1.230 + dump("output" + JSON.stringify(output)); 1.231 + let authHeader = output.headers.authorization; 1.232 + do_check_true(authHeader.startsWith('Hawk')); 1.233 + 1.234 + // Skew correction is applied in the header and we're within the two-minute 1.235 + // window. 1.236 + do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); 1.237 + do_check_true( 1.238 + (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); 1.239 + 1.240 + run_next_test(); 1.241 +}); 1.242 + 1.243 +add_task(function test_ensureLoggedIn() { 1.244 + configureFxAccountIdentity(browseridManager); 1.245 + yield browseridManager.initializeWithCurrentIdentity(); 1.246 + Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked"); 1.247 + yield browseridManager.ensureLoggedIn(); 1.248 + Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked"); 1.249 + Assert.ok(browseridManager._shouldHaveSyncKeyBundle, 1.250 + "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); 1.251 + 1.252 + // arrange for no logged in user. 1.253 + let fxa = browseridManager._fxaService 1.254 + let signedInUser = fxa.internal.currentAccountState.signedInUser; 1.255 + fxa.internal.currentAccountState.signedInUser = null; 1.256 + browseridManager.initializeWithCurrentIdentity(); 1.257 + Assert.ok(!browseridManager._shouldHaveSyncKeyBundle, 1.258 + "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are."); 1.259 + Status.login = LOGIN_FAILED_NO_USERNAME; 1.260 + yield Assert_rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user"); 1.261 + Assert.ok(browseridManager._shouldHaveSyncKeyBundle, 1.262 + "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); 1.263 + fxa.internal.currentAccountState.signedInUser = signedInUser; 1.264 + Status.login = LOGIN_FAILED_LOGIN_REJECTED; 1.265 + yield Assert_rejects(browseridManager.ensureLoggedIn(), 1.266 + "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection"); 1.267 + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, 1.268 + "status should remain LOGIN_FAILED_LOGIN_REJECTED"); 1.269 + Status.login = LOGIN_FAILED_NETWORK_ERROR; 1.270 + yield browseridManager.ensureLoggedIn(); 1.271 + Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked"); 1.272 +}); 1.273 + 1.274 +add_test(function test_tokenExpiration() { 1.275 + _("BrowserIDManager notices token expiration:"); 1.276 + let bimExp = new BrowserIDManager(); 1.277 + configureFxAccountIdentity(bimExp, identityConfig); 1.278 + 1.279 + let authenticator = bimExp.getResourceAuthenticator(); 1.280 + do_check_true(!!authenticator); 1.281 + let req = {uri: CommonUtils.makeURI( 1.282 + "https://example.net/somewhere/over/the/rainbow"), 1.283 + method: 'GET'}; 1.284 + authenticator(req, 'GET'); 1.285 + 1.286 + // Mock the clock. 1.287 + _("Forcing the token to expire ..."); 1.288 + Object.defineProperty(bimExp, "_now", { 1.289 + value: function customNow() { 1.290 + return (Date.now() + 3000001); 1.291 + }, 1.292 + writable: true, 1.293 + }); 1.294 + do_check_true(bimExp._token.expiration < bimExp._now()); 1.295 + _("... means BrowserIDManager knows to re-fetch it on the next call."); 1.296 + do_check_false(bimExp.hasValidToken()); 1.297 + run_next_test(); 1.298 + } 1.299 +); 1.300 + 1.301 +add_test(function test_sha256() { 1.302 + // Test vectors from http://www.bichlmeier.info/sha256test.html 1.303 + let vectors = [ 1.304 + ["", 1.305 + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"], 1.306 + ["abc", 1.307 + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"], 1.308 + ["message digest", 1.309 + "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"], 1.310 + ["secure hash algorithm", 1.311 + "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"], 1.312 + ["SHA256 is considered to be safe", 1.313 + "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"], 1.314 + ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 1.315 + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"], 1.316 + ["For this sample, this 63-byte string will be used as input data", 1.317 + "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"], 1.318 + ["This is exactly 64 bytes long, not counting the terminating byte", 1.319 + "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"] 1.320 + ]; 1.321 + let bidUser = new BrowserIDManager(); 1.322 + for (let [input,output] of vectors) { 1.323 + do_check_eq(CommonUtils.bytesAsHex(bidUser._sha256(input)), output); 1.324 + } 1.325 + run_next_test(); 1.326 +}); 1.327 + 1.328 +add_test(function test_computeXClientStateHeader() { 1.329 + let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d"; 1.330 + let kB = CommonUtils.hexToBytes(kBhex); 1.331 + 1.332 + let bidUser = new BrowserIDManager(); 1.333 + let header = bidUser._computeXClientState(kB); 1.334 + 1.335 + do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f"); 1.336 + run_next_test(); 1.337 +}); 1.338 + 1.339 +add_task(function test_getTokenErrors() { 1.340 + _("BrowserIDManager correctly handles various failures to get a token."); 1.341 + 1.342 + _("Arrange for a 401 - Sync should reflect an auth error."); 1.343 + initializeIdentityWithTokenServerResponse({ 1.344 + status: 401, 1.345 + headers: {"content-type": "application/json"}, 1.346 + body: JSON.stringify({}), 1.347 + }); 1.348 + let browseridManager = Service.identity; 1.349 + 1.350 + yield browseridManager.initializeWithCurrentIdentity(); 1.351 + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, 1.352 + "should reject due to 401"); 1.353 + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); 1.354 + 1.355 + // XXX - other interesting responses to return? 1.356 + 1.357 + // And for good measure, some totally "unexpected" errors - we generally 1.358 + // assume these problems are going to magically go away at some point. 1.359 + _("Arrange for an empty body with a 200 response - should reflect a network error."); 1.360 + initializeIdentityWithTokenServerResponse({ 1.361 + status: 200, 1.362 + headers: [], 1.363 + body: "", 1.364 + }); 1.365 + browseridManager = Service.identity; 1.366 + yield browseridManager.initializeWithCurrentIdentity(); 1.367 + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, 1.368 + "should reject due to non-JSON response"); 1.369 + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); 1.370 +}); 1.371 + 1.372 +add_task(function test_getTokenErrorWithRetry() { 1.373 + _("tokenserver sends an observer notification on various backoff headers."); 1.374 + 1.375 + // Set Sync's backoffInterval to zero - after we simulated the backoff header 1.376 + // it should reflect the value we sent. 1.377 + Status.backoffInterval = 0; 1.378 + _("Arrange for a 503 with a Retry-After header."); 1.379 + initializeIdentityWithTokenServerResponse({ 1.380 + status: 503, 1.381 + headers: {"content-type": "application/json", 1.382 + "retry-after": "100"}, 1.383 + body: JSON.stringify({}), 1.384 + }); 1.385 + let browseridManager = Service.identity; 1.386 + 1.387 + yield browseridManager.initializeWithCurrentIdentity(); 1.388 + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, 1.389 + "should reject due to 503"); 1.390 + 1.391 + // The observer should have fired - check it got the value in the response. 1.392 + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); 1.393 + // Sync will have the value in ms with some slop - so check it is at least that. 1.394 + Assert.ok(Status.backoffInterval >= 100000); 1.395 + 1.396 + _("Arrange for a 200 with an X-Backoff header."); 1.397 + Status.backoffInterval = 0; 1.398 + initializeIdentityWithTokenServerResponse({ 1.399 + status: 503, 1.400 + headers: {"content-type": "application/json", 1.401 + "x-backoff": "200"}, 1.402 + body: JSON.stringify({}), 1.403 + }); 1.404 + browseridManager = Service.identity; 1.405 + 1.406 + yield browseridManager.initializeWithCurrentIdentity(); 1.407 + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, 1.408 + "should reject due to no token in response"); 1.409 + 1.410 + // The observer should have fired - check it got the value in the response. 1.411 + Assert.ok(Status.backoffInterval >= 200000); 1.412 +}); 1.413 + 1.414 +add_task(function test_getKeysErrorWithBackoff() { 1.415 + _("Auth server (via hawk) sends an observer notification on backoff headers."); 1.416 + 1.417 + // Set Sync's backoffInterval to zero - after we simulated the backoff header 1.418 + // it should reflect the value we sent. 1.419 + Status.backoffInterval = 0; 1.420 + _("Arrange for a 503 with a X-Backoff header."); 1.421 + 1.422 + let config = makeIdentityConfig(); 1.423 + // We want no kA or kB so we attempt to fetch them. 1.424 + delete config.fxaccount.user.kA; 1.425 + delete config.fxaccount.user.kB; 1.426 + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; 1.427 + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { 1.428 + Assert.equal(method, "get"); 1.429 + Assert.equal(uri, "http://mockedserver:9999/account/keys") 1.430 + return { 1.431 + status: 503, 1.432 + headers: {"content-type": "application/json", 1.433 + "x-backoff": "100"}, 1.434 + body: "{}", 1.435 + } 1.436 + }); 1.437 + 1.438 + let browseridManager = Service.identity; 1.439 + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, 1.440 + "should reject due to 503"); 1.441 + 1.442 + // The observer should have fired - check it got the value in the response. 1.443 + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); 1.444 + // Sync will have the value in ms with some slop - so check it is at least that. 1.445 + Assert.ok(Status.backoffInterval >= 100000); 1.446 +}); 1.447 + 1.448 +add_task(function test_getKeysErrorWithRetry() { 1.449 + _("Auth server (via hawk) sends an observer notification on retry headers."); 1.450 + 1.451 + // Set Sync's backoffInterval to zero - after we simulated the backoff header 1.452 + // it should reflect the value we sent. 1.453 + Status.backoffInterval = 0; 1.454 + _("Arrange for a 503 with a Retry-After header."); 1.455 + 1.456 + let config = makeIdentityConfig(); 1.457 + // We want no kA or kB so we attempt to fetch them. 1.458 + delete config.fxaccount.user.kA; 1.459 + delete config.fxaccount.user.kB; 1.460 + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; 1.461 + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { 1.462 + Assert.equal(method, "get"); 1.463 + Assert.equal(uri, "http://mockedserver:9999/account/keys") 1.464 + return { 1.465 + status: 503, 1.466 + headers: {"content-type": "application/json", 1.467 + "retry-after": "100"}, 1.468 + body: "{}", 1.469 + } 1.470 + }); 1.471 + 1.472 + let browseridManager = Service.identity; 1.473 + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, 1.474 + "should reject due to 503"); 1.475 + 1.476 + // The observer should have fired - check it got the value in the response. 1.477 + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); 1.478 + // Sync will have the value in ms with some slop - so check it is at least that. 1.479 + Assert.ok(Status.backoffInterval >= 100000); 1.480 +}); 1.481 + 1.482 +add_task(function test_getHAWKErrors() { 1.483 + _("BrowserIDManager correctly handles various HAWK failures."); 1.484 + 1.485 + _("Arrange for a 401 - Sync should reflect an auth error."); 1.486 + let config = makeIdentityConfig(); 1.487 + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { 1.488 + Assert.equal(method, "post"); 1.489 + Assert.equal(uri, "http://mockedserver:9999/certificate/sign") 1.490 + return { 1.491 + status: 401, 1.492 + headers: {"content-type": "application/json"}, 1.493 + body: JSON.stringify({}), 1.494 + } 1.495 + }); 1.496 + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); 1.497 + 1.498 + // XXX - other interesting responses to return? 1.499 + 1.500 + // And for good measure, some totally "unexpected" errors - we generally 1.501 + // assume these problems are going to magically go away at some point. 1.502 + _("Arrange for an empty body with a 200 response - should reflect a network error."); 1.503 + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { 1.504 + Assert.equal(method, "post"); 1.505 + Assert.equal(uri, "http://mockedserver:9999/certificate/sign") 1.506 + return { 1.507 + status: 200, 1.508 + headers: [], 1.509 + body: "", 1.510 + } 1.511 + }); 1.512 + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); 1.513 +}); 1.514 + 1.515 +add_task(function test_getGetKeysFailing401() { 1.516 + _("BrowserIDManager correctly handles 401 responses fetching keys."); 1.517 + 1.518 + _("Arrange for a 401 - Sync should reflect an auth error."); 1.519 + let config = makeIdentityConfig(); 1.520 + // We want no kA or kB so we attempt to fetch them. 1.521 + delete config.fxaccount.user.kA; 1.522 + delete config.fxaccount.user.kB; 1.523 + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; 1.524 + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { 1.525 + Assert.equal(method, "get"); 1.526 + Assert.equal(uri, "http://mockedserver:9999/account/keys") 1.527 + return { 1.528 + status: 401, 1.529 + headers: {"content-type": "application/json"}, 1.530 + body: "{}", 1.531 + } 1.532 + }); 1.533 + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); 1.534 +}); 1.535 + 1.536 +add_task(function test_getGetKeysFailing503() { 1.537 + _("BrowserIDManager correctly handles 5XX responses fetching keys."); 1.538 + 1.539 + _("Arrange for a 503 - Sync should reflect a network error."); 1.540 + let config = makeIdentityConfig(); 1.541 + // We want no kA or kB so we attempt to fetch them. 1.542 + delete config.fxaccount.user.kA; 1.543 + delete config.fxaccount.user.kB; 1.544 + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; 1.545 + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { 1.546 + Assert.equal(method, "get"); 1.547 + Assert.equal(uri, "http://mockedserver:9999/account/keys") 1.548 + return { 1.549 + status: 503, 1.550 + headers: {"content-type": "application/json"}, 1.551 + body: "{}", 1.552 + } 1.553 + }); 1.554 + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error"); 1.555 +}); 1.556 + 1.557 +add_task(function test_getKeysMissing() { 1.558 + _("BrowserIDManager correctly handles getKeys succeeding but not returning keys."); 1.559 + 1.560 + let browseridManager = new BrowserIDManager(); 1.561 + let identityConfig = makeIdentityConfig(); 1.562 + // our mock identity config already has kA and kB - remove them or we never 1.563 + // try and fetch them. 1.564 + delete identityConfig.fxaccount.user.kA; 1.565 + delete identityConfig.fxaccount.user.kB; 1.566 + identityConfig.fxaccount.user.keyFetchToken = 'keyFetchToken'; 1.567 + 1.568 + configureFxAccountIdentity(browseridManager, identityConfig); 1.569 + 1.570 + // Mock a fxAccounts object that returns no keys 1.571 + let fxa = new FxAccounts({ 1.572 + fetchAndUnwrapKeys: function () { 1.573 + return Promise.resolve({}); 1.574 + }, 1.575 + fxAccountsClient: new MockFxAccountsClient() 1.576 + }); 1.577 + 1.578 + // Add a mock to the currentAccountState object. 1.579 + fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { 1.580 + this.cert = { 1.581 + validUntil: fxa.internal.now() + CERT_LIFETIME, 1.582 + cert: "certificate", 1.583 + }; 1.584 + return Promise.resolve(this.cert.cert); 1.585 + }; 1.586 + 1.587 + // Ensure the new FxAccounts mock has a signed-in user. 1.588 + fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; 1.589 + 1.590 + browseridManager._fxaService = fxa; 1.591 + 1.592 + yield browseridManager.initializeWithCurrentIdentity(); 1.593 + 1.594 + let ex; 1.595 + try { 1.596 + yield browseridManager.whenReadyToAuthenticate.promise; 1.597 + } catch (e) { 1.598 + ex = e; 1.599 + } 1.600 + 1.601 + Assert.ok(ex.message.indexOf("missing kA or kB") >= 0); 1.602 +}); 1.603 + 1.604 +// End of tests 1.605 +// Utility functions follow 1.606 + 1.607 +// Create a new browserid_identity object and initialize it with a 1.608 +// hawk mock that simulates HTTP responses. 1.609 +// The callback function will be called each time the mocked hawk server wants 1.610 +// to make a request. The result of the callback should be the mock response 1.611 +// object that will be returned to hawk. 1.612 +// A token server mock will be used that doesn't hit a server, so we move 1.613 +// directly to a hawk request. 1.614 +function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) { 1.615 + // A mock request object. 1.616 + function MockRESTRequest(uri, credentials, extra) { 1.617 + this._uri = uri; 1.618 + this._credentials = credentials; 1.619 + this._extra = extra; 1.620 + }; 1.621 + MockRESTRequest.prototype = { 1.622 + setHeader: function() {}, 1.623 + post: function(data, callback) { 1.624 + this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra); 1.625 + callback.call(this); 1.626 + }, 1.627 + get: function(callback) { 1.628 + this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra); 1.629 + callback.call(this); 1.630 + } 1.631 + } 1.632 + 1.633 + // The hawk client. 1.634 + function MockedHawkClient() {} 1.635 + MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999"); 1.636 + MockedHawkClient.prototype.constructor = MockedHawkClient; 1.637 + MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) { 1.638 + return new MockRESTRequest(uri, credentials, extra); 1.639 + } 1.640 + // Arrange for the same observerPrefix as FxAccountsClient uses 1.641 + MockedHawkClient.prototype.observerPrefix = "FxA:hawk"; 1.642 + 1.643 + // tie it all together - configureFxAccountIdentity isn't useful here :( 1.644 + let fxaClient = new MockFxAccountsClient(); 1.645 + fxaClient.hawk = new MockedHawkClient(); 1.646 + let internal = { 1.647 + fxAccountsClient: fxaClient, 1.648 + } 1.649 + let fxa = new FxAccounts(internal); 1.650 + fxa.internal.currentAccountState.signedInUser = { 1.651 + accountData: config.fxaccount.user, 1.652 + }; 1.653 + 1.654 + browseridManager._fxaService = fxa; 1.655 + browseridManager._signedInUser = null; 1.656 + yield browseridManager.initializeWithCurrentIdentity(); 1.657 + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, 1.658 + "expecting rejection due to hawk error"); 1.659 +} 1.660 + 1.661 + 1.662 +function getTimestamp(hawkAuthHeader) { 1.663 + return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS; 1.664 +} 1.665 + 1.666 +function getTimestampDelta(hawkAuthHeader, now=Date.now()) { 1.667 + return Math.abs(getTimestamp(hawkAuthHeader) - now); 1.668 +} 1.669 +