Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
const { AuthenticationError, SyncAuthManager } = ChromeUtils.importESModule(
);
const { Resource } = ChromeUtils.importESModule(
);
const { initializeIdentityWithTokenServerResponse } =
ChromeUtils.importESModule(
);
const { HawkClient } = ChromeUtils.importESModule(
);
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(
);
const { Status } = ChromeUtils.importESModule(
);
const { TokenServerClient, TokenServerClientServerError } =
ChromeUtils.importESModule(
);
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");
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");
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) {
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");
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");
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();
}