1769 lines
51 KiB
JavaScript
1769 lines
51 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
const { CryptoUtils } = ChromeUtils.importESModule(
|
|
"resource://services-crypto/utils.sys.mjs"
|
|
);
|
|
const { FxAccounts, ERROR_INVALID_ACCOUNT_STATE } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/FxAccounts.sys.mjs"
|
|
);
|
|
const { FxAccountsClient } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/FxAccountsClient.sys.mjs"
|
|
);
|
|
const {
|
|
CLIENT_IS_THUNDERBIRD,
|
|
ERRNO_INVALID_AUTH_TOKEN,
|
|
ERROR_NO_ACCOUNT,
|
|
OAUTH_CLIENT_ID,
|
|
ONLOGIN_NOTIFICATION,
|
|
ONLOGOUT_NOTIFICATION,
|
|
ONVERIFIED_NOTIFICATION,
|
|
DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY,
|
|
PREF_LAST_FXA_USER,
|
|
} = ChromeUtils.importESModule(
|
|
"resource://gre/modules/FxAccountsCommon.sys.mjs"
|
|
);
|
|
|
|
// We grab some additional stuff via the system global.
|
|
var { AccountState } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/FxAccounts.sys.mjs"
|
|
);
|
|
|
|
const MOCK_TOKEN_RESPONSE = {
|
|
access_token:
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69",
|
|
token_type: "bearer",
|
|
scope: SCOPE_APP_SYNC,
|
|
expires_in: 21600,
|
|
auth_at: 1589579900,
|
|
};
|
|
|
|
initTestLogging("Trace");
|
|
|
|
var log = Log.repository.getLogger("Services.FxAccounts.test");
|
|
log.level = Log.Level.Debug;
|
|
|
|
// See verbose logging from FxAccounts.sys.mjs and jwcrypto.sys.mjs.
|
|
Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace");
|
|
Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace;
|
|
Services.prefs.setStringPref("services.crypto.jwcrypto.log.level", "Debug");
|
|
|
|
/*
|
|
* The FxAccountsClient communicates with the remote Firefox
|
|
* Accounts auth server. Mock the server calls, with a little
|
|
* lag time to simulate some latency.
|
|
*
|
|
* We add the _verified attribute to mock the change in verification
|
|
* state on the FXA server.
|
|
*/
|
|
|
|
function MockStorageManager() {}
|
|
|
|
MockStorageManager.prototype = {
|
|
promiseInitialized: Promise.resolve(),
|
|
|
|
initialize(accountData) {
|
|
this.accountData = accountData;
|
|
},
|
|
|
|
finalize() {
|
|
return Promise.resolve();
|
|
},
|
|
|
|
getAccountData(fields = null) {
|
|
let result;
|
|
if (!this.accountData) {
|
|
result = null;
|
|
} else if (fields == null) {
|
|
// can't use cloneInto as the keys get upset...
|
|
result = {};
|
|
for (let field of Object.keys(this.accountData)) {
|
|
result[field] = this.accountData[field];
|
|
}
|
|
} else {
|
|
if (!Array.isArray(fields)) {
|
|
fields = [fields];
|
|
}
|
|
result = {};
|
|
for (let field of fields) {
|
|
result[field] = this.accountData[field];
|
|
}
|
|
}
|
|
return Promise.resolve(result);
|
|
},
|
|
|
|
updateAccountData(updatedFields) {
|
|
if (!this.accountData) {
|
|
return Promise.resolve();
|
|
}
|
|
for (let [name, value] of Object.entries(updatedFields)) {
|
|
if (value == null) {
|
|
delete this.accountData[name];
|
|
} else {
|
|
this.accountData[name] = value;
|
|
}
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
|
|
deleteAccountData() {
|
|
this.accountData = null;
|
|
return Promise.resolve();
|
|
},
|
|
};
|
|
|
|
function MockFxAccountsClient() {
|
|
this._email = "nobody@example.com";
|
|
this._verified = false;
|
|
this._deletedOnServer = false; // for our accountStatus mock
|
|
|
|
// mock calls up to the auth server to determine whether the
|
|
// user account has been verified
|
|
this.recoveryEmailStatus = async function () {
|
|
// simulate a call to /recovery_email/status
|
|
return {
|
|
email: this._email,
|
|
verified: this._verified,
|
|
};
|
|
};
|
|
|
|
this.accountStatus = async function (uid) {
|
|
return !!uid && !this._deletedOnServer;
|
|
};
|
|
|
|
this.sessionStatus = async function () {
|
|
// If the sessionStatus check says an account is OK, we typically will not
|
|
// end up calling accountStatus - so this must return false if accountStatus
|
|
// would.
|
|
return !this._deletedOnServer;
|
|
};
|
|
|
|
this.accountKeys = function () {
|
|
return new Promise(resolve => {
|
|
do_timeout(50, () => {
|
|
resolve({
|
|
kA: expandBytes("11"),
|
|
wrapKB: expandBytes("22"),
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
this.getScopedKeyData = function (sessionToken, client_id, scopes) {
|
|
Assert.ok(sessionToken);
|
|
Assert.equal(client_id, OAUTH_CLIENT_ID);
|
|
Assert.equal(scopes, SCOPE_APP_SYNC);
|
|
return new Promise(resolve => {
|
|
do_timeout(50, () => {
|
|
resolve({
|
|
[SCOPE_APP_SYNC]: {
|
|
identifier: SCOPE_APP_SYNC,
|
|
keyRotationSecret:
|
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
keyRotationTimestamp: 1234567890123,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
this.resendVerificationEmail = function (sessionToken) {
|
|
// Return the session token to show that we received it in the first place
|
|
return Promise.resolve(sessionToken);
|
|
};
|
|
|
|
this.signOut = () => Promise.resolve();
|
|
|
|
FxAccountsClient.apply(this);
|
|
}
|
|
MockFxAccountsClient.prototype = {};
|
|
Object.setPrototypeOf(
|
|
MockFxAccountsClient.prototype,
|
|
FxAccountsClient.prototype
|
|
);
|
|
/*
|
|
* We need to mock the FxAccounts module's interfaces to external
|
|
* services, such as storage and the FxAccounts client. We also
|
|
* mock the now() method, so that we can simulate the passing of
|
|
* time and verify that signatures expire correctly.
|
|
*/
|
|
function MockFxAccounts() {
|
|
let result = new FxAccounts({
|
|
VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms
|
|
|
|
_getCertificateSigned_calls: [],
|
|
_d_signCertificate: Promise.withResolvers(),
|
|
_now_is: new Date(),
|
|
now() {
|
|
return this._now_is;
|
|
},
|
|
newAccountState(newCredentials) {
|
|
// we use a real accountState but mocked storage.
|
|
let storage = new MockStorageManager();
|
|
storage.initialize(newCredentials);
|
|
return new AccountState(storage);
|
|
},
|
|
fxAccountsClient: new MockFxAccountsClient(),
|
|
observerPreloads: [],
|
|
device: {
|
|
_registerOrUpdateDevice() {},
|
|
_checkRemoteCommandsUpdateNeeded: async () => false,
|
|
},
|
|
profile: {
|
|
getProfile() {
|
|
return null;
|
|
},
|
|
},
|
|
});
|
|
// and for convenience so we don't have to touch as many lines in this test
|
|
// when we refactored FxAccounts.sys.mjs :)
|
|
result.setSignedInUser = function (creds) {
|
|
return result._internal.setSignedInUser(creds);
|
|
};
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Some tests want a "real" fxa instance - however, we still mock the storage
|
|
* to keep the tests fast on b2g.
|
|
*/
|
|
async function MakeFxAccounts({ internal = {}, credentials } = {}) {
|
|
if (!internal.newAccountState) {
|
|
// we use a real accountState but mocked storage.
|
|
internal.newAccountState = function (newCredentials) {
|
|
let storage = new MockStorageManager();
|
|
storage.initialize(newCredentials);
|
|
return new AccountState(storage);
|
|
};
|
|
}
|
|
if (!internal._signOutServer) {
|
|
internal._signOutServer = () => Promise.resolve();
|
|
}
|
|
if (internal.device) {
|
|
if (!internal.device._registerOrUpdateDevice) {
|
|
internal.device._registerOrUpdateDevice = () => Promise.resolve();
|
|
internal.device._checkRemoteCommandsUpdateNeeded = async () => false;
|
|
}
|
|
} else {
|
|
internal.device = {
|
|
_registerOrUpdateDevice() {},
|
|
_checkRemoteCommandsUpdateNeeded: async () => false,
|
|
};
|
|
}
|
|
if (!internal.observerPreloads) {
|
|
internal.observerPreloads = [];
|
|
}
|
|
let result = new FxAccounts(internal);
|
|
|
|
if (credentials) {
|
|
await result._internal.setSignedInUser(credentials);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
add_task(async function test_get_signed_in_user_initially_unset() {
|
|
_("Check getSignedInUser initially and after signout reports no user");
|
|
let account = await MakeFxAccounts();
|
|
let credentials = {
|
|
email: "foo@example.com",
|
|
uid: "1234567890abcdef1234567890abcdef",
|
|
sessionToken: "dead",
|
|
verified: true,
|
|
...MOCK_ACCOUNT_KEYS,
|
|
};
|
|
let result = await account.getSignedInUser();
|
|
Assert.equal(result, null);
|
|
|
|
await account._internal.setSignedInUser(credentials);
|
|
|
|
// getSignedInUser only returns a subset.
|
|
result = await account.getSignedInUser();
|
|
Assert.deepEqual(result.email, credentials.email);
|
|
Assert.deepEqual(result.scopedKeys, undefined);
|
|
|
|
// for the sake of testing, use the low-level function to check it's all there
|
|
result = await account._internal.currentAccountState.getUserAccountData();
|
|
Assert.deepEqual(result.email, credentials.email);
|
|
Assert.deepEqual(result.scopedKeys, credentials.scopedKeys);
|
|
|
|
// sign out
|
|
let localOnly = true;
|
|
await account.signOut(localOnly);
|
|
|
|
// user should be undefined after sign out
|
|
result = await account.getSignedInUser();
|
|
Assert.equal(result, null);
|
|
});
|
|
|
|
add_task(async function test_set_signed_in_user_signs_out_previous_account() {
|
|
_("Check setSignedInUser signs out the previous account.");
|
|
let signOutServerCalled = false;
|
|
let credentials = {
|
|
email: "foo@example.com",
|
|
uid: "1234567890abcdef1234567890abcdef",
|
|
sessionToken: "dead",
|
|
verified: true,
|
|
...MOCK_ACCOUNT_KEYS,
|
|
};
|
|
let account = await MakeFxAccounts({ credentials });
|
|
|
|
account._internal._signOutServer = () => {
|
|
signOutServerCalled = true;
|
|
return Promise.resolve(true);
|
|
};
|
|
|
|
await account._internal.setSignedInUser(credentials);
|
|
Assert.ok(signOutServerCalled);
|
|
});
|
|
|
|
add_task(async function test_update_account_data() {
|
|
_("Check updateUserAccountData does the right thing.");
|
|
let credentials = {
|
|
email: "foo@example.com",
|
|
uid: "1234567890abcdef1234567890abcdef",
|
|
sessionToken: "dead",
|
|
verified: true,
|
|
...MOCK_ACCOUNT_KEYS,
|
|
};
|
|
let account = await MakeFxAccounts({ credentials });
|
|
|
|
let newCreds = {
|
|
email: credentials.email,
|
|
uid: credentials.uid,
|
|
sessionToken: "alive",
|
|
};
|
|
await account._internal.updateUserAccountData(newCreds);
|
|
Assert.equal(
|
|
(await account._internal.getUserAccountData()).sessionToken,
|
|
"alive",
|
|
"new field value was saved"
|
|
);
|
|
|
|
// but we should fail attempting to change the uid.
|
|
newCreds = {
|
|
email: credentials.email,
|
|
uid: "11111111111111111111222222222222",
|
|
sessionToken: "alive",
|
|
};
|
|
await Assert.rejects(
|
|
account._internal.updateUserAccountData(newCreds),
|
|
/The specified credentials aren't for the current user/
|
|
);
|
|
|
|
// should fail without the uid.
|
|
newCreds = {
|
|
sessionToken: "alive",
|
|
};
|
|
await Assert.rejects(
|
|
account._internal.updateUserAccountData(newCreds),
|
|
/The specified credentials have no uid/
|
|
);
|
|
|
|
// and should fail with a field name that's not known by storage.
|
|
newCreds = {
|
|
email: credentials.email,
|
|
uid: "11111111111111111111222222222222",
|
|
foo: "bar",
|
|
};
|
|
await Assert.rejects(
|
|
account._internal.updateUserAccountData(newCreds),
|
|
/The specified credentials aren't for the current user/
|
|
);
|
|
});
|
|
|
|
// Sanity-check that our mocked client is working correctly
|
|
add_test(function test_client_mock() {
|
|
let fxa = new MockFxAccounts();
|
|
let client = fxa._internal.fxAccountsClient;
|
|
Assert.equal(client._verified, false);
|
|
Assert.equal(typeof client.signIn, "function");
|
|
|
|
// The recoveryEmailStatus function eventually fulfills its promise
|
|
client.recoveryEmailStatus().then(response => {
|
|
Assert.equal(response.verified, false);
|
|
run_next_test();
|
|
});
|
|
});
|
|
|
|
// Sign in a user, and after a little while, verify the user's email.
|
|
// Right after signing in the user, we should get the 'onlogin' notification.
|
|
// Polling should detect that the email is verified, and eventually
|
|
// 'onverified' should be observed
|
|
add_test(function test_verification_poll() {
|
|
ensureOauthNotConfigured();
|
|
let fxa = new MockFxAccounts();
|
|
let test_user = getTestUser("francine");
|
|
let login_notification_received = false;
|
|
|
|
makeObserver(ONVERIFIED_NOTIFICATION, function () {
|
|
log.debug("test_verification_poll observed onverified");
|
|
// Once email verification is complete, we will observe onverified
|
|
fxa._internal
|
|
.getUserAccountData()
|
|
.then(user => {
|
|
// And confirm that the user's state has changed
|
|
Assert.equal(user.verified, true);
|
|
Assert.equal(user.email, test_user.email);
|
|
Assert.equal(
|
|
Services.prefs.getStringPref(PREF_LAST_FXA_USER),
|
|
CryptoUtils.sha256Base64(test_user.email)
|
|
);
|
|
Assert.ok(login_notification_received);
|
|
})
|
|
.finally(run_next_test);
|
|
});
|
|
|
|
makeObserver(ONLOGIN_NOTIFICATION, function () {
|
|
log.debug("test_verification_poll observer onlogin");
|
|
login_notification_received = true;
|
|
});
|
|
|
|
fxa.setSignedInUser(test_user).then(() => {
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
// The user is signing in, but email has not been verified yet
|
|
Assert.equal(user.verified, false);
|
|
do_timeout(200, function () {
|
|
log.debug("Mocking verification of francine's email");
|
|
fxa._internal.fxAccountsClient._email = test_user.email;
|
|
fxa._internal.fxAccountsClient._verified = true;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// Sign in the user, but never verify the email. The check-email
|
|
// poll should time out. No verifiedlogin event should be observed, and the
|
|
// internal whenVerified promise should be rejected
|
|
add_test(function test_polling_timeout() {
|
|
ensureOauthNotConfigured();
|
|
// This test could be better - the onverified observer might fire on
|
|
// somebody else's stack, and we're not making sure that we're not receiving
|
|
// such a message. In other words, this tests either failure, or success, but
|
|
// not both.
|
|
|
|
let fxa = new MockFxAccounts();
|
|
let test_user = getTestUser("carol");
|
|
|
|
let removeObserver = makeObserver(ONVERIFIED_NOTIFICATION, function () {
|
|
do_throw("We should not be getting a login event!");
|
|
});
|
|
|
|
fxa._internal.POLL_SESSION = 1;
|
|
|
|
let p = fxa._internal.whenVerified({});
|
|
|
|
fxa.setSignedInUser(test_user).then(() => {
|
|
p.then(
|
|
() => {
|
|
do_throw("this should not succeed");
|
|
},
|
|
() => {
|
|
removeObserver();
|
|
fxa.signOut().then(run_next_test);
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
// For bug 1585299 - ensure we only get a single ONVERIFIED notification.
|
|
add_task(async function test_onverified_once() {
|
|
let fxa = new MockFxAccounts();
|
|
let user = getTestUser("francine");
|
|
|
|
let numNotifications = 0;
|
|
|
|
function observe() {
|
|
numNotifications += 1;
|
|
}
|
|
Services.obs.addObserver(observe, ONVERIFIED_NOTIFICATION);
|
|
|
|
fxa._internal.POLL_SESSION = 1;
|
|
|
|
await fxa.setSignedInUser(user);
|
|
|
|
Assert.ok(!(await fxa.getSignedInUser()).verified, "starts unverified");
|
|
|
|
await fxa._internal.startPollEmailStatus(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
"start"
|
|
);
|
|
|
|
Assert.ok(!(await fxa.getSignedInUser()).verified, "still unverified");
|
|
|
|
log.debug("Mocking verification of francine's email");
|
|
fxa._internal.fxAccountsClient._email = user.email;
|
|
fxa._internal.fxAccountsClient._verified = true;
|
|
|
|
await fxa._internal.startPollEmailStatus(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
"again"
|
|
);
|
|
|
|
Assert.ok((await fxa.getSignedInUser()).verified, "now verified");
|
|
|
|
Assert.equal(numNotifications, 1, "expect exactly 1 ONVERIFIED");
|
|
|
|
Services.obs.removeObserver(observe, ONVERIFIED_NOTIFICATION);
|
|
await fxa.signOut();
|
|
});
|
|
|
|
add_test(function test_pollEmailStatus_start_verified() {
|
|
let fxa = new MockFxAccounts();
|
|
let test_user = getTestUser("carol");
|
|
|
|
fxa._internal.POLL_SESSION = 20 * 60000;
|
|
fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 50000;
|
|
|
|
fxa.setSignedInUser(test_user).then(() => {
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
fxa._internal.fxAccountsClient._email = test_user.email;
|
|
fxa._internal.fxAccountsClient._verified = true;
|
|
const mock = sinon.mock(fxa._internal);
|
|
mock.expects("_scheduleNextPollEmailStatus").never();
|
|
fxa._internal
|
|
.startPollEmailStatus(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
"start"
|
|
)
|
|
.then(() => {
|
|
mock.verify();
|
|
mock.restore();
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
add_test(function test_pollEmailStatus_start() {
|
|
let fxa = new MockFxAccounts();
|
|
let test_user = getTestUser("carol");
|
|
|
|
fxa._internal.POLL_SESSION = 20 * 60000;
|
|
fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 123456;
|
|
|
|
fxa.setSignedInUser(test_user).then(() => {
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
const mock = sinon.mock(fxa._internal);
|
|
mock
|
|
.expects("_scheduleNextPollEmailStatus")
|
|
.once()
|
|
.withArgs(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
123456,
|
|
"start"
|
|
);
|
|
fxa._internal
|
|
.startPollEmailStatus(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
"start"
|
|
)
|
|
.then(() => {
|
|
mock.verify();
|
|
mock.restore();
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
add_test(function test_pollEmailStatus_start_subsequent() {
|
|
let fxa = new MockFxAccounts();
|
|
let test_user = getTestUser("carol");
|
|
|
|
fxa._internal.POLL_SESSION = 20 * 60000;
|
|
fxa._internal.VERIFICATION_POLL_TIMEOUT_INITIAL = 123456;
|
|
fxa._internal.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT = 654321;
|
|
fxa._internal.VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD = -1;
|
|
|
|
fxa.setSignedInUser(test_user).then(() => {
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
const mock = sinon.mock(fxa._internal);
|
|
mock
|
|
.expects("_scheduleNextPollEmailStatus")
|
|
.once()
|
|
.withArgs(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
654321,
|
|
"start"
|
|
);
|
|
fxa._internal
|
|
.startPollEmailStatus(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
"start"
|
|
)
|
|
.then(() => {
|
|
mock.verify();
|
|
mock.restore();
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
add_test(function test_pollEmailStatus_browser_startup() {
|
|
let fxa = new MockFxAccounts();
|
|
let test_user = getTestUser("carol");
|
|
|
|
fxa._internal.POLL_SESSION = 20 * 60000;
|
|
fxa._internal.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT = 654321;
|
|
|
|
fxa.setSignedInUser(test_user).then(() => {
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
const mock = sinon.mock(fxa._internal);
|
|
mock
|
|
.expects("_scheduleNextPollEmailStatus")
|
|
.once()
|
|
.withArgs(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
654321,
|
|
"browser-startup"
|
|
);
|
|
fxa._internal
|
|
.startPollEmailStatus(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
"browser-startup"
|
|
)
|
|
.then(() => {
|
|
mock.verify();
|
|
mock.restore();
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
add_test(function test_pollEmailStatus_push() {
|
|
let fxa = new MockFxAccounts();
|
|
let test_user = getTestUser("carol");
|
|
|
|
fxa.setSignedInUser(test_user).then(() => {
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
const mock = sinon.mock(fxa._internal);
|
|
mock.expects("_scheduleNextPollEmailStatus").never();
|
|
fxa._internal
|
|
.startPollEmailStatus(
|
|
fxa._internal.currentAccountState,
|
|
user.sessionToken,
|
|
"push"
|
|
)
|
|
.then(() => {
|
|
mock.verify();
|
|
mock.restore();
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
add_test(function test_getKeyForScope() {
|
|
let fxa = new MockFxAccounts();
|
|
let user = getTestUser("eusebius");
|
|
|
|
// Once email has been verified, we will be able to get keys
|
|
user.verified = true;
|
|
|
|
fxa.setSignedInUser(user).then(() => {
|
|
fxa._internal.getUserAccountData().then(user2 => {
|
|
// Before getKeyForScope, we have no keys
|
|
Assert.equal(!!user2.scopedKeys, false);
|
|
// And we still have a key-fetch token and unwrapBKey to use
|
|
Assert.equal(!!user2.keyFetchToken, true);
|
|
Assert.equal(!!user2.unwrapBKey, true);
|
|
|
|
fxa.keys.getKeyForScope(SCOPE_APP_SYNC).then(() => {
|
|
fxa._internal.getUserAccountData().then(user3 => {
|
|
// Now we should have keys
|
|
Assert.equal(!!user3.verified, true);
|
|
Assert.notEqual(null, user3.scopedKeys);
|
|
Assert.equal(user3.keyFetchToken, undefined);
|
|
Assert.equal(user3.unwrapBKey, undefined);
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
add_task(async function test_oauth_verification() {
|
|
ensureOauthConfigured();
|
|
let fxa = new MockFxAccounts();
|
|
let user = getTestUser("eusebius");
|
|
user.verified = true;
|
|
|
|
await fxa.setSignedInUser(user);
|
|
let fetched = await fxa.getSignedInUser();
|
|
Assert.ok(!fetched.verified);
|
|
|
|
fxa._withCurrentAccountState(state => {
|
|
state.updateUserAccountData({ scopedKeys: { test: { foo: "bar" } } });
|
|
});
|
|
|
|
fetched = await fxa.getSignedInUser();
|
|
Assert.ok(fetched.verified);
|
|
|
|
resetOauthConfig();
|
|
});
|
|
|
|
add_task(
|
|
async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_high_level_keys() {
|
|
let fxa = new MockFxAccounts();
|
|
let user = getTestUser("eusebius");
|
|
|
|
user.verified = true;
|
|
|
|
// An account state with the deprecated kinto extension sync keys...
|
|
user.kExtSync =
|
|
"f5ccd9cfdefd9b1ac4d02c56964f59239d8dfa1ca326e63696982765c1352cdc" +
|
|
"5d78a5a9c633a6d25edfea0a6c221a3480332a49fd866f311c2e3508ddd07395";
|
|
user.kExtKbHash =
|
|
"6192f1cc7dce95334455ba135fa1d8fca8f70e8f594ae318528de06f24ed0273";
|
|
user.scopedKeys = {
|
|
...MOCK_ACCOUNT_KEYS.scopedKeys,
|
|
};
|
|
|
|
await fxa.setSignedInUser(user);
|
|
// getKeyForScope will run the migration
|
|
await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
|
|
let newUser = await fxa._internal.getUserAccountData();
|
|
// Then, the deprecated keys will be removed
|
|
Assert.strictEqual(newUser.kExtSync, undefined);
|
|
Assert.strictEqual(newUser.kExtKbHash, undefined);
|
|
}
|
|
);
|
|
|
|
add_task(
|
|
async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_scoped_keys() {
|
|
let fxa = new MockFxAccounts();
|
|
let user = getTestUser("eusebius");
|
|
const DEPRECATED_SCOPE_WEBEXT_SYNC = "sync:addon_storage";
|
|
const EXTRA_SCOPE = "an unknown, but non-deprecated scope";
|
|
user.verified = true;
|
|
user.ecosystemUserId = "ecoUserId";
|
|
user.ecosystemAnonId = "ecoAnonId";
|
|
user.scopedKeys = {
|
|
...MOCK_ACCOUNT_KEYS.scopedKeys,
|
|
[DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY]:
|
|
MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
|
|
[DEPRECATED_SCOPE_WEBEXT_SYNC]:
|
|
MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
|
|
[EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
|
|
};
|
|
|
|
await fxa.setSignedInUser(user);
|
|
await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
|
|
let newUser = await fxa._internal.getUserAccountData();
|
|
// It should have removed the deprecated ecosystem_telemetry key,
|
|
// and the old kinto extension sync key
|
|
// but left the other keys intact.
|
|
const expectedScopedKeys = {
|
|
...MOCK_ACCOUNT_KEYS.scopedKeys,
|
|
[EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
|
|
};
|
|
Assert.deepEqual(newUser.scopedKeys, expectedScopedKeys);
|
|
Assert.equal(newUser.ecosystemUserId, null);
|
|
Assert.equal(newUser.ecosystemAnonId, null);
|
|
}
|
|
);
|
|
|
|
add_task(async function test_getKeyForScope_nonexistent_account() {
|
|
let fxa = new MockFxAccounts();
|
|
let bismarck = getTestUser("bismarck");
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.accountStatus = () => Promise.resolve(false);
|
|
client.sessionStatus = () => Promise.resolve(false);
|
|
client.accountKeys = () => {
|
|
return Promise.reject({
|
|
code: 401,
|
|
errno: ERRNO_INVALID_AUTH_TOKEN,
|
|
});
|
|
};
|
|
|
|
await fxa.setSignedInUser(bismarck);
|
|
|
|
let promiseLogout = new Promise(resolve => {
|
|
makeObserver(ONLOGOUT_NOTIFICATION, function () {
|
|
log.debug("test_getKeyForScope_nonexistent_account observed logout");
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await Assert.rejects(fxa.keys.getKeyForScope(SCOPE_APP_SYNC), err => {
|
|
Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE);
|
|
return true; // expected error
|
|
});
|
|
|
|
await promiseLogout;
|
|
|
|
let user = await fxa._internal.getUserAccountData();
|
|
Assert.equal(user, null);
|
|
});
|
|
|
|
// getKeyForScope with invalid keyFetchToken should delete keyFetchToken from storage
|
|
add_task(async function test_getKeyForScope_invalid_token() {
|
|
let fxa = new MockFxAccounts();
|
|
let yusuf = getTestUser("yusuf");
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.accountStatus = () => Promise.resolve(true); // account exists.
|
|
client.sessionStatus = () => Promise.resolve(false); // session is invalid.
|
|
client.accountKeys = () => {
|
|
return Promise.reject({
|
|
code: 401,
|
|
errno: ERRNO_INVALID_AUTH_TOKEN,
|
|
});
|
|
};
|
|
|
|
await fxa.setSignedInUser(yusuf);
|
|
let user = await fxa._internal.getUserAccountData();
|
|
Assert.notEqual(user.encryptedSendTabKeys, null);
|
|
|
|
try {
|
|
await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
|
|
Assert.ok(false);
|
|
} catch (err) {
|
|
Assert.equal(err.code, 401);
|
|
Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
|
|
}
|
|
|
|
user = await fxa._internal.getUserAccountData();
|
|
Assert.equal(user.email, yusuf.email);
|
|
Assert.equal(user.keyFetchToken, null);
|
|
// We verify that encryptedSendTabKeys are also wiped
|
|
// when a user's credentials are wiped
|
|
Assert.equal(user.encryptedSendTabKeys, null);
|
|
await fxa._internal.abortExistingFlow();
|
|
});
|
|
|
|
// Test vectors from
|
|
// https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#Test_Vectors
|
|
add_task(async function test_getKeyForScope_oldsync() {
|
|
let fxa = new MockFxAccounts();
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.getScopedKeyData = () =>
|
|
Promise.resolve({
|
|
[SCOPE_APP_SYNC]: {
|
|
identifier: SCOPE_APP_SYNC,
|
|
keyRotationSecret:
|
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
keyRotationTimestamp: 1510726317123,
|
|
},
|
|
});
|
|
|
|
// We mock the server returning the wrapKB from our test vectors
|
|
client.accountKeys = async () => {
|
|
return {
|
|
wrapKB: CommonUtils.hexToBytes(
|
|
"404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"
|
|
),
|
|
};
|
|
};
|
|
|
|
// We set the user to have the keyFetchToken and unwrapBKey from our test vectors
|
|
let user = {
|
|
...getTestUser("eusebius"),
|
|
uid: "aeaa1725c7a24ff983c6295725d5fc9b",
|
|
keyFetchToken:
|
|
"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
|
|
unwrapBKey:
|
|
"6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a",
|
|
sessionToken: "mock session token, used in metadata request",
|
|
verified: true,
|
|
};
|
|
await fxa.setSignedInUser(user);
|
|
|
|
// We derive, persist and return the sync key
|
|
const key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
|
|
|
|
// We verify the key returned matches what we would expect from the test vectors
|
|
// kb = 2ee722fdd8ccaa721bdeb2d1b76560efef705b04349d9357c3e592cf4906e075 (from test vectors)
|
|
//
|
|
// kid can be verified by "${keyRotationTimestamp}-${sha256(kb)[0:16]}"
|
|
//
|
|
// k can be verified by HKDF(kb, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)
|
|
Assert.deepEqual(key, {
|
|
scope: SCOPE_APP_SYNC,
|
|
kid: "1510726317123-BAik7hEOdpGnPZnPBSdaTg",
|
|
k: "fwM5VZu0Spf5XcFRZYX2zk6MrqZP7zvovCBcvuKwgYMif3hz98FHmIVa3qjKjrW0J244Zj-P5oWaOcQbvypmpw",
|
|
kty: "oct",
|
|
});
|
|
});
|
|
|
|
add_task(async function test_getScopedKeys_cached_key() {
|
|
let fxa = new MockFxAccounts();
|
|
let user = {
|
|
...getTestUser("eusebius"),
|
|
uid: "aeaa1725c7a24ff983c6295725d5fc9b",
|
|
verified: true,
|
|
...MOCK_ACCOUNT_KEYS,
|
|
};
|
|
|
|
await fxa.setSignedInUser(user);
|
|
let key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
|
|
Assert.deepEqual(key, {
|
|
scope: SCOPE_APP_SYNC,
|
|
...MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
|
|
});
|
|
});
|
|
|
|
add_task(async function test_getScopedKeys_unavailable_scope() {
|
|
let fxa = new MockFxAccounts();
|
|
let user = {
|
|
...getTestUser("eusebius"),
|
|
uid: "aeaa1725c7a24ff983c6295725d5fc9b",
|
|
verified: true,
|
|
...MOCK_ACCOUNT_KEYS,
|
|
};
|
|
await fxa.setSignedInUser(user);
|
|
await Assert.rejects(
|
|
fxa.keys.getKeyForScope("otherkeybearingscope"),
|
|
/Key not available for scope/
|
|
);
|
|
});
|
|
|
|
add_task(async function test_getScopedKeys_misconfigured_fxa_server() {
|
|
let fxa = new MockFxAccounts();
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.getScopedKeyData = () =>
|
|
Promise.resolve({
|
|
wrongscope: {
|
|
identifier: "wrongscope",
|
|
keyRotationSecret:
|
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
keyRotationTimestamp: 1510726331712,
|
|
},
|
|
});
|
|
let user = {
|
|
...getTestUser("eusebius"),
|
|
uid: "aeaa1725c7a24ff983c6295725d5fc9b",
|
|
verified: true,
|
|
keyFetchToken:
|
|
"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
|
|
unwrapBKey:
|
|
"6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a",
|
|
sessionToken: "mock session token, used in metadata request",
|
|
};
|
|
await fxa.setSignedInUser(user);
|
|
await Assert.rejects(
|
|
fxa.keys.getKeyForScope(SCOPE_APP_SYNC),
|
|
/The FxA server did not grant Firefox the sync scope/
|
|
);
|
|
});
|
|
|
|
add_task(async function test_setScopedKeys() {
|
|
const fxa = new MockFxAccounts();
|
|
const user = {
|
|
...getTestUser("foo"),
|
|
verified: true,
|
|
};
|
|
await fxa.setSignedInUser(user);
|
|
await fxa.keys.setScopedKeys(MOCK_ACCOUNT_KEYS.scopedKeys);
|
|
const key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
|
|
Assert.deepEqual(key, {
|
|
scope: SCOPE_APP_SYNC,
|
|
...MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
|
|
});
|
|
});
|
|
|
|
add_task(async function test_setScopedKeys_user_not_signed_in() {
|
|
const fxa = new MockFxAccounts();
|
|
await Assert.rejects(
|
|
fxa.keys.setScopedKeys(MOCK_ACCOUNT_KEYS.scopedKeys),
|
|
/Cannot persist keys, no user signed in/
|
|
);
|
|
});
|
|
|
|
// _fetchAndUnwrapAndDeriveKeys with no keyFetchToken should trigger signOut
|
|
// XXX - actually, it probably shouldn't - bug 1572313.
|
|
add_test(function test_fetchAndUnwrapAndDeriveKeys_no_token() {
|
|
let fxa = new MockFxAccounts();
|
|
let user = getTestUser("lettuce.protheroe");
|
|
delete user.keyFetchToken;
|
|
|
|
makeObserver(ONLOGOUT_NOTIFICATION, function () {
|
|
log.debug("test_fetchAndUnwrapKeys_no_token observed logout");
|
|
fxa._internal.getUserAccountData().then(() => {
|
|
fxa._internal.abortExistingFlow().then(run_next_test);
|
|
});
|
|
});
|
|
|
|
fxa
|
|
.setSignedInUser(user)
|
|
.then(() => {
|
|
return fxa.keys._fetchAndUnwrapAndDeriveKeys();
|
|
})
|
|
.catch(() => {
|
|
log.info("setSignedInUser correctly rejected");
|
|
});
|
|
});
|
|
|
|
// Alice (User A) signs up but never verifies her email. Then Bob (User B)
|
|
// signs in with a verified email. Ensure that no sign-in events are triggered
|
|
// on Alice's behalf. In the end, Bob should be the signed-in user.
|
|
add_test(function test_overlapping_signins() {
|
|
ensureOauthNotConfigured();
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
let bob = getTestUser("bob");
|
|
|
|
makeObserver(ONVERIFIED_NOTIFICATION, function () {
|
|
log.debug("test_overlapping_signins observed onverified");
|
|
// Once email verification is complete, we will observe onverified
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
Assert.equal(user.email, bob.email);
|
|
Assert.equal(user.verified, true);
|
|
run_next_test();
|
|
});
|
|
});
|
|
|
|
// Alice is the user signing in; her email is unverified.
|
|
fxa.setSignedInUser(alice).then(() => {
|
|
log.debug("Alice signing in ...");
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
Assert.equal(user.email, alice.email);
|
|
Assert.equal(user.verified, false);
|
|
log.debug("Alice has not verified her email ...");
|
|
|
|
// Now Bob signs in instead and actually verifies his email
|
|
log.debug("Bob signing in ...");
|
|
fxa.setSignedInUser(bob).then(() => {
|
|
do_timeout(200, function () {
|
|
// Mock email verification ...
|
|
log.debug("Bob verifying his email ...");
|
|
fxa._internal.fxAccountsClient._verified = true;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
add_task(async function test_resend_email_not_signed_in() {
|
|
let fxa = new MockFxAccounts();
|
|
|
|
try {
|
|
await fxa.resendVerificationEmail();
|
|
} catch (err) {
|
|
Assert.equal(err.message, ERROR_NO_ACCOUNT);
|
|
return;
|
|
}
|
|
do_throw("Should not be able to resend email when nobody is signed in");
|
|
});
|
|
|
|
add_task(async function test_accountStatus() {
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
|
|
// If we have no user, we have no account server-side
|
|
let result = await fxa.checkAccountStatus();
|
|
Assert.ok(!result);
|
|
// Set a user - the fxAccountsClient mock will say "ok".
|
|
await fxa.setSignedInUser(alice);
|
|
result = await fxa.checkAccountStatus();
|
|
Assert.ok(result);
|
|
// flag the item as deleted on the server.
|
|
fxa._internal.fxAccountsClient._deletedOnServer = true;
|
|
result = await fxa.checkAccountStatus();
|
|
Assert.ok(!result);
|
|
fxa._internal.fxAccountsClient._deletedOnServer = false;
|
|
await fxa.signOut();
|
|
});
|
|
|
|
add_task(async function test_resend_email_invalid_token() {
|
|
let fxa = new MockFxAccounts();
|
|
let sophia = getTestUser("sophia");
|
|
Assert.notEqual(sophia.sessionToken, null);
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.resendVerificationEmail = () => {
|
|
return Promise.reject({
|
|
code: 401,
|
|
errno: ERRNO_INVALID_AUTH_TOKEN,
|
|
});
|
|
};
|
|
// This test wants the account to exist but the local session invalid.
|
|
client.accountStatus = uid => {
|
|
Assert.ok(uid, "got a uid to check");
|
|
return Promise.resolve(true);
|
|
};
|
|
client.sessionStatus = token => {
|
|
Assert.ok(token, "got a token to check");
|
|
return Promise.resolve(false);
|
|
};
|
|
|
|
await fxa.setSignedInUser(sophia);
|
|
let user = await fxa._internal.getUserAccountData();
|
|
Assert.equal(user.email, sophia.email);
|
|
Assert.equal(user.verified, false);
|
|
log.debug("Sophia wants verification email resent");
|
|
|
|
try {
|
|
await fxa.resendVerificationEmail();
|
|
Assert.ok(
|
|
false,
|
|
"resendVerificationEmail should reject invalid session token"
|
|
);
|
|
} catch (err) {
|
|
Assert.equal(err.code, 401);
|
|
Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
|
|
}
|
|
|
|
user = await fxa._internal.getUserAccountData();
|
|
Assert.equal(user.email, sophia.email);
|
|
Assert.equal(user.sessionToken, null);
|
|
await fxa._internal.abortExistingFlow();
|
|
});
|
|
|
|
add_test(function test_resend_email() {
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
|
|
let initialState = fxa._internal.currentAccountState;
|
|
|
|
// Alice is the user signing in; her email is unverified.
|
|
fxa.setSignedInUser(alice).then(() => {
|
|
log.debug("Alice signing in");
|
|
|
|
// We're polling for the first email
|
|
Assert.ok(fxa._internal.currentAccountState !== initialState);
|
|
let aliceState = fxa._internal.currentAccountState;
|
|
|
|
// The polling timer is ticking
|
|
Assert.ok(fxa._internal.currentTimer > 0);
|
|
|
|
fxa._internal.getUserAccountData().then(user => {
|
|
Assert.equal(user.email, alice.email);
|
|
Assert.equal(user.verified, false);
|
|
log.debug("Alice wants verification email resent");
|
|
|
|
fxa.resendVerificationEmail().then(result => {
|
|
// Mock server response; ensures that the session token actually was
|
|
// passed to the client to make the hawk call
|
|
Assert.equal(result, "alice's session token");
|
|
|
|
// Timer was not restarted
|
|
Assert.ok(fxa._internal.currentAccountState === aliceState);
|
|
|
|
// Timer is still ticking
|
|
Assert.ok(fxa._internal.currentTimer > 0);
|
|
|
|
// Ok abort polling before we go on to the next test
|
|
fxa._internal.abortExistingFlow();
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
Services.prefs.setStringPref(
|
|
"identity.fxaccounts.remote.oauth.uri",
|
|
"https://example.com/v1"
|
|
);
|
|
|
|
add_test(async function test_getOAuthTokenWithSessionToken() {
|
|
Services.prefs.setBoolPref(
|
|
"identity.fxaccounts.useSessionTokensForOAuth",
|
|
true
|
|
);
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
let oauthTokenCalled = false;
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.accessTokenWithSessionToken = async (
|
|
sessionTokenHex,
|
|
clientId,
|
|
scope,
|
|
ttl
|
|
) => {
|
|
oauthTokenCalled = true;
|
|
Assert.equal(sessionTokenHex, "alice's session token");
|
|
Assert.equal(
|
|
clientId,
|
|
CLIENT_IS_THUNDERBIRD ? "8269bacd7bbc7f80" : "5882386c6d801776"
|
|
);
|
|
Assert.equal(scope, "profile");
|
|
Assert.equal(ttl, undefined);
|
|
return MOCK_TOKEN_RESPONSE;
|
|
};
|
|
|
|
await fxa.setSignedInUser(alice);
|
|
const result = await fxa.getOAuthToken({ scope: "profile" });
|
|
Assert.ok(oauthTokenCalled);
|
|
Assert.equal(result, MOCK_TOKEN_RESPONSE.access_token);
|
|
Services.prefs.setBoolPref(
|
|
"identity.fxaccounts.useSessionTokensForOAuth",
|
|
false
|
|
);
|
|
run_next_test();
|
|
});
|
|
|
|
add_task(async function test_getOAuthTokenCachedWithSessionToken() {
|
|
Services.prefs.setBoolPref(
|
|
"identity.fxaccounts.useSessionTokensForOAuth",
|
|
true
|
|
);
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
let numOauthTokenCalls = 0;
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.accessTokenWithSessionToken = async () => {
|
|
numOauthTokenCalls++;
|
|
return MOCK_TOKEN_RESPONSE;
|
|
};
|
|
|
|
await fxa.setSignedInUser(alice);
|
|
let result = await fxa.getOAuthToken({
|
|
scope: "profile",
|
|
service: "test-service",
|
|
});
|
|
Assert.equal(numOauthTokenCalls, 1);
|
|
Assert.equal(
|
|
result,
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
|
|
);
|
|
|
|
// requesting it again should not re-fetch the token.
|
|
result = await fxa.getOAuthToken({
|
|
scope: "profile",
|
|
service: "test-service",
|
|
});
|
|
Assert.equal(numOauthTokenCalls, 1);
|
|
Assert.equal(
|
|
result,
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
|
|
);
|
|
// But requesting the same service and a different scope *will* get a new one.
|
|
result = await fxa.getOAuthToken({
|
|
scope: "something-else",
|
|
service: "test-service",
|
|
});
|
|
Assert.equal(numOauthTokenCalls, 2);
|
|
Assert.equal(
|
|
result,
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
|
|
);
|
|
Services.prefs.setBoolPref(
|
|
"identity.fxaccounts.useSessionTokensForOAuth",
|
|
false
|
|
);
|
|
});
|
|
|
|
add_test(function test_getOAuthTokenScopedWithSessionToken() {
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
let numOauthTokenCalls = 0;
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.accessTokenWithSessionToken = async (
|
|
_sessionTokenHex,
|
|
_clientId,
|
|
scopeString
|
|
) => {
|
|
equal(scopeString, "bar foo"); // scopes are sorted locally before request.
|
|
numOauthTokenCalls++;
|
|
return MOCK_TOKEN_RESPONSE;
|
|
};
|
|
|
|
fxa.setSignedInUser(alice).then(() => {
|
|
fxa.getOAuthToken({ scope: ["foo", "bar"] }).then(result => {
|
|
Assert.equal(numOauthTokenCalls, 1);
|
|
Assert.equal(
|
|
result,
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
|
|
);
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
|
|
add_task(async function test_getOAuthTokenCachedScopeNormalization() {
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
let numOAuthTokenCalls = 0;
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.accessTokenWithSessionToken = async (_sessionTokenHex, _clientId) => {
|
|
numOAuthTokenCalls++;
|
|
return MOCK_TOKEN_RESPONSE;
|
|
};
|
|
|
|
await fxa.setSignedInUser(alice);
|
|
let result = await fxa.getOAuthToken({
|
|
scope: ["foo", "bar"],
|
|
service: "test-service",
|
|
});
|
|
Assert.equal(numOAuthTokenCalls, 1);
|
|
Assert.equal(
|
|
result,
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
|
|
);
|
|
|
|
// requesting it again with the scope array in a different order should not re-fetch the token.
|
|
result = await fxa.getOAuthToken({
|
|
scope: ["bar", "foo"],
|
|
service: "test-service",
|
|
});
|
|
Assert.equal(numOAuthTokenCalls, 1);
|
|
Assert.equal(
|
|
result,
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
|
|
);
|
|
// requesting it again with the scope array in different case should not re-fetch the token.
|
|
result = await fxa.getOAuthToken({
|
|
scope: ["Bar", "Foo"],
|
|
service: "test-service",
|
|
});
|
|
Assert.equal(numOAuthTokenCalls, 1);
|
|
Assert.equal(
|
|
result,
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
|
|
);
|
|
// But requesting with a new entry in the array does fetch one.
|
|
result = await fxa.getOAuthToken({
|
|
scope: ["foo", "bar", "etc"],
|
|
service: "test-service",
|
|
});
|
|
Assert.equal(numOAuthTokenCalls, 2);
|
|
Assert.equal(
|
|
result,
|
|
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
|
|
);
|
|
});
|
|
|
|
add_test(function test_getOAuthToken_invalid_param() {
|
|
let fxa = new MockFxAccounts();
|
|
|
|
fxa.getOAuthToken().catch(err => {
|
|
Assert.equal(err.message, "INVALID_PARAMETER");
|
|
fxa.signOut().then(run_next_test);
|
|
});
|
|
});
|
|
|
|
add_test(function test_getOAuthToken_invalid_scope_array() {
|
|
let fxa = new MockFxAccounts();
|
|
|
|
fxa.getOAuthToken({ scope: [] }).catch(err => {
|
|
Assert.equal(err.message, "INVALID_PARAMETER");
|
|
fxa.signOut().then(run_next_test);
|
|
});
|
|
});
|
|
|
|
add_test(function test_getOAuthToken_misconfigure_oauth_uri() {
|
|
let fxa = new MockFxAccounts();
|
|
|
|
const prevServerURL = Services.prefs.getStringPref(
|
|
"identity.fxaccounts.remote.oauth.uri"
|
|
);
|
|
Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri");
|
|
|
|
fxa.getOAuthToken().catch(err => {
|
|
Assert.equal(err.message, "INVALID_PARAMETER");
|
|
// revert the pref
|
|
Services.prefs.setStringPref(
|
|
"identity.fxaccounts.remote.oauth.uri",
|
|
prevServerURL
|
|
);
|
|
fxa.signOut().then(run_next_test);
|
|
});
|
|
});
|
|
|
|
add_test(function test_getOAuthToken_no_account() {
|
|
let fxa = new MockFxAccounts();
|
|
|
|
fxa._internal.currentAccountState.getUserAccountData = function () {
|
|
return Promise.resolve(null);
|
|
};
|
|
|
|
fxa.getOAuthToken({ scope: "profile" }).catch(err => {
|
|
Assert.equal(err.message, "NO_ACCOUNT");
|
|
fxa.signOut().then(run_next_test);
|
|
});
|
|
});
|
|
|
|
add_test(function test_getOAuthToken_unverified() {
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
|
|
fxa.setSignedInUser(alice).then(() => {
|
|
fxa.getOAuthToken({ scope: "profile" }).catch(err => {
|
|
Assert.equal(err.message, "UNVERIFIED_ACCOUNT");
|
|
fxa.signOut().then(run_next_test);
|
|
});
|
|
});
|
|
});
|
|
|
|
add_test(function test_getOAuthToken_error() {
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.accessTokenWithSessionToken = () => {
|
|
return Promise.reject("boom");
|
|
};
|
|
|
|
fxa.setSignedInUser(alice).then(() => {
|
|
fxa.getOAuthToken({ scope: "profile" }).catch(err => {
|
|
equal(err.details, "boom");
|
|
run_next_test();
|
|
});
|
|
});
|
|
});
|
|
|
|
add_test(async function test_getOAuthTokenAndKey_errors_if_user_change() {
|
|
const fxa = new MockFxAccounts();
|
|
const alice = getTestUser("alice");
|
|
const bob = getTestUser("bob");
|
|
alice.verified = true;
|
|
bob.verified = true;
|
|
|
|
fxa.getOAuthToken = async () => {
|
|
// We mock what would happen if the user got changed
|
|
// after we got the access token
|
|
await fxa.setSignedInUser(bob);
|
|
return "access token";
|
|
};
|
|
fxa.keys.getKeyForScope = () => Promise.resolve("key!");
|
|
await fxa.setSignedInUser(alice);
|
|
await Assert.rejects(
|
|
fxa.getOAuthTokenAndKey({ scope: "foo", ttl: 10 }),
|
|
err => {
|
|
Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE);
|
|
return true; // expected error
|
|
}
|
|
);
|
|
run_next_test();
|
|
});
|
|
|
|
add_task(async function test_listAttachedOAuthClients() {
|
|
const ONE_HOUR = 60 * 60 * 1000;
|
|
const ONE_DAY = 24 * ONE_HOUR;
|
|
|
|
const timestamp = Date.now();
|
|
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.attachedClients = async () => {
|
|
return {
|
|
body: [
|
|
// This entry was previously filtered but no longer is!
|
|
{
|
|
clientId: "a2270f727f45f648",
|
|
deviceId: "deadbeef",
|
|
sessionTokenId: null,
|
|
name: "Firefox Preview (no session token)",
|
|
scope: ["profile", SCOPE_APP_SYNC],
|
|
lastAccessTime: Date.now(),
|
|
},
|
|
{
|
|
clientId: "802d56ef2a9af9fa",
|
|
deviceId: null,
|
|
sessionTokenId: null,
|
|
name: "Firefox Monitor",
|
|
scope: ["profile"],
|
|
lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR,
|
|
},
|
|
{
|
|
clientId: "1f30e32975ae5112",
|
|
deviceId: null,
|
|
sessionTokenId: null,
|
|
name: "Firefox Send",
|
|
scope: ["profile", "https://identity.mozilla.com/apps/send"],
|
|
lastAccessTime: Date.now() - ONE_DAY * 2 - ONE_HOUR,
|
|
},
|
|
// One with a future date should be impossible, but having a negative
|
|
// result here would almost certainly confuse something!
|
|
{
|
|
clientId: "future-date",
|
|
deviceId: null,
|
|
sessionTokenId: null,
|
|
name: "Whatever",
|
|
lastAccessTime: Date.now() + ONE_DAY,
|
|
},
|
|
// A missing/null lastAccessTime should end up with a missing lastAccessedDaysAgo
|
|
{
|
|
clientId: "missing-date",
|
|
deviceId: null,
|
|
sessionTokenId: null,
|
|
name: "Whatever",
|
|
},
|
|
],
|
|
headers: { "x-timestamp": timestamp.toString() },
|
|
};
|
|
};
|
|
|
|
await fxa.setSignedInUser(alice);
|
|
const clients = await fxa.listAttachedOAuthClients();
|
|
Assert.deepEqual(clients, [
|
|
{
|
|
id: "a2270f727f45f648",
|
|
lastAccessedDaysAgo: 0,
|
|
},
|
|
{
|
|
id: "802d56ef2a9af9fa",
|
|
lastAccessedDaysAgo: 1,
|
|
},
|
|
{
|
|
id: "1f30e32975ae5112",
|
|
lastAccessedDaysAgo: 2,
|
|
},
|
|
{
|
|
id: "future-date",
|
|
lastAccessedDaysAgo: 0,
|
|
},
|
|
{
|
|
id: "missing-date",
|
|
lastAccessedDaysAgo: null,
|
|
},
|
|
]);
|
|
});
|
|
|
|
add_task(async function test_listAttachedOAuthClients_withCaching() {
|
|
const ONE_HOUR = 60 * 60 * 1000;
|
|
const ONE_DAY = 24 * ONE_HOUR;
|
|
|
|
const timestamp = Date.now();
|
|
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
let originalResponse = {
|
|
body: [
|
|
{
|
|
clientId: "a2270f727f45f648",
|
|
deviceId: "deadbeef",
|
|
sessionTokenId: null,
|
|
name: "Firefox Preview (no session token)",
|
|
scope: ["profile", SCOPE_APP_SYNC],
|
|
lastAccessTime: Date.now(),
|
|
},
|
|
{
|
|
clientId: "802d56ef2a9af9fa",
|
|
deviceId: null,
|
|
sessionTokenId: null,
|
|
name: "Firefox Monitor",
|
|
scope: ["profile"],
|
|
lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR,
|
|
},
|
|
],
|
|
headers: { "x-timestamp": timestamp.toString() },
|
|
};
|
|
|
|
// Mock the client method.
|
|
client.attachedClients = async () => {
|
|
return originalResponse;
|
|
};
|
|
|
|
await fxa.setSignedInUser(alice);
|
|
|
|
// First call: should fetch from server
|
|
const clientsFirstCall = await fxa.listAttachedOAuthClients();
|
|
Assert.deepEqual(clientsFirstCall, [
|
|
{ id: "a2270f727f45f648", lastAccessedDaysAgo: 0 },
|
|
{ id: "802d56ef2a9af9fa", lastAccessedDaysAgo: 1 },
|
|
]);
|
|
|
|
// Now modify the client so if it calls again, we would get different clients.
|
|
const updatedResponse = {
|
|
body: [
|
|
{
|
|
clientId: "updated-client",
|
|
lastAccessTime: Date.now() - ONE_DAY * 3,
|
|
},
|
|
],
|
|
headers: { "x-timestamp": (timestamp + 1000).toString() },
|
|
};
|
|
client.attachedClients = async () => {
|
|
return updatedResponse;
|
|
};
|
|
|
|
// Second call without forceRefresh: should return cached data, not the updated one.
|
|
const clientsSecondCall = await fxa.listAttachedOAuthClients();
|
|
Assert.deepEqual(
|
|
clientsSecondCall,
|
|
[
|
|
{ id: "a2270f727f45f648", lastAccessedDaysAgo: 0 },
|
|
{ id: "802d56ef2a9af9fa", lastAccessedDaysAgo: 1 },
|
|
],
|
|
"Should return cached clients from the first call, ignoring the updated mock"
|
|
);
|
|
|
|
// Now force refresh
|
|
const clientsForceRefresh = await fxa.listAttachedOAuthClients(true);
|
|
Assert.deepEqual(
|
|
clientsForceRefresh,
|
|
[{ id: "updated-client", lastAccessedDaysAgo: 3 }],
|
|
"Forcing a refresh should return the updated data"
|
|
);
|
|
});
|
|
|
|
add_task(async function test_getSignedInUserProfile() {
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
|
|
let mockProfile = {
|
|
getProfile() {
|
|
return Promise.resolve({ avatar: "image" });
|
|
},
|
|
tearDown() {},
|
|
};
|
|
let fxa = new FxAccounts({
|
|
_signOutServer() {
|
|
return Promise.resolve();
|
|
},
|
|
device: {
|
|
_registerOrUpdateDevice() {
|
|
return Promise.resolve();
|
|
},
|
|
},
|
|
});
|
|
|
|
await fxa._internal.setSignedInUser(alice);
|
|
fxa._internal._profile = mockProfile;
|
|
let result = await fxa.getSignedInUser();
|
|
Assert.ok(!!result);
|
|
Assert.equal(result.avatar, "image");
|
|
});
|
|
|
|
add_task(async function test_getSignedInUserProfile_error_uses_account_data() {
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
|
|
fxa._internal.getSignedInUser = function () {
|
|
return Promise.resolve({ email: "foo@bar.com" });
|
|
};
|
|
fxa._internal._profile = {
|
|
getProfile() {
|
|
return Promise.reject("boom");
|
|
},
|
|
tearDown() {
|
|
teardownCalled = true;
|
|
},
|
|
};
|
|
|
|
let teardownCalled = false;
|
|
await fxa.setSignedInUser(alice);
|
|
let result = await fxa.getSignedInUser();
|
|
Assert.deepEqual(result.avatar, null);
|
|
await fxa.signOut();
|
|
Assert.ok(teardownCalled);
|
|
});
|
|
|
|
add_task(async function test_checkVerificationStatusFailed() {
|
|
let fxa = new MockFxAccounts();
|
|
let alice = getTestUser("alice");
|
|
alice.verified = true;
|
|
|
|
let client = fxa._internal.fxAccountsClient;
|
|
client.recoveryEmailStatus = () => {
|
|
return Promise.reject({
|
|
code: 401,
|
|
errno: ERRNO_INVALID_AUTH_TOKEN,
|
|
});
|
|
};
|
|
client.accountStatus = () => Promise.resolve(true);
|
|
client.sessionStatus = () => Promise.resolve(false);
|
|
|
|
await fxa.setSignedInUser(alice);
|
|
let user = await fxa._internal.getUserAccountData();
|
|
Assert.notEqual(alice.sessionToken, null);
|
|
Assert.equal(user.email, alice.email);
|
|
Assert.equal(user.verified, true);
|
|
|
|
await fxa._internal.checkVerificationStatus();
|
|
|
|
user = await fxa._internal.getUserAccountData();
|
|
Assert.equal(user.email, alice.email);
|
|
Assert.equal(user.sessionToken, null);
|
|
});
|
|
|
|
add_task(async function test_flushLogFile() {
|
|
_("Tests flushLogFile");
|
|
let account = await MakeFxAccounts();
|
|
let promiseObserved = new Promise(res => {
|
|
log.info("Adding flush-log-file observer.");
|
|
Services.obs.addObserver(function onFlushLogFile() {
|
|
Services.obs.removeObserver(
|
|
onFlushLogFile,
|
|
"service:log-manager:flush-log-file"
|
|
);
|
|
res();
|
|
}, "service:log-manager:flush-log-file");
|
|
});
|
|
account.flushLogFile();
|
|
await promiseObserved;
|
|
});
|
|
|
|
/*
|
|
* End of tests.
|
|
* Utility functions follow.
|
|
*/
|
|
|
|
function expandHex(two_hex) {
|
|
// Return a 64-character hex string, encoding 32 identical bytes.
|
|
let eight_hex = two_hex + two_hex + two_hex + two_hex;
|
|
let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex;
|
|
return thirtytwo_hex + thirtytwo_hex;
|
|
}
|
|
|
|
function expandBytes(two_hex) {
|
|
return CommonUtils.hexToBytes(expandHex(two_hex));
|
|
}
|
|
|
|
function getTestUser(name) {
|
|
return {
|
|
email: name + "@example.com",
|
|
uid: "1ad7f5024cc74ec1a209071fd2fae348",
|
|
sessionToken: name + "'s session token",
|
|
keyFetchToken: name + "'s keyfetch token",
|
|
unwrapBKey: expandHex("44"),
|
|
verified: false,
|
|
encryptedSendTabKeys: name + "'s encrypted Send tab keys",
|
|
};
|
|
}
|
|
|
|
function makeObserver(aObserveTopic, aObserveFunc) {
|
|
let observer = {
|
|
// nsISupports provides type management in C++
|
|
// nsIObserver is to be an observer
|
|
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
|
|
|
|
observe(aSubject, aTopic, aData) {
|
|
log.debug("observed " + aTopic + " " + aData);
|
|
if (aTopic == aObserveTopic) {
|
|
removeMe();
|
|
aObserveFunc(aSubject, aTopic, aData);
|
|
}
|
|
},
|
|
};
|
|
|
|
function removeMe() {
|
|
log.debug("removing observer for " + aObserveTopic);
|
|
Services.obs.removeObserver(observer, aObserveTopic);
|
|
}
|
|
|
|
Services.obs.addObserver(observer, aObserveTopic);
|
|
return removeMe;
|
|
}
|