forked from mirrors/gecko-dev
1024 lines
33 KiB
JavaScript
1024 lines
33 KiB
JavaScript
/* 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.importESModule(
|
|
"resource://gre/modules/FxAccountsCommon.sys.mjs"
|
|
);
|
|
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, ERROR_INVALID_ACCOUNT_STATE } =
|
|
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(),
|
|
err => {
|
|
Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE);
|
|
return true; // expected error
|
|
},
|
|
"should reject because the account was deleted"
|
|
);
|
|
|
|
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 scopedKeys so we attempt to fetch them.
|
|
delete config.fxaccount.user.scopedKeys;
|
|
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 scopedKeys so we attempt to fetch them.
|
|
delete config.fxaccount.user.scopedKeys;
|
|
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 scopedKeys so we attempt to fetch them.
|
|
delete config.fxaccount.user.scopedKeys;
|
|
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 scopedKeys so we attempt to fetch them.
|
|
delete config.fxaccount.user.scopedKeys;
|
|
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 scopedKeys remove them or we never
|
|
// try and fetch them.
|
|
delete identityConfig.fxaccount.user.scopedKeys;
|
|
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);
|
|
},
|
|
});
|
|
fxa.getOAuthTokenAndKey = () => {
|
|
// And the keys object with a mock that returns no keys.
|
|
return Promise.resolve({ key: null, token: "fake token" });
|
|
};
|
|
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 scopedKeys - remove them or we never
|
|
// try and fetch them.
|
|
delete identityConfig.fxaccount.user.scopedKeys;
|
|
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);
|
|
},
|
|
});
|
|
|
|
fxa.getOAuthTokenAndKey = () => {
|
|
return Promise.reject("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.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();
|
|
}
|