/* 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,
SCOPE_APP_SYNC,
} = 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({
[SCOPE_APP_SYNC]: {
identifier: SCOPE_APP_SYNC,
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.");
if (Services.prefs.getBoolPref(
"identity.fxaccounts.oauth.enabled",
false)) {
return;
}
_(
"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.");
if (Services.prefs.getBoolPref(
"identity.fxaccounts.oauth.enabled",
false)) {
return;
}
_(
"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."
);
if (Services.prefs.getBoolPref(
"identity.fxaccounts.oauth.enabled",
false)) {
return;
}
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."
);
if (Services.prefs.getBoolPref(
"identity.fxaccounts.oauth.enabled",
false)) {
return;
}
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() {}
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();
}