summaryrefslogtreecommitdiffstats
path: root/services/sync/tests/unit/test_sync_auth_manager.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/tests/unit/test_sync_auth_manager.js')
-rw-r--r--services/sync/tests/unit/test_sync_auth_manager.js1053
1 files changed, 1053 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_sync_auth_manager.js b/services/sync/tests/unit/test_sync_auth_manager.js
new file mode 100644
index 0000000000..8b9bffe377
--- /dev/null
+++ b/services/sync/tests/unit/test_sync_auth_manager.js
@@ -0,0 +1,1053 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AuthenticationError, SyncAuthManager } = ChromeUtils.importESModule(
+ "resource://services-sync/sync_auth.sys.mjs"
+);
+const { Resource } = ChromeUtils.importESModule(
+ "resource://services-sync/resource.sys.mjs"
+);
+const { initializeIdentityWithTokenServerResponse } =
+ ChromeUtils.importESModule(
+ "resource://testing-common/services/sync/fxa_utils.sys.mjs"
+ );
+const { HawkClient } = ChromeUtils.importESModule(
+ "resource://services-common/hawkclient.sys.mjs"
+);
+const { FxAccounts } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+const { FxAccountsClient } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccountsClient.sys.mjs"
+);
+const {
+ ERRNO_INVALID_AUTH_TOKEN,
+ ONLOGIN_NOTIFICATION,
+ ONVERIFIED_NOTIFICATION,
+} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+const { Status } = ChromeUtils.importESModule(
+ "resource://services-sync/status.sys.mjs"
+);
+const { TokenServerClient, TokenServerClientServerError } =
+ ChromeUtils.importESModule(
+ "resource://services-common/tokenserverclient.sys.mjs"
+ );
+const { AccountState } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+
+const SECOND_MS = 1000;
+const MINUTE_MS = SECOND_MS * 60;
+const HOUR_MS = MINUTE_MS * 60;
+
+const MOCK_ACCESS_TOKEN =
+ "e3c5caf17f27a0d9e351926a928938b3737df43e91d4992a5a5fca9a7bdef8ba";
+
+var globalIdentityConfig = makeIdentityConfig();
+var globalSyncAuthManager = new SyncAuthManager();
+configureFxAccountIdentity(globalSyncAuthManager, globalIdentityConfig);
+
+/**
+ * Mock client clock and skew vs server in FxAccounts signed-in user module and
+ * API client. sync_auth.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 = {
+ accountStatus() {
+ return Promise.resolve(true);
+ },
+ getScopedKeyData() {
+ return Promise.resolve({
+ "https://identity.mozilla.com/apps/oldsync": {
+ identifier: "https://identity.mozilla.com/apps/oldsync",
+ keyRotationSecret:
+ "0000000000000000000000000000000000000000000000000000000000000000",
+ keyRotationTimestamp: 1234567890123,
+ },
+ });
+ },
+};
+Object.setPrototypeOf(
+ MockFxAccountsClient.prototype,
+ FxAccountsClient.prototype
+);
+
+add_test(function test_initial_state() {
+ _("Verify initial state");
+ Assert.ok(!globalSyncAuthManager._token);
+ Assert.ok(!globalSyncAuthManager._hasValidToken());
+ run_next_test();
+});
+
+add_task(async function test_initialialize() {
+ _("Verify start after fetching token");
+ await globalSyncAuthManager._ensureValidToken();
+ Assert.ok(!!globalSyncAuthManager._token);
+ Assert.ok(globalSyncAuthManager._hasValidToken());
+});
+
+add_task(async function test_refreshOAuthTokenOn401() {
+ _("Refreshes the FXA OAuth token after a 401.");
+ let getTokenCount = 0;
+ let syncAuthManager = new SyncAuthManager();
+ let identityConfig = makeIdentityConfig();
+ let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+ configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
+ syncAuthManager._fxaService._internal.initialize();
+ syncAuthManager._fxaService.getOAuthToken = () => {
+ ++getTokenCount;
+ return Promise.resolve(MOCK_ACCESS_TOKEN);
+ };
+
+ let didReturn401 = false;
+ let didReturn200 = false;
+ let mockTSC = mockTokenServer(() => {
+ if (getTokenCount <= 1) {
+ didReturn401 = true;
+ return {
+ status: 401,
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({}),
+ };
+ }
+ 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,
+ }),
+ };
+ });
+
+ syncAuthManager._tokenServerClient = mockTSC;
+
+ await syncAuthManager._ensureValidToken();
+
+ Assert.equal(getTokenCount, 2);
+ Assert.ok(didReturn401);
+ Assert.ok(didReturn200);
+ Assert.ok(syncAuthManager._token);
+ Assert.ok(syncAuthManager._hasValidToken());
+});
+
+add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
+ _("Verify sync state with auth error + account deleted");
+
+ var identityConfig = makeIdentityConfig();
+ var syncAuthManager = new SyncAuthManager();
+
+ // Use the real `getOAuthToken` method that calls
+ // `mockFxAClient.accessTokenWithSessionToken`.
+ let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+ delete fxaInternal.getOAuthToken;
+
+ configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
+ syncAuthManager._fxaService._internal.initialize();
+
+ let accessTokenWithSessionTokenCalled = false;
+ let accountStatusCalled = false;
+ let sessionStatusCalled = false;
+
+ let AuthErrorMockFxAClient = function () {
+ FxAccountsClient.apply(this);
+ };
+ AuthErrorMockFxAClient.prototype = {
+ accessTokenWithSessionToken() {
+ accessTokenWithSessionTokenCalled = true;
+ return Promise.reject({
+ code: 401,
+ errno: ERRNO_INVALID_AUTH_TOKEN,
+ });
+ },
+ accountStatus() {
+ accountStatusCalled = true;
+ return Promise.resolve(false);
+ },
+ sessionStatus() {
+ sessionStatusCalled = true;
+ return Promise.resolve(false);
+ },
+ };
+ Object.setPrototypeOf(
+ AuthErrorMockFxAClient.prototype,
+ FxAccountsClient.prototype
+ );
+
+ let mockFxAClient = new AuthErrorMockFxAClient();
+ syncAuthManager._fxaService._internal._fxAccountsClient = mockFxAClient;
+
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ AuthenticationError,
+ "should reject due to an auth error"
+ );
+
+ Assert.ok(accessTokenWithSessionTokenCalled);
+ Assert.ok(sessionStatusCalled);
+ Assert.ok(accountStatusCalled);
+ Assert.ok(!syncAuthManager._token);
+ Assert.ok(!syncAuthManager._hasValidToken());
+});
+
+add_task(async function test_getResourceAuthenticator() {
+ _(
+ "SyncAuthManager supplies a Resource Authenticator callback which returns a Hawk header."
+ );
+ configureFxAccountIdentity(globalSyncAuthManager);
+ let authenticator = globalSyncAuthManager.getResourceAuthenticator();
+ Assert.ok(!!authenticator);
+ let req = {
+ uri: CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"),
+ method: "GET",
+ };
+ let output = await authenticator(req, "GET");
+ Assert.ok("headers" in output);
+ Assert.ok("authorization" in output.headers);
+ Assert.ok(output.headers.authorization.startsWith("Hawk"));
+ _("Expected internal state after successful call.");
+ Assert.equal(
+ globalSyncAuthManager._token.uid,
+ globalIdentityConfig.fxaccount.token.uid
+ );
+});
+
+add_task(async function test_resourceAuthenticatorSkew() {
+ _(
+ "SyncAuthManager 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 syncAuthManager = new SyncAuthManager();
+ 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
+ Assert.equal(hawkClient.now(), now);
+ Assert.equal(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec);
+
+ // Properly picked up by the client
+ Assert.equal(fxaClient.now(), now);
+ Assert.equal(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec);
+
+ let identityConfig = makeIdentityConfig();
+ let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+ fxaInternal._now_is = now;
+ fxaInternal.fxAccountsClient = fxaClient;
+
+ // Mocks within mocks...
+ configureFxAccountIdentity(
+ syncAuthManager,
+ globalIdentityConfig,
+ fxaInternal
+ );
+
+ Assert.equal(syncAuthManager._fxaService._internal.now(), now);
+ Assert.equal(
+ syncAuthManager._fxaService._internal.localtimeOffsetMsec,
+ localtimeOffsetMsec
+ );
+
+ Assert.equal(syncAuthManager._fxaService._internal.now(), now);
+ Assert.equal(
+ syncAuthManager._fxaService._internal.localtimeOffsetMsec,
+ localtimeOffsetMsec
+ );
+
+ let request = new Resource("https://example.net/i/like/pie/");
+ let authenticator = syncAuthManager.getResourceAuthenticator();
+ let output = await authenticator(request, "GET");
+ dump("output" + JSON.stringify(output));
+ let authHeader = output.headers.authorization;
+ Assert.ok(authHeader.startsWith("Hawk"));
+
+ // Skew correction is applied in the header and we're within the two-minute
+ // window.
+ Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS);
+ Assert.ok(getTimestampDelta(authHeader, now) - 12 * HOUR_MS < 2 * MINUTE_MS);
+});
+
+add_task(async function test_RESTResourceAuthenticatorSkew() {
+ _(
+ "SyncAuthManager 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 syncAuthManager = new SyncAuthManager();
+ 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 identityConfig = makeIdentityConfig();
+ let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+ fxaInternal._now_is = now;
+ fxaInternal.fxAccountsClient = fxaClient;
+
+ configureFxAccountIdentity(
+ syncAuthManager,
+ globalIdentityConfig,
+ fxaInternal
+ );
+
+ Assert.equal(syncAuthManager._fxaService._internal.now(), now);
+
+ let request = new Resource("https://example.net/i/like/pie/");
+ let authenticator = syncAuthManager.getResourceAuthenticator();
+ let output = await authenticator(request, "GET");
+ dump("output" + JSON.stringify(output));
+ let authHeader = output.headers.authorization;
+ Assert.ok(authHeader.startsWith("Hawk"));
+
+ // Skew correction is applied in the header and we're within the two-minute
+ // window.
+ Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS);
+ Assert.ok(getTimestampDelta(authHeader, now) - 12 * HOUR_MS < 2 * MINUTE_MS);
+});
+
+add_task(async function test_ensureLoggedIn() {
+ configureFxAccountIdentity(globalSyncAuthManager);
+ await globalSyncAuthManager._ensureValidToken();
+ Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
+ Assert.ok(globalSyncAuthManager._token);
+
+ // arrange for no logged in user.
+ let fxa = globalSyncAuthManager._fxaService;
+ let signedInUser =
+ fxa._internal.currentAccountState.storageManager.accountData;
+ fxa._internal.currentAccountState.storageManager.accountData = null;
+ await Assert.rejects(
+ globalSyncAuthManager._ensureValidToken(true),
+ /no user is logged in/,
+ "expecting rejection due to no user"
+ );
+ // Restore the logged in user to what it was.
+ fxa._internal.currentAccountState.storageManager.accountData = signedInUser;
+ Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+ await globalSyncAuthManager._ensureValidToken(true);
+ Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
+});
+
+add_task(async function test_syncState() {
+ // Avoid polling for an unverified user.
+ let identityConfig = makeIdentityConfig();
+ let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+ fxaInternal.startVerifiedCheck = () => {};
+ configureFxAccountIdentity(
+ globalSyncAuthManager,
+ globalIdentityConfig,
+ fxaInternal
+ );
+
+ // arrange for no logged in user.
+ let fxa = globalSyncAuthManager._fxaService;
+ let signedInUser =
+ fxa._internal.currentAccountState.storageManager.accountData;
+ fxa._internal.currentAccountState.storageManager.accountData = null;
+ await Assert.rejects(
+ globalSyncAuthManager._ensureValidToken(true),
+ /no user is logged in/,
+ "expecting rejection due to no user"
+ );
+ // Restore to an unverified user.
+ Services.prefs.setStringPref("services.sync.username", signedInUser.email);
+ signedInUser.verified = false;
+ fxa._internal.currentAccountState.storageManager.accountData = signedInUser;
+ Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+ // The sync_auth observers are async, so call them directly.
+ await globalSyncAuthManager.observe(null, ONLOGIN_NOTIFICATION, "");
+ Assert.equal(
+ Status.login,
+ LOGIN_FAILED_LOGIN_REJECTED,
+ "should not have changed the login state for an unverified user"
+ );
+
+ // now pretend the user because verified.
+ signedInUser.verified = true;
+ await globalSyncAuthManager.observe(null, ONVERIFIED_NOTIFICATION, "");
+ Assert.equal(
+ Status.login,
+ LOGIN_SUCCEEDED,
+ "should have changed the login state to success"
+ );
+});
+
+add_task(async function test_tokenExpiration() {
+ _("SyncAuthManager notices token expiration:");
+ let bimExp = new SyncAuthManager();
+ configureFxAccountIdentity(bimExp, globalIdentityConfig);
+
+ let authenticator = bimExp.getResourceAuthenticator();
+ Assert.ok(!!authenticator);
+ let req = {
+ uri: CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"),
+ method: "GET",
+ };
+ await authenticator(req, "GET");
+
+ // Mock the clock.
+ _("Forcing the token to expire ...");
+ Object.defineProperty(bimExp, "_now", {
+ value: function customNow() {
+ return Date.now() + 3000001;
+ },
+ writable: true,
+ });
+ Assert.ok(bimExp._token.expiration < bimExp._now());
+ _("... means SyncAuthManager knows to re-fetch it on the next call.");
+ Assert.ok(!bimExp._hasValidToken());
+});
+
+add_task(async function test_getTokenErrors() {
+ _("SyncAuthManager 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 syncAuthManager = Service.identity;
+
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ AuthenticationError,
+ "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: "",
+ });
+ syncAuthManager = Service.identity;
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ TokenServerClientServerError,
+ "should reject due to non-JSON response"
+ );
+ Assert.equal(
+ Status.login,
+ LOGIN_FAILED_NETWORK_ERROR,
+ "login state is LOGIN_FAILED_NETWORK_ERROR"
+ );
+});
+
+add_task(async function test_refreshAccessTokenOn401() {
+ _("SyncAuthManager refreshes the FXA OAuth access token after a 401.");
+ var identityConfig = makeIdentityConfig();
+ var syncAuthManager = new SyncAuthManager();
+ // Use the real `getOAuthToken` method that calls
+ // `mockFxAClient.accessTokenWithSessionToken`.
+ let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+ delete fxaInternal.getOAuthToken;
+ configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
+ syncAuthManager._fxaService._internal.initialize();
+
+ let getTokenCount = 0;
+
+ let CheckSignMockFxAClient = function () {
+ FxAccountsClient.apply(this);
+ };
+ CheckSignMockFxAClient.prototype = {
+ accessTokenWithSessionToken() {
+ ++getTokenCount;
+ return Promise.resolve({ access_token: "token" });
+ },
+ };
+ Object.setPrototypeOf(
+ CheckSignMockFxAClient.prototype,
+ FxAccountsClient.prototype
+ );
+
+ let mockFxAClient = new CheckSignMockFxAClient();
+ syncAuthManager._fxaService._internal._fxAccountsClient = mockFxAClient;
+
+ let didReturn401 = false;
+ let didReturn200 = false;
+ let mockTSC = mockTokenServer(() => {
+ if (getTokenCount <= 1) {
+ didReturn401 = true;
+ return {
+ status: 401,
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({}),
+ };
+ }
+ 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,
+ }),
+ };
+ });
+
+ syncAuthManager._tokenServerClient = mockTSC;
+
+ await syncAuthManager._ensureValidToken();
+
+ Assert.equal(getTokenCount, 2);
+ Assert.ok(didReturn401);
+ Assert.ok(didReturn200);
+ Assert.ok(syncAuthManager._token);
+ Assert.ok(syncAuthManager._hasValidToken());
+});
+
+add_task(async 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 syncAuthManager = Service.identity;
+
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ TokenServerClientServerError,
+ "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({}),
+ });
+ syncAuthManager = Service.identity;
+
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ TokenServerClientServerError,
+ "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(async 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 kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+ delete config.fxaccount.user.scopedKeys;
+ delete config.fxaccount.user.kSync;
+ delete config.fxaccount.user.kXCS;
+ delete config.fxaccount.user.kExtSync;
+ delete config.fxaccount.user.kExtKbHash;
+ config.fxaccount.user.keyFetchToken = "keyfetchtoken";
+ await 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 syncAuthManager = Service.identity;
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ TokenServerClientServerError,
+ "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(async 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 kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+ delete config.fxaccount.user.scopedKeys;
+ delete config.fxaccount.user.kSync;
+ delete config.fxaccount.user.kXCS;
+ delete config.fxaccount.user.kExtSync;
+ delete config.fxaccount.user.kExtKbHash;
+ config.fxaccount.user.keyFetchToken = "keyfetchtoken";
+ await 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 syncAuthManager = Service.identity;
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ TokenServerClientServerError,
+ "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(async function test_getHAWKErrors() {
+ _("SyncAuthManager correctly handles various HAWK failures.");
+
+ _("Arrange for a 401 - Sync should reflect an auth error.");
+ let config = makeIdentityConfig();
+ await initializeIdentityWithHAWKResponseFactory(
+ config,
+ function (method, data, uri) {
+ if (uri == "http://mockedserver:9999/oauth/token") {
+ Assert.equal(method, "post");
+ return {
+ status: 401,
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ code: 401,
+ errno: 110,
+ error: "invalid token",
+ }),
+ };
+ }
+ // For any follow-up requests that check account status.
+ return {
+ status: 200,
+ 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."
+ );
+ await initializeIdentityWithHAWKResponseFactory(
+ config,
+ function (method, data, uri) {
+ Assert.equal(method, "post");
+ Assert.equal(uri, "http://mockedserver:9999/oauth/token");
+ return {
+ status: 200,
+ headers: [],
+ body: "",
+ };
+ }
+ );
+ Assert.equal(
+ Status.login,
+ LOGIN_FAILED_NETWORK_ERROR,
+ "login state is LOGIN_FAILED_NETWORK_ERROR"
+ );
+});
+
+add_task(async function test_getGetKeysFailing401() {
+ _("SyncAuthManager correctly handles 401 responses fetching keys.");
+
+ _("Arrange for a 401 - Sync should reflect an auth error.");
+ let config = makeIdentityConfig();
+ // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+ delete config.fxaccount.user.scopedKeys;
+ delete config.fxaccount.user.kSync;
+ delete config.fxaccount.user.kXCS;
+ delete config.fxaccount.user.kExtSync;
+ delete config.fxaccount.user.kExtKbHash;
+ config.fxaccount.user.keyFetchToken = "keyfetchtoken";
+ await 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(async function test_getGetKeysFailing503() {
+ _("SyncAuthManager correctly handles 5XX responses fetching keys.");
+
+ _("Arrange for a 503 - Sync should reflect a network error.");
+ let config = makeIdentityConfig();
+ // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+ delete config.fxaccount.user.scopedKeys;
+ delete config.fxaccount.user.kSync;
+ delete config.fxaccount.user.kXCS;
+ delete config.fxaccount.user.kExtSync;
+ delete config.fxaccount.user.kExtKbHash;
+ config.fxaccount.user.keyFetchToken = "keyfetchtoken";
+ await 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(async function test_getKeysMissing() {
+ _(
+ "SyncAuthManager correctly handles getKeyForScope succeeding but not returning the key."
+ );
+
+ let syncAuthManager = new SyncAuthManager();
+ let identityConfig = makeIdentityConfig();
+ // our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
+ // try and fetch them.
+ delete identityConfig.fxaccount.user.scopedKeys;
+ delete identityConfig.fxaccount.user.kSync;
+ delete identityConfig.fxaccount.user.kXCS;
+ delete identityConfig.fxaccount.user.kExtSync;
+ delete identityConfig.fxaccount.user.kExtKbHash;
+ identityConfig.fxaccount.user.keyFetchToken = "keyFetchToken";
+
+ configureFxAccountIdentity(syncAuthManager, identityConfig);
+
+ // Mock a fxAccounts object
+ let fxa = new FxAccounts({
+ 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);
+ },
+ // And the keys object with a mock that returns no keys.
+ keys: {
+ getKeyForScope() {
+ return Promise.resolve(null);
+ },
+ },
+ });
+
+ syncAuthManager._fxaService = fxa;
+
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ /browser does not have the sync key, cannot sync/
+ );
+});
+
+add_task(async function test_getKeysUnexpecedError() {
+ _(
+ "SyncAuthManager correctly handles getKeyForScope throwing an unexpected error."
+ );
+
+ let syncAuthManager = new SyncAuthManager();
+ let identityConfig = makeIdentityConfig();
+ // our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
+ // try and fetch them.
+ delete identityConfig.fxaccount.user.scopedKeys;
+ delete identityConfig.fxaccount.user.kSync;
+ delete identityConfig.fxaccount.user.kXCS;
+ delete identityConfig.fxaccount.user.kExtSync;
+ delete identityConfig.fxaccount.user.kExtKbHash;
+ identityConfig.fxaccount.user.keyFetchToken = "keyFetchToken";
+
+ configureFxAccountIdentity(syncAuthManager, identityConfig);
+
+ // Mock a fxAccounts object
+ let fxa = new FxAccounts({
+ 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);
+ },
+ // And the keys object with a mock that returns no keys.
+ keys: {
+ async getKeyForScope() {
+ throw new Error("well that was unexpected");
+ },
+ },
+ });
+
+ syncAuthManager._fxaService = fxa;
+
+ await Assert.rejects(
+ syncAuthManager._ensureValidToken(),
+ /well that was unexpected/
+ );
+});
+
+add_task(async function test_signedInUserMissing() {
+ _(
+ "SyncAuthManager detects getSignedInUser returning incomplete account data"
+ );
+
+ let syncAuthManager = new SyncAuthManager();
+ // Delete stored keys and the key fetch token.
+ delete globalIdentityConfig.fxaccount.user.scopedKeys;
+ delete globalIdentityConfig.fxaccount.user.kSync;
+ delete globalIdentityConfig.fxaccount.user.kXCS;
+ delete globalIdentityConfig.fxaccount.user.kExtSync;
+ delete globalIdentityConfig.fxaccount.user.kExtKbHash;
+ delete globalIdentityConfig.fxaccount.user.keyFetchToken;
+
+ configureFxAccountIdentity(syncAuthManager, globalIdentityConfig);
+
+ let fxa = new FxAccounts({
+ fetchAndUnwrapKeys() {
+ 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(globalIdentityConfig.fxaccount.user);
+ return new AccountState(storageManager);
+ },
+ });
+
+ syncAuthManager._fxaService = fxa;
+
+ let status = await syncAuthManager.unlockAndVerifyAuthState();
+ Assert.equal(status, LOGIN_FAILED_LOGIN_REJECTED);
+});
+
+// End of tests
+// Utility functions follow
+
+// Create a new sync_auth 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.
+async 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() {},
+ async post(data) {
+ this.response = cbGetResponse(
+ "post",
+ data,
+ this._uri,
+ this._credentials,
+ this._extra
+ );
+ return this.response;
+ },
+ async get() {
+ // Skip /status requests (sync_auth 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
+ );
+ }
+ return this.response;
+ },
+ };
+
+ // 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);
+
+ globalSyncAuthManager._fxaService = fxa;
+ await Assert.rejects(
+ globalSyncAuthManager._ensureValidToken(true),
+ // TODO: Ideally this should have a specific check for an error.
+ () => true,
+ "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() {},
+ async get() {
+ this.response = func();
+ return this.response;
+ },
+ };
+ // 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 sync_auth uses.
+ MockTSC.prototype.observerPrefix = "weave:service";
+ return new MockTSC();
+}