gecko-dev/services/fxaccounts/tests/xpcshell/test_accounts.js

2013 строки
60 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { FxAccounts } = ChromeUtils.import(
"resource://gre/modules/FxAccounts.jsm"
);
const { FxAccountsClient } = ChromeUtils.import(
"resource://gre/modules/FxAccountsClient.jsm"
);
const {
ASSERTION_LIFETIME,
CERT_LIFETIME,
ERRNO_INVALID_AUTH_TOKEN,
ERROR_NETWORK,
ERROR_NO_ACCOUNT,
FX_OAUTH_CLIENT_ID,
KEY_LIFETIME,
ONLOGIN_NOTIFICATION,
ONLOGOUT_NOTIFICATION,
ONVERIFIED_NOTIFICATION,
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
const { PromiseUtils } = ChromeUtils.import(
"resource://gre/modules/PromiseUtils.jsm"
);
// We grab some additional stuff via backstage passes.
var { AccountState } = ChromeUtils.import(
"resource://gre/modules/FxAccounts.jsm",
null
);
const ONE_HOUR_MS = 1000 * 60 * 60;
const ONE_DAY_MS = ONE_HOUR_MS * 24;
const MOCK_TOKEN_RESPONSE = {
access_token:
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69",
token_type: "bearer",
scope: "https://identity.mozilla.com/apps/oldsync",
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.jsm and jwcrypto.jsm.
Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace");
Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace;
Services.prefs.setCharPref("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(sessionToken) {
// 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(keyFetchToken) {
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, FX_OAUTH_CLIENT_ID);
Assert.equal(scopes, SCOPE_OLD_SYNC + " " + SCOPE_ECOSYSTEM_TELEMETRY);
return new Promise(resolve => {
do_timeout(50, () => {
resolve({
"https://identity.mozilla.com/apps/oldsync": {
identifier: "https://identity.mozilla.com/apps/oldsync",
keyRotationSecret:
"0000000000000000000000000000000000000000000000000000000000000000",
keyRotationTimestamp: 1234567890123,
},
"https://identity.mozilla.com/ids/ecosystem_telemetry": {
identifier: "https://identity.mozilla.com/ids/ecosystem_telemetry",
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.signCertificate = function() {
throw new Error("no");
};
this.signOut = () => Promise.resolve();
FxAccountsClient.apply(this);
}
MockFxAccountsClient.prototype = {
__proto__: 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(credentials = null) {
let result = new FxAccounts({
VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms
_getCertificateSigned_calls: [],
_d_signCertificate: PromiseUtils.defer(),
_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);
},
getCertificateSigned(sessionToken, serializedPublicKey) {
_("mock getCertificateSigned\n");
this._getCertificateSigned_calls.push([
sessionToken,
serializedPublicKey,
]);
return this._d_signCertificate.promise;
},
fxAccountsClient: new MockFxAccountsClient(),
observerPreloads: [],
device: {
_registerOrUpdateDevice() {},
},
profile: {
getProfile() {
return null;
},
},
});
// and for convenience so we don't have to touch as many lines in this test
// when we refactored FxAccounts.jsm :)
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();
}
} else {
internal.device = {
_registerOrUpdateDevice() {},
};
}
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",
assertion: "foobar",
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.assertion, undefined);
Assert.deepEqual(result.scopedKeys, undefined);
Assert.deepEqual(result.kSync, undefined);
Assert.deepEqual(result.kXCS, undefined);
Assert.deepEqual(result.kExtSync, undefined);
Assert.deepEqual(result.kExtKbHash, 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.assertion, credentials.assertion);
Assert.deepEqual(result.scopedKeys, credentials.scopedKeys);
Assert.ok(result.kSync);
Assert.ok(result.kXCS);
Assert.ok(result.kExtSync);
Assert.ok(result.kExtKbHash);
// 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",
assertion: "foobar",
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",
assertion: "foobar",
sessionToken: "dead",
verified: true,
...MOCK_ACCOUNT_KEYS,
};
let account = await MakeFxAccounts({ credentials });
let newCreds = {
email: credentials.email,
uid: credentials.uid,
assertion: "new_assertion",
};
await account._internal.updateUserAccountData(newCreds);
Assert.equal(
(await account._internal.getUserAccountData()).assertion,
"new_assertion",
"new field value was saved"
);
// but we should fail attempting to change the uid.
newCreds = {
email: credentials.email,
uid: "11111111111111111111222222222222",
assertion: "new_assertion",
};
await Assert.rejects(
account._internal.updateUserAccountData(newCreds),
/The specified credentials aren't for the current user/
);
// should fail without the uid.
newCreds = {
assertion: "new_assertion",
};
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/
);
});
add_task(async function test_getCertificateOffline() {
_("getCertificateOffline()");
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
sessionToken: "dead",
verified: true,
};
let fxa = await MakeFxAccounts({ credentials });
// Test that an expired cert throws if we're offline.
let offline = Services.io.offline;
Services.io.offline = true;
await fxa._internal
.getKeypairAndCertificate(fxa._internal.currentAccountState)
.then(
result => {
Services.io.offline = offline;
do_throw("Unexpected success");
},
err => {
Services.io.offline = offline;
// ... so we have to check the error string.
Assert.equal(err, "Error: OFFLINE");
}
);
await fxa.signOut(/* localOnly = */ true);
});
add_task(async function test_getCertificateCached() {
_("getCertificateCached()");
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
sessionToken: "dead",
verified: true,
// A cached keypair and cert that remain valid.
keyPair: {
validUntil: Date.now() + KEY_LIFETIME + 10000,
rawKeyPair: "good-keypair",
},
cert: {
validUntil: Date.now() + CERT_LIFETIME + 10000,
rawCert: "good-cert",
},
};
let fxa = await MakeFxAccounts({ credentials });
let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
fxa._internal.currentAccountState
);
// should have the same keypair and cert.
Assert.equal(keyPair, credentials.keyPair.rawKeyPair);
Assert.equal(certificate, credentials.cert.rawCert);
await fxa.signOut(/* localOnly = */ true);
});
add_task(async function test_getCertificateExpiredCert() {
_("getCertificateExpiredCert()");
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef1234567890abcdef",
sessionToken: "dead",
verified: true,
// A cached keypair that remains valid.
keyPair: {
validUntil: Date.now() + KEY_LIFETIME + 10000,
rawKeyPair: "good-keypair",
},
// A cached certificate which has expired.
cert: {
validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT"),
rawCert: "expired-cert",
},
};
let fxa = await MakeFxAccounts({
internal: {
getCertificateSigned() {
return "new cert";
},
},
credentials,
});
let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
fxa._internal.currentAccountState
);
// should have the same keypair but a new cert.
Assert.equal(keyPair, credentials.keyPair.rawKeyPair);
Assert.notEqual(certificate, credentials.cert.rawCert);
await fxa.signOut(/* localOnly = */ true);
});
add_task(async function test_getCertificateExpiredKeypair() {
_("getCertificateExpiredKeypair()");
let credentials = {
email: "foo@example.com",
uid: "1234567890abcdef",
sessionToken: "dead",
verified: true,
// A cached keypair that has expired.
keyPair: {
validUntil: Date.now() - 1000,
rawKeyPair: "expired-keypair",
},
// A cached certificate which remains valid.
cert: {
validUntil: Date.now() + CERT_LIFETIME + 10000,
rawCert: "expired-cert",
},
};
let fxa = await MakeFxAccounts({
internal: {
getCertificateSigned() {
return "new cert";
},
},
credentials,
});
let { keyPair, certificate } = await fxa._internal.getKeypairAndCertificate(
fxa._internal.currentAccountState
);
// even though the cert was valid, the fact the keypair was not means we
// should have fetched both.
Assert.notEqual(keyPair, credentials.keyPair.rawKeyPair);
Assert.notEqual(certificate, credentials.cert.rawCert);
await fxa.signOut(/* localOnly = */ true);
});
// 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() {
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.ok(login_notification_received);
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() {
// 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(
success => {
do_throw("this should not succeed");
},
fail => {
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(aSubject, aTopic, aData) {
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);
Assert.equal(!!user2.kSync, false);
Assert.equal(!!user2.kXCS, false);
Assert.equal(!!user2.kExtSync, false);
Assert.equal(!!user2.kExtKbHash, false);
Assert.equal(!!user2.ecosystemUserId, 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_OLD_SYNC).then(() => {
fxa._internal.getUserAccountData().then(user3 => {
// Now we should have keys
Assert.equal(fxa._internal.isUserEmailVerified(user3), true);
Assert.equal(!!user3.verified, true);
Assert.notEqual(null, user3.scopedKeys);
Assert.notEqual(null, user3.kSync);
Assert.notEqual(null, user3.kXCS);
Assert.notEqual(null, user3.kExtSync);
Assert.notEqual(null, user3.kExtKbHash);
Assert.notEqual(null, user3.ecosystemUserId);
Assert.equal(user3.keyFetchToken, undefined);
Assert.equal(user3.unwrapBKey, undefined);
run_next_test();
});
});
});
});
});
add_task(async function test_getKeyForScope_kb_migration() {
let fxa = new MockFxAccounts();
let user = getTestUser("eusebius");
user.verified = true;
// Set-up the deprecated set of keys.
user.kA = "e0245ab7f10e483470388e0a28f0a03379a3b417174fb2b42feab158b4ac2dbd";
user.kB = "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9";
await fxa.setSignedInUser(user);
await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
let newUser = await fxa._internal.getUserAccountData();
Assert.equal(newUser.kA, null);
Assert.equal(newUser.kB, null);
Assert.deepEqual(newUser.scopedKeys, {
"https://identity.mozilla.com/apps/oldsync": {
kid: "1234567890123-IqQv4onc7VcVE1kTQkyyOw",
k:
"DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
kty: "oct",
},
"https://identity.mozilla.com/ids/ecosystem_telemetry": {
kid: "1234567890-ruhbB-qilFS-9bwxlCe4Qw",
k: "niMTzlPWb01A2nkO4SkEAUalO7FiQ61yq69X6b8V08Y",
kty: "oct",
},
"sync:addon_storage": {
kid: "1234567890123-pBOR6B6JulbJr3BxKVOqIU4Cq_WAjFp4ApLn5NRVARE",
k:
"ut7VPrNYfXkA5gTopo2GCr-d4wtclV08TV26Y_Jv2IJlzYWSP26dzRau87gryIA5qJxZ7NnojeCadBjH2U-QyQ",
kty: "oct",
},
});
// These hex values were manually confirmed to be equivalent to the b64 values above.
Assert.equal(
newUser.kSync,
"0d6fe59791b05fa489e463ea25502e3143f6b7a903aa152e95cd9c6eddbac5b4" +
"dc68a19097ef65dbd147010ee45222444e66b8b3d7c8a441ebb7dd3dce015a9e"
);
Assert.equal(newUser.kXCS, "22a42fe289dced5715135913424cb23b");
Assert.equal(
newUser.kExtSync,
"baded53eb3587d7900e604e8a68d860abf9de30b5c955d3c4d5dba63f26fd882" +
"65cd85923f6e9dcd16aef3b82bc88039a89c59ecd9e88de09a7418c7d94f90c9"
);
Assert.equal(
newUser.kExtKbHash,
"a41391e81e89ba56c9af70712953aa214e02abf5808c5a780292e7e4d4550111"
);
Assert.equal(
newUser.ecosystemUserId,
"9e2313ce53d66f4d40da790ee129040146a53bb16243ad72abaf57e9bf15d3c6"
);
});
add_task(async function test_getKeyForScope_scopedKeys_migration() {
let fxa = new MockFxAccounts();
let user = getTestUser("eusebius");
user.verified = true;
// Set-up the keys in deprecated fields.
user.kSync = MOCK_ACCOUNT_KEYS.kSync;
user.kXCS = MOCK_ACCOUNT_KEYS.kXCS;
user.kExtSync = MOCK_ACCOUNT_KEYS.kExtSync;
user.kExtKbHash = MOCK_ACCOUNT_KEYS.kExtKbHash;
Assert.equal(user.ecosystemUserId, null);
Assert.equal(user.scopedKeys, null);
await fxa.setSignedInUser(user);
await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
let newUser = await fxa._internal.getUserAccountData();
Assert.equal(newUser.kA, null);
Assert.equal(newUser.kB, null);
// It should have correctly formatted the corresponding scoped keys,
// but failed to magic the ecosystem-telemetry key out of nowhere.
const expectedScopedKeys = { ...MOCK_ACCOUNT_KEYS.scopedKeys };
delete expectedScopedKeys[SCOPE_ECOSYSTEM_TELEMETRY];
Assert.deepEqual(newUser.scopedKeys, expectedScopedKeys);
// And left the existing key fields unchanged.
Assert.equal(newUser.kSync, user.kSync);
Assert.equal(newUser.kXCS, user.kXCS);
Assert.equal(newUser.kExtSync, user.kExtSync);
Assert.equal(newUser.kExtKbHash, user.kExtKbHash);
Assert.equal(user.ecosystemUserId, 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();
});
});
// XXX - the exception message here isn't ideal, but doesn't really matter...
await Assert.rejects(
fxa.keys.getKeyForScope(SCOPE_OLD_SYNC),
/A different user signed in/
);
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);
try {
await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
Assert.ok(false);
} catch (err) {
Assert.equal(err.code, 401);
Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
}
let user = await fxa._internal.getUserAccountData();
Assert.equal(user.email, yusuf.email);
Assert.equal(user.keyFetchToken, null);
await fxa._internal.abortExistingFlow();
});
// This is the exact same test vectors as
// https://github.com/mozilla/fxa-crypto-relier/blob/f94f441159029a645a474d4b6439c38308da0bb0/test/deriver/ScopedKeys.js#L58
add_task(async function test_getKeyForScope_oldsync() {
let fxa = new MockFxAccounts();
let client = fxa._internal.fxAccountsClient;
client.getScopedKeyData = () =>
Promise.resolve({
"https://identity.mozilla.com/apps/oldsync": {
identifier: "https://identity.mozilla.com/apps/oldsync",
keyRotationSecret:
"0000000000000000000000000000000000000000000000000000000000000000",
keyRotationTimestamp: 1510726317123,
},
});
let user = {
...getTestUser("eusebius"),
uid: "aeaa1725c7a24ff983c6295725d5fc9b",
kB: "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9",
verified: true,
};
await fxa.setSignedInUser(user);
const key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
Assert.deepEqual(key, {
scope: SCOPE_OLD_SYNC,
kid: "1510726317123-IqQv4onc7VcVE1kTQkyyOw",
k:
"DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
kty: "oct",
});
});
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,
kSync:
"0d6fe59791b05fa489e463ea25502e3143f6b7a903aa152e95cd9c6eddbac5b4dc68a19097ef65dbd147010ee45222444e66b8b3d7c8a441ebb7dd3dce015a9e",
kXCS: "22a42fe289dced5715135913424cb23b",
kExtSync:
"baded53eb3587d7900e604e8a68d860abf9de30b5c955d3c4d5dba63f26fd88265cd85923f6e9dcd16aef3b82bc88039a89c59ecd9e88de09a7418c7d94f90c9",
kExtKbHash:
"b776a89db29f22daedd154b44ff88397d0b210223fb956f5a749521dd8de8ddf",
};
await fxa.setSignedInUser(user);
await Assert.rejects(
fxa.keys.getKeyForScope(SCOPE_OLD_SYNC),
/The FxA server did not grant Firefox the `oldsync` scope/
);
});
// _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(user2 => {
fxa._internal.abortExistingFlow().then(run_next_test);
});
});
fxa
.setSignedInUser(user)
.then(user2 => {
return fxa.keys._fetchAndUnwrapAndDeriveKeys();
})
.catch(error => {
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() {
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_getAssertion_invalid_token() {
let fxa = new MockFxAccounts();
let client = fxa._internal.fxAccountsClient;
client.accountStatus = () => Promise.resolve(true);
client.sessionStatus = () => Promise.resolve(false);
let creds = {
sessionToken: "sessionToken",
verified: true,
email: "sonia@example.com",
...MOCK_ACCOUNT_KEYS,
};
await fxa.setSignedInUser(creds);
// we have what we still believe to be a valid session token, so we should
// consider that we have a local session.
Assert.ok(await fxa.hasLocalSession());
try {
let promiseAssertion = fxa._internal.getAssertion("audience.example.com");
fxa._internal._d_signCertificate.reject({
code: 401,
errno: ERRNO_INVALID_AUTH_TOKEN,
});
await promiseAssertion;
Assert.ok(false, "getAssertion should reject invalid session token");
} catch (err) {
Assert.equal(err.code, 401);
Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
}
let user = await fxa._internal.getUserAccountData();
Assert.equal(user.email, creds.email);
Assert.equal(user.sessionToken, null);
Assert.ok(!(await fxa.hasLocalSession()));
});
add_task(async function test_getAssertion() {
let fxa = new MockFxAccounts();
let creds = {
sessionToken: "sessionToken",
verified: true,
...MOCK_ACCOUNT_KEYS,
};
// By putting scopedKeys in "creds", we skip ahead to the "we're ready" stage.
await fxa.setSignedInUser(creds);
_("== ready to go\n");
// Start with a nice arbitrary but realistic date. Here we use a nice RFC
// 1123 date string like we would get from an HTTP header. Over the course of
// the test, we will update 'now', but leave 'start' where it is.
let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
let start = now;
fxa._internal._now_is = now;
let d = fxa._internal.getAssertion("audience.example.com");
// At this point, a thread has been spawned to generate the keys.
_("-- back from fxa.getAssertion\n");
fxa._internal._d_signCertificate.resolve("cert1");
let assertion = await d;
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 1);
Assert.equal(fxa._internal._getCertificateSigned_calls[0][0], "sessionToken");
Assert.notEqual(assertion, null);
_("ASSERTION: " + assertion + "\n");
let pieces = assertion.split("~");
Assert.equal(pieces[0], "cert1");
let userData = await fxa._internal.getUserAccountData();
let keyPair = userData.keyPair;
let cert = userData.cert;
Assert.notEqual(keyPair, undefined);
_(keyPair.validUntil + "\n");
let p2 = pieces[1].split(".");
let header = JSON.parse(atob(p2[0]));
_("HEADER: " + JSON.stringify(header) + "\n");
Assert.equal(header.alg, "DS128");
let payload = JSON.parse(atob(p2[1]));
_("PAYLOAD: " + JSON.stringify(payload) + "\n");
Assert.equal(payload.aud, "audience.example.com");
Assert.equal(keyPair.validUntil, start + KEY_LIFETIME);
Assert.equal(cert.validUntil, start + CERT_LIFETIME);
_("delta: " + Date.parse(payload.exp - start) + "\n");
let exp = Number(payload.exp);
Assert.equal(exp, now + ASSERTION_LIFETIME);
// Reset for next call.
fxa._internal._d_signCertificate = PromiseUtils.defer();
// Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for
// a new audience, should not provoke key generation or a signing request.
assertion = await fxa._internal.getAssertion("other.example.com");
// There were no additional calls - same number of getcert calls as before
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 1);
// Wait an hour; assertion use period expires, but not the certificate
now += ONE_HOUR_MS;
fxa._internal._now_is = now;
// This won't block on anything - will make an assertion, but not get a
// new certificate.
assertion = await fxa._internal.getAssertion("third.example.com");
// Test will time out if that failed (i.e., if that had to go get a new cert)
pieces = assertion.split("~");
Assert.equal(pieces[0], "cert1");
p2 = pieces[1].split(".");
header = JSON.parse(atob(p2[0]));
payload = JSON.parse(atob(p2[1]));
Assert.equal(payload.aud, "third.example.com");
// The keypair and cert should have the same validity as before, but the
// expiration time of the assertion should be different. We compare this to
// the initial start time, to which they are relative, not the current value
// of "now".
userData = await fxa._internal.getUserAccountData();
keyPair = userData.keyPair;
cert = userData.cert;
Assert.equal(keyPair.validUntil, start + KEY_LIFETIME);
Assert.equal(cert.validUntil, start + CERT_LIFETIME);
exp = Number(payload.exp);
Assert.equal(exp, now + ASSERTION_LIFETIME);
// Now we wait even longer, and expect both assertion and cert to expire. So
// we will have to get a new keypair and cert.
now += ONE_DAY_MS;
fxa._internal._now_is = now;
d = fxa._internal.getAssertion("fourth.example.com");
fxa._internal._d_signCertificate.resolve("cert2");
assertion = await d;
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 2);
Assert.equal(fxa._internal._getCertificateSigned_calls[1][0], "sessionToken");
pieces = assertion.split("~");
Assert.equal(pieces[0], "cert2");
p2 = pieces[1].split(".");
header = JSON.parse(atob(p2[0]));
payload = JSON.parse(atob(p2[1]));
Assert.equal(payload.aud, "fourth.example.com");
userData = await fxa._internal.getUserAccountData();
keyPair = userData.keyPair;
cert = userData.cert;
Assert.equal(keyPair.validUntil, now + KEY_LIFETIME);
Assert.equal(cert.validUntil, now + CERT_LIFETIME);
exp = Number(payload.exp);
Assert.equal(exp, now + ASSERTION_LIFETIME);
_("----- DONE ----\n");
});
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.setCharPref(
"identity.fxaccounts.remote.oauth.uri",
"https://example.com/v1"
);
add_test(function test_getOAuthToken() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let oauthTokenCalled = false;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = () => {
oauthTokenCalled = true;
return Promise.resolve({ access_token: "token" });
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile" }).then(result => {
Assert.ok(oauthTokenCalled);
Assert.equal(result, "token");
run_next_test();
});
});
});
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, "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_getOAuthTokenScoped() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let oauthTokenCalled = false;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = (_assertion, scopeString) => {
equal(scopeString, "bar foo"); // scopes are sorted locally before request.
oauthTokenCalled = true;
return Promise.resolve({ access_token: "token" });
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: ["foo", "bar"] }).then(result => {
Assert.ok(oauthTokenCalled);
Assert.equal(result, "token");
run_next_test();
});
});
});
add_task(async function test_getOAuthTokenCached() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let numOauthTokenCalls = 0;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = () => {
numOauthTokenCalls += 1;
return Promise.resolve({ access_token: "token" });
};
await fxa.setSignedInUser(alice);
let result = await fxa.getOAuthToken({
scope: "profile",
service: "test-service",
});
Assert.equal(numOauthTokenCalls, 1);
Assert.equal(result, "token");
// 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, "token");
// 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, "token");
});
add_task(async function test_getOAuthTokenCachedScopeNormalization() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let numOAuthTokenCalls = 0;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = () => {
numOAuthTokenCalls += 1;
return Promise.resolve({ access_token: "token" });
};
await fxa.setSignedInUser(alice);
let result = await fxa.getOAuthToken({
scope: ["foo", "bar"],
service: "test-service",
});
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
// requesting it again with the scope array in a different order not re-fetch the token.
result = await fxa.getOAuthToken({
scope: ["bar", "foo"],
service: "test-service",
});
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
// requesting it again with the scope array in different case not re-fetch the token.
result = await fxa.getOAuthToken({
scope: ["Bar", "Foo"],
service: "test-service",
});
Assert.equal(numOAuthTokenCalls, 1);
Assert.equal(result, "token");
// 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, "token");
});
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.getCharPref(
"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.setCharPref(
"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;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
client.getTokenFromAssertion = () => {
return Promise.reject("boom");
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile" }).catch(err => {
run_next_test();
});
});
});
add_task(async function test_getOAuthToken_authErrorRefreshesCertificate() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
fxa._internal._d_signCertificate.resolve("cert1");
let client = fxa._internal.fxAccountsOAuthGrantClient;
let numTokenCalls = 0;
client.getTokenFromAssertion = () => {
numTokenCalls++;
// First time around, reject with a 401.
if (numTokenCalls == 1) {
return Promise.reject({
code: 401,
errno: 1104,
});
}
// Second time around, succeed.
if (numTokenCalls == 2) {
return Promise.resolve({ access_token: "token" });
}
throw new Error("too many token calls");
};
await fxa.setSignedInUser(alice);
let result = await fxa.getOAuthToken({ scope: "profile" });
Assert.equal(result, "token");
Assert.equal(numTokenCalls, 2);
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 2);
});
add_task(async function test_listAttachedOAuthClients() {
const ONE_HOUR = 60 * 60 * 1000;
const ONE_DAY = 24 * ONE_HOUR;
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let client = fxa._internal.fxAccountsClient;
client.attachedClients = async () => {
return [
// This entry was previously filtered but no longer is!
{
clientId: "a2270f727f45f648",
deviceId: "deadbeef",
sessionTokenId: null,
name: "Firefox Preview (no session token)",
scope: ["profile", "https://identity.mozilla.com/apps/oldsync"],
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",
},
];
};
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_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_deriveKeys() {
let account = await MakeFxAccounts();
let kBhex =
"fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
let kB = CommonUtils.hexToBytes(kBhex);
const uid = "1ad7f502-4cc7-4ec1-a209-071fd2fae348";
const { kSync, kXCS, kExtSync, kExtKbHash } = await account.keys._deriveKeys(
uid,
kB
);
Assert.equal(
kSync,
"ad501a50561be52b008878b2e0d8a73357778a712255f7722f497b5d4df14b05" +
"dc06afb836e1521e882f521eb34691d172337accdbf6e2a5b968b05a7bbb9885"
);
Assert.equal(kXCS, "6ae94683571c7a7c54dab4700aa3995f");
Assert.equal(
kExtSync,
"f5ccd9cfdefd9b1ac4d02c56964f59239d8dfa1ca326e63696982765c1352cdc" +
"5d78a5a9c633a6d25edfea0a6c221a3480332a49fd866f311c2e3508ddd07395"
);
Assert.equal(
kExtKbHash,
"6192f1cc7dce95334455ba135fa1d8fca8f70e8f594ae318528de06f24ed0273"
);
});
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,
};
}
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;
}