diff options
Diffstat (limited to 'services/sync/tests/unit/test_browserid_identity.js')
-rw-r--r-- | services/sync/tests/unit/test_browserid_identity.js | 890 |
1 files changed, 890 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_browserid_identity.js b/services/sync/tests/unit/test_browserid_identity.js new file mode 100644 index 000000000..531c01bf6 --- /dev/null +++ b/services/sync/tests/unit/test_browserid_identity.js @@ -0,0 +1,890 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/FxAccounts.jsm"); +Cu.import("resource://services-sync/browserid_identity.js"); +Cu.import("resource://services-sync/rest.js"); +Cu.import("resource://services-sync/util.js"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-crypto/utils.js"); +Cu.import("resource://testing-common/services/sync/utils.js"); +Cu.import("resource://testing-common/services/sync/fxa_utils.js"); +Cu.import("resource://services-common/hawkclient.js"); +Cu.import("resource://gre/modules/FxAccounts.jsm"); +Cu.import("resource://gre/modules/FxAccountsClient.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/status.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-common/tokenserverclient.js"); + +const SECOND_MS = 1000; +const MINUTE_MS = SECOND_MS * 60; +const HOUR_MS = MINUTE_MS * 60; + +var identityConfig = makeIdentityConfig(); +var browseridManager = new BrowserIDManager(); +configureFxAccountIdentity(browseridManager, identityConfig); + +/** + * Mock client clock and skew vs server in FxAccounts signed-in user module and + * API client. browserid_identity.js queries these values to construct HAWK + * headers. We will use this to test clock skew compensation in these headers + * below. + */ +var MockFxAccountsClient = function() { + FxAccountsClient.apply(this); +}; +MockFxAccountsClient.prototype = { + __proto__: FxAccountsClient.prototype, + accountStatus() { + return Promise.resolve(true); + } +}; + +function MockFxAccounts() { + let fxa = new FxAccounts({ + _now_is: Date.now(), + + now: function () { + return this._now_is; + }, + + fxAccountsClient: new MockFxAccountsClient() + }); + fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { + this.cert = { + validUntil: fxa.internal.now() + CERT_LIFETIME, + cert: "certificate", + }; + return Promise.resolve(this.cert.cert); + }; + return fxa; +} + +function run_test() { + initTestLogging("Trace"); + Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace; + Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace; + run_next_test(); +}; + +add_test(function test_initial_state() { + _("Verify initial state"); + do_check_false(!!browseridManager._token); + do_check_false(browseridManager.hasValidToken()); + run_next_test(); + } +); + +add_task(function* test_initialializeWithCurrentIdentity() { + _("Verify start after initializeWithCurrentIdentity"); + browseridManager.initializeWithCurrentIdentity(); + yield browseridManager.whenReadyToAuthenticate.promise; + do_check_true(!!browseridManager._token); + do_check_true(browseridManager.hasValidToken()); + do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email); + } +); + +add_task(function* test_initialializeWithAuthErrorAndDeletedAccount() { + _("Verify sync unpair after initializeWithCurrentIdentity with auth error + account deleted"); + + var identityConfig = makeIdentityConfig(); + var browseridManager = new BrowserIDManager(); + + // Use the real `_getAssertion` method that calls + // `mockFxAClient.signCertificate`. + let fxaInternal = makeFxAccountsInternalMock(identityConfig); + delete fxaInternal._getAssertion; + + configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal); + browseridManager._fxaService.internal.initialize(); + + let signCertificateCalled = false; + let accountStatusCalled = false; + + let MockFxAccountsClient = function() { + FxAccountsClient.apply(this); + }; + MockFxAccountsClient.prototype = { + __proto__: FxAccountsClient.prototype, + signCertificate() { + signCertificateCalled = true; + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }, + accountStatus() { + accountStatusCalled = true; + return Promise.resolve(false); + } + }; + + let mockFxAClient = new MockFxAccountsClient(); + browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient; + + yield browseridManager.initializeWithCurrentIdentity(); + yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to an auth error"); + + do_check_true(signCertificateCalled); + do_check_true(accountStatusCalled); + do_check_false(browseridManager.account); + do_check_false(browseridManager._token); + do_check_false(browseridManager.hasValidToken()); + do_check_false(browseridManager.account); +}); + +add_task(function* test_initialializeWithNoKeys() { + _("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken"); + let identityConfig = makeIdentityConfig(); + delete identityConfig.fxaccount.user.kA; + delete identityConfig.fxaccount.user.kB; + // there's no keyFetchToken by default, so the initialize should fail. + configureFxAccountIdentity(browseridManager, identityConfig); + + yield browseridManager.initializeWithCurrentIdentity(); + yield browseridManager.whenReadyToAuthenticate.promise; + do_check_eq(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys"); + do_check_false(browseridManager._canFetchKeys(), "_canFetchKeys reflects lack of keys"); + do_check_eq(browseridManager._token, null, "we don't have a token"); +}); + +add_test(function test_getResourceAuthenticator() { + _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header."); + configureFxAccountIdentity(browseridManager); + let authenticator = browseridManager.getResourceAuthenticator(); + do_check_true(!!authenticator); + let req = {uri: CommonUtils.makeURI( + "https://example.net/somewhere/over/the/rainbow"), + method: 'GET'}; + let output = authenticator(req, 'GET'); + do_check_true('headers' in output); + do_check_true('authorization' in output.headers); + do_check_true(output.headers.authorization.startsWith('Hawk')); + _("Expected internal state after successful call."); + do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid); + run_next_test(); + } +); + +add_test(function test_getRESTRequestAuthenticator() { + _("BrowserIDManager supplies a REST Request Authenticator callback which sets a Hawk header on a request object."); + let request = new SyncStorageRequest( + "https://example.net/somewhere/over/the/rainbow"); + let authenticator = browseridManager.getRESTRequestAuthenticator(); + do_check_true(!!authenticator); + let output = authenticator(request, 'GET'); + do_check_eq(request.uri, output.uri); + do_check_true(output._headers.authorization.startsWith('Hawk')); + do_check_true(output._headers.authorization.includes('nonce')); + do_check_true(browseridManager.hasValidToken()); + run_next_test(); + } +); + +add_test(function test_resourceAuthenticatorSkew() { + _("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header."); + + // Clock is skewed 12 hours into the future + // We pick a date in the past so we don't risk concealing bugs in code that + // uses new Date() instead of our given date. + let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; + let browseridManager = new BrowserIDManager(); + let hawkClient = new HawkClient("https://example.net/v1", "/foo"); + + // mock fxa hawk client skew + hawkClient.now = function() { + dump("mocked client now: " + now + '\n'); + return now; + } + // Imagine there's already been one fxa request and the hawk client has + // already detected skew vs the fxa auth server. + let localtimeOffsetMsec = -1 * 12 * HOUR_MS; + hawkClient._localtimeOffsetMsec = localtimeOffsetMsec; + + let fxaClient = new MockFxAccountsClient(); + fxaClient.hawk = hawkClient; + + // Sanity check + do_check_eq(hawkClient.now(), now); + do_check_eq(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec); + + // Properly picked up by the client + do_check_eq(fxaClient.now(), now); + do_check_eq(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec); + + let fxa = new MockFxAccounts(); + fxa.internal._now_is = now; + fxa.internal.fxAccountsClient = fxaClient; + + // Picked up by the signed-in user module + do_check_eq(fxa.internal.now(), now); + do_check_eq(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec); + + do_check_eq(fxa.now(), now); + do_check_eq(fxa.localtimeOffsetMsec, localtimeOffsetMsec); + + // Mocks within mocks... + configureFxAccountIdentity(browseridManager, identityConfig); + + // Ensure the new FxAccounts mock has a signed-in user. + fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; + + browseridManager._fxaService = fxa; + + do_check_eq(browseridManager._fxaService.internal.now(), now); + do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec, + localtimeOffsetMsec); + + do_check_eq(browseridManager._fxaService.now(), now); + do_check_eq(browseridManager._fxaService.localtimeOffsetMsec, + localtimeOffsetMsec); + + let request = new SyncStorageRequest("https://example.net/i/like/pie/"); + let authenticator = browseridManager.getResourceAuthenticator(); + let output = authenticator(request, 'GET'); + dump("output" + JSON.stringify(output)); + let authHeader = output.headers.authorization; + do_check_true(authHeader.startsWith('Hawk')); + + // Skew correction is applied in the header and we're within the two-minute + // window. + do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); + do_check_true( + (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); + + run_next_test(); +}); + +add_test(function test_RESTResourceAuthenticatorSkew() { + _("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header."); + + // Clock is skewed 12 hours into the future from our arbitary date + let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; + let browseridManager = new BrowserIDManager(); + let hawkClient = new HawkClient("https://example.net/v1", "/foo"); + + // mock fxa hawk client skew + hawkClient.now = function() { + return now; + } + // Imagine there's already been one fxa request and the hawk client has + // already detected skew vs the fxa auth server. + hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS; + + let fxaClient = new MockFxAccountsClient(); + fxaClient.hawk = hawkClient; + let fxa = new MockFxAccounts(); + fxa.internal._now_is = now; + fxa.internal.fxAccountsClient = fxaClient; + + configureFxAccountIdentity(browseridManager, identityConfig); + + // Ensure the new FxAccounts mock has a signed-in user. + fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; + + browseridManager._fxaService = fxa; + + do_check_eq(browseridManager._fxaService.internal.now(), now); + + let request = new SyncStorageRequest("https://example.net/i/like/pie/"); + let authenticator = browseridManager.getResourceAuthenticator(); + let output = authenticator(request, 'GET'); + dump("output" + JSON.stringify(output)); + let authHeader = output.headers.authorization; + do_check_true(authHeader.startsWith('Hawk')); + + // Skew correction is applied in the header and we're within the two-minute + // window. + do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); + do_check_true( + (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); + + run_next_test(); +}); + +add_task(function* test_ensureLoggedIn() { + configureFxAccountIdentity(browseridManager); + yield browseridManager.initializeWithCurrentIdentity(); + yield browseridManager.whenReadyToAuthenticate.promise; + Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked"); + yield browseridManager.ensureLoggedIn(); + Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked"); + Assert.ok(browseridManager._shouldHaveSyncKeyBundle, + "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); + + // arrange for no logged in user. + let fxa = browseridManager._fxaService + let signedInUser = fxa.internal.currentAccountState.storageManager.accountData; + fxa.internal.currentAccountState.storageManager.accountData = null; + browseridManager.initializeWithCurrentIdentity(); + Assert.ok(!browseridManager._shouldHaveSyncKeyBundle, + "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are."); + Status.login = LOGIN_FAILED_NO_USERNAME; + yield Assert.rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user"); + Assert.ok(browseridManager._shouldHaveSyncKeyBundle, + "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); + // Restore the logged in user to what it was. + fxa.internal.currentAccountState.storageManager.accountData = signedInUser; + Status.login = LOGIN_FAILED_LOGIN_REJECTED; + yield Assert.rejects(browseridManager.ensureLoggedIn(), + "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection"); + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, + "status should remain LOGIN_FAILED_LOGIN_REJECTED"); + Status.login = LOGIN_FAILED_NETWORK_ERROR; + yield browseridManager.ensureLoggedIn(); + Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked"); +}); + +add_test(function test_tokenExpiration() { + _("BrowserIDManager notices token expiration:"); + let bimExp = new BrowserIDManager(); + configureFxAccountIdentity(bimExp, identityConfig); + + let authenticator = bimExp.getResourceAuthenticator(); + do_check_true(!!authenticator); + let req = {uri: CommonUtils.makeURI( + "https://example.net/somewhere/over/the/rainbow"), + method: 'GET'}; + authenticator(req, 'GET'); + + // Mock the clock. + _("Forcing the token to expire ..."); + Object.defineProperty(bimExp, "_now", { + value: function customNow() { + return (Date.now() + 3000001); + }, + writable: true, + }); + do_check_true(bimExp._token.expiration < bimExp._now()); + _("... means BrowserIDManager knows to re-fetch it on the next call."); + do_check_false(bimExp.hasValidToken()); + run_next_test(); + } +); + +add_test(function test_sha256() { + // Test vectors from http://www.bichlmeier.info/sha256test.html + let vectors = [ + ["", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"], + ["abc", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"], + ["message digest", + "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"], + ["secure hash algorithm", + "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"], + ["SHA256 is considered to be safe", + "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"], + ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"], + ["For this sample, this 63-byte string will be used as input data", + "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"], + ["This is exactly 64 bytes long, not counting the terminating byte", + "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"] + ]; + let bidUser = new BrowserIDManager(); + for (let [input,output] of vectors) { + do_check_eq(CommonUtils.bytesAsHex(bidUser._sha256(input)), output); + } + run_next_test(); +}); + +add_test(function test_computeXClientStateHeader() { + let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d"; + let kB = CommonUtils.hexToBytes(kBhex); + + let bidUser = new BrowserIDManager(); + let header = bidUser._computeXClientState(kB); + + do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f"); + run_next_test(); +}); + +add_task(function* test_getTokenErrors() { + _("BrowserIDManager correctly handles various failures to get a token."); + + _("Arrange for a 401 - Sync should reflect an auth error."); + initializeIdentityWithTokenServerResponse({ + status: 401, + headers: {"content-type": "application/json"}, + body: JSON.stringify({}), + }); + let browseridManager = Service.identity; + + yield browseridManager.initializeWithCurrentIdentity(); + yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to 401"); + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); + + // XXX - other interesting responses to return? + + // And for good measure, some totally "unexpected" errors - we generally + // assume these problems are going to magically go away at some point. + _("Arrange for an empty body with a 200 response - should reflect a network error."); + initializeIdentityWithTokenServerResponse({ + status: 200, + headers: [], + body: "", + }); + browseridManager = Service.identity; + yield browseridManager.initializeWithCurrentIdentity(); + yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to non-JSON response"); + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); +}); + +add_task(function* test_refreshCertificateOn401() { + _("BrowserIDManager refreshes the FXA certificate after a 401."); + var identityConfig = makeIdentityConfig(); + var browseridManager = new BrowserIDManager(); + // Use the real `_getAssertion` method that calls + // `mockFxAClient.signCertificate`. + let fxaInternal = makeFxAccountsInternalMock(identityConfig); + delete fxaInternal._getAssertion; + configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal); + browseridManager._fxaService.internal.initialize(); + + let getCertCount = 0; + + let MockFxAccountsClient = function() { + FxAccountsClient.apply(this); + }; + MockFxAccountsClient.prototype = { + __proto__: FxAccountsClient.prototype, + signCertificate() { + ++getCertCount; + } + }; + + let mockFxAClient = new MockFxAccountsClient(); + browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient; + + let didReturn401 = false; + let didReturn200 = false; + let mockTSC = mockTokenServer(() => { + if (getCertCount <= 1) { + didReturn401 = true; + return { + status: 401, + headers: {"content-type": "application/json"}, + body: JSON.stringify({}), + }; + } else { + didReturn200 = true; + return { + status: 200, + headers: {"content-type": "application/json"}, + body: JSON.stringify({ + id: "id", + key: "key", + api_endpoint: "http://example.com/", + uid: "uid", + duration: 300, + }) + }; + } + }); + + browseridManager._tokenServerClient = mockTSC; + + yield browseridManager.initializeWithCurrentIdentity(); + yield browseridManager.whenReadyToAuthenticate.promise; + + do_check_eq(getCertCount, 2); + do_check_true(didReturn401); + do_check_true(didReturn200); + do_check_true(browseridManager.account); + do_check_true(browseridManager._token); + do_check_true(browseridManager.hasValidToken()); + do_check_true(browseridManager.account); +}); + + + +add_task(function* test_getTokenErrorWithRetry() { + _("tokenserver sends an observer notification on various backoff headers."); + + // Set Sync's backoffInterval to zero - after we simulated the backoff header + // it should reflect the value we sent. + Status.backoffInterval = 0; + _("Arrange for a 503 with a Retry-After header."); + initializeIdentityWithTokenServerResponse({ + status: 503, + headers: {"content-type": "application/json", + "retry-after": "100"}, + body: JSON.stringify({}), + }); + let browseridManager = Service.identity; + + yield browseridManager.initializeWithCurrentIdentity(); + yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to 503"); + + // The observer should have fired - check it got the value in the response. + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); + // Sync will have the value in ms with some slop - so check it is at least that. + Assert.ok(Status.backoffInterval >= 100000); + + _("Arrange for a 200 with an X-Backoff header."); + Status.backoffInterval = 0; + initializeIdentityWithTokenServerResponse({ + status: 503, + headers: {"content-type": "application/json", + "x-backoff": "200"}, + body: JSON.stringify({}), + }); + browseridManager = Service.identity; + + yield browseridManager.initializeWithCurrentIdentity(); + yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to no token in response"); + + // The observer should have fired - check it got the value in the response. + Assert.ok(Status.backoffInterval >= 200000); +}); + +add_task(function* test_getKeysErrorWithBackoff() { + _("Auth server (via hawk) sends an observer notification on backoff headers."); + + // Set Sync's backoffInterval to zero - after we simulated the backoff header + // it should reflect the value we sent. + Status.backoffInterval = 0; + _("Arrange for a 503 with a X-Backoff header."); + + let config = makeIdentityConfig(); + // We want no kA or kB so we attempt to fetch them. + delete config.fxaccount.user.kA; + delete config.fxaccount.user.kB; + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "get"); + Assert.equal(uri, "http://mockedserver:9999/account/keys") + return { + status: 503, + headers: {"content-type": "application/json", + "x-backoff": "100"}, + body: "{}", + } + }); + + let browseridManager = Service.identity; + yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to 503"); + + // The observer should have fired - check it got the value in the response. + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); + // Sync will have the value in ms with some slop - so check it is at least that. + Assert.ok(Status.backoffInterval >= 100000); +}); + +add_task(function* test_getKeysErrorWithRetry() { + _("Auth server (via hawk) sends an observer notification on retry headers."); + + // Set Sync's backoffInterval to zero - after we simulated the backoff header + // it should reflect the value we sent. + Status.backoffInterval = 0; + _("Arrange for a 503 with a Retry-After header."); + + let config = makeIdentityConfig(); + // We want no kA or kB so we attempt to fetch them. + delete config.fxaccount.user.kA; + delete config.fxaccount.user.kB; + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "get"); + Assert.equal(uri, "http://mockedserver:9999/account/keys") + return { + status: 503, + headers: {"content-type": "application/json", + "retry-after": "100"}, + body: "{}", + } + }); + + let browseridManager = Service.identity; + yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to 503"); + + // The observer should have fired - check it got the value in the response. + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); + // Sync will have the value in ms with some slop - so check it is at least that. + Assert.ok(Status.backoffInterval >= 100000); +}); + +add_task(function* test_getHAWKErrors() { + _("BrowserIDManager correctly handles various HAWK failures."); + + _("Arrange for a 401 - Sync should reflect an auth error."); + let config = makeIdentityConfig(); + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "post"); + Assert.equal(uri, "http://mockedserver:9999/certificate/sign") + return { + status: 401, + headers: {"content-type": "application/json"}, + body: JSON.stringify({}), + } + }); + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); + + // XXX - other interesting responses to return? + + // And for good measure, some totally "unexpected" errors - we generally + // assume these problems are going to magically go away at some point. + _("Arrange for an empty body with a 200 response - should reflect a network error."); + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "post"); + Assert.equal(uri, "http://mockedserver:9999/certificate/sign") + return { + status: 200, + headers: [], + body: "", + } + }); + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); +}); + +add_task(function* test_getGetKeysFailing401() { + _("BrowserIDManager correctly handles 401 responses fetching keys."); + + _("Arrange for a 401 - Sync should reflect an auth error."); + let config = makeIdentityConfig(); + // We want no kA or kB so we attempt to fetch them. + delete config.fxaccount.user.kA; + delete config.fxaccount.user.kB; + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "get"); + Assert.equal(uri, "http://mockedserver:9999/account/keys") + return { + status: 401, + headers: {"content-type": "application/json"}, + body: "{}", + } + }); + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); +}); + +add_task(function* test_getGetKeysFailing503() { + _("BrowserIDManager correctly handles 5XX responses fetching keys."); + + _("Arrange for a 503 - Sync should reflect a network error."); + let config = makeIdentityConfig(); + // We want no kA or kB so we attempt to fetch them. + delete config.fxaccount.user.kA; + delete config.fxaccount.user.kB; + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "get"); + Assert.equal(uri, "http://mockedserver:9999/account/keys") + return { + status: 503, + headers: {"content-type": "application/json"}, + body: "{}", + } + }); + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error"); +}); + +add_task(function* test_getKeysMissing() { + _("BrowserIDManager correctly handles getKeys succeeding but not returning keys."); + + let browseridManager = new BrowserIDManager(); + let identityConfig = makeIdentityConfig(); + // our mock identity config already has kA and kB - remove them or we never + // try and fetch them. + delete identityConfig.fxaccount.user.kA; + delete identityConfig.fxaccount.user.kB; + identityConfig.fxaccount.user.keyFetchToken = 'keyFetchToken'; + + configureFxAccountIdentity(browseridManager, identityConfig); + + // Mock a fxAccounts object that returns no keys + let fxa = new FxAccounts({ + fetchAndUnwrapKeys: function () { + return Promise.resolve({}); + }, + fxAccountsClient: new MockFxAccountsClient(), + newAccountState(credentials) { + // We only expect this to be called with null indicating the (mock) + // storage should be read. + if (credentials) { + throw new Error("Not expecting to have credentials passed"); + } + let storageManager = new MockFxaStorageManager(); + storageManager.initialize(identityConfig.fxaccount.user); + return new AccountState(storageManager); + }, + }); + + // Add a mock to the currentAccountState object. + fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { + this.cert = { + validUntil: fxa.internal.now() + CERT_LIFETIME, + cert: "certificate", + }; + return Promise.resolve(this.cert.cert); + }; + + browseridManager._fxaService = fxa; + + yield browseridManager.initializeWithCurrentIdentity(); + + let ex; + try { + yield browseridManager.whenReadyToAuthenticate.promise; + } catch (e) { + ex = e; + } + + Assert.ok(ex.message.indexOf("missing kA or kB") >= 0); +}); + +add_task(function* test_signedInUserMissing() { + _("BrowserIDManager detects getSignedInUser returning incomplete account data"); + + let browseridManager = new BrowserIDManager(); + let config = makeIdentityConfig(); + // Delete stored keys and the key fetch token. + delete identityConfig.fxaccount.user.kA; + delete identityConfig.fxaccount.user.kB; + delete identityConfig.fxaccount.user.keyFetchToken; + + configureFxAccountIdentity(browseridManager, identityConfig); + + let fxa = new FxAccounts({ + fetchAndUnwrapKeys: function () { + return Promise.resolve({}); + }, + fxAccountsClient: new MockFxAccountsClient(), + newAccountState(credentials) { + // We only expect this to be called with null indicating the (mock) + // storage should be read. + if (credentials) { + throw new Error("Not expecting to have credentials passed"); + } + let storageManager = new MockFxaStorageManager(); + storageManager.initialize(identityConfig.fxaccount.user); + return new AccountState(storageManager); + }, + }); + + browseridManager._fxaService = fxa; + + let status = yield browseridManager.unlockAndVerifyAuthState(); + Assert.equal(status, LOGIN_FAILED_LOGIN_REJECTED); +}); + +// End of tests +// Utility functions follow + +// Create a new browserid_identity object and initialize it with a +// hawk mock that simulates HTTP responses. +// The callback function will be called each time the mocked hawk server wants +// to make a request. The result of the callback should be the mock response +// object that will be returned to hawk. +// A token server mock will be used that doesn't hit a server, so we move +// directly to a hawk request. +function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) { + // A mock request object. + function MockRESTRequest(uri, credentials, extra) { + this._uri = uri; + this._credentials = credentials; + this._extra = extra; + }; + MockRESTRequest.prototype = { + setHeader: function() {}, + post: function(data, callback) { + this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra); + callback.call(this); + }, + get: function(callback) { + // Skip /status requests (browserid_identity checks if the account still + // exists after an auth error) + if (this._uri.startsWith("http://mockedserver:9999/account/status")) { + this.response = { + status: 200, + headers: {"content-type": "application/json"}, + body: JSON.stringify({exists: true}), + }; + } else { + this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra); + } + callback.call(this); + } + } + + // The hawk client. + function MockedHawkClient() {} + MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999"); + MockedHawkClient.prototype.constructor = MockedHawkClient; + MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) { + return new MockRESTRequest(uri, credentials, extra); + } + // Arrange for the same observerPrefix as FxAccountsClient uses + MockedHawkClient.prototype.observerPrefix = "FxA:hawk"; + + // tie it all together - configureFxAccountIdentity isn't useful here :( + let fxaClient = new MockFxAccountsClient(); + fxaClient.hawk = new MockedHawkClient(); + let internal = { + fxAccountsClient: fxaClient, + newAccountState(credentials) { + // We only expect this to be called with null indicating the (mock) + // storage should be read. + if (credentials) { + throw new Error("Not expecting to have credentials passed"); + } + let storageManager = new MockFxaStorageManager(); + storageManager.initialize(config.fxaccount.user); + return new AccountState(storageManager); + }, + } + let fxa = new FxAccounts(internal); + + browseridManager._fxaService = fxa; + browseridManager._signedInUser = null; + yield browseridManager.initializeWithCurrentIdentity(); + yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, + "expecting rejection due to hawk error"); +} + + +function getTimestamp(hawkAuthHeader) { + return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS; +} + +function getTimestampDelta(hawkAuthHeader, now=Date.now()) { + return Math.abs(getTimestamp(hawkAuthHeader) - now); +} + +function mockTokenServer(func) { + let requestLog = Log.repository.getLogger("testing.mock-rest"); + if (!requestLog.appenders.length) { // might as well see what it says :) + requestLog.addAppender(new Log.DumpAppender()); + requestLog.level = Log.Level.Trace; + } + function MockRESTRequest(url) {}; + MockRESTRequest.prototype = { + _log: requestLog, + setHeader: function() {}, + get: function(callback) { + this.response = func(); + callback.call(this); + } + } + // The mocked TokenServer client which will get the response. + function MockTSC() { } + MockTSC.prototype = new TokenServerClient(); + MockTSC.prototype.constructor = MockTSC; + MockTSC.prototype.newRESTRequest = function(url) { + return new MockRESTRequest(url); + } + // Arrange for the same observerPrefix as browserid_identity uses. + MockTSC.prototype.observerPrefix = "weave:service"; + return new MockTSC(); +} |