summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
commitda4c7e7ed675c3bf405668739c3012d140856109 (patch)
treecdd868dba063fecba609a1d819de271f0d51b23e /services/fxaccounts
parentAdding upstream version 125.0.3. (diff)
downloadfirefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz
firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'services/fxaccounts')
-rw-r--r--services/fxaccounts/FxAccounts.sys.mjs27
-rw-r--r--services/fxaccounts/FxAccountsKeys.sys.mjs61
-rw-r--r--services/fxaccounts/FxAccountsOAuth.sys.mjs14
-rw-r--r--services/fxaccounts/FxAccountsWebChannel.sys.mjs34
-rw-r--r--services/fxaccounts/tests/xpcshell/test_accounts.js4
-rw-r--r--services/fxaccounts/tests/xpcshell/test_keys.js122
-rw-r--r--services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js2
-rw-r--r--services/fxaccounts/tests/xpcshell/test_oauth_flow.js82
8 files changed, 314 insertions, 32 deletions
diff --git a/services/fxaccounts/FxAccounts.sys.mjs b/services/fxaccounts/FxAccounts.sys.mjs
index 790a6195f8..e258c99c2d 100644
--- a/services/fxaccounts/FxAccounts.sys.mjs
+++ b/services/fxaccounts/FxAccounts.sys.mjs
@@ -65,6 +65,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
true
);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "oauthEnabled",
+ "identity.fxaccounts.oauth.enabled",
+ true
+);
+
export const ERROR_INVALID_ACCOUNT_STATE = "ERROR_INVALID_ACCOUNT_STATE";
// An AccountState object holds all state related to one specific account.
@@ -550,7 +557,7 @@ export class FxAccounts {
await this.signOut();
return null;
}
- if (!this._internal.isUserEmailVerified(data)) {
+ if (!this._internal.isUserEmailVerified(data) && !lazy.oauthEnabled) {
// If the email is not verified, start polling for verification,
// but return null right away. We don't want to return a promise
// that might not be fulfilled for a long time.
@@ -865,7 +872,7 @@ FxAccountsInternal.prototype = {
_oauth: null,
get oauth() {
if (!this._oauth) {
- this._oauth = new lazy.FxAccountsOAuth(this.fxAccountsClient);
+ this._oauth = new lazy.FxAccountsOAuth(this.fxAccountsClient, this.keys);
}
return this._oauth;
},
@@ -995,16 +1002,17 @@ FxAccountsInternal.prototype = {
);
}
await this.abortExistingFlow();
- let currentAccountState = (this.currentAccountState = this.newAccountState(
- Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object.
- ));
+ const currentAccountState = (this.currentAccountState =
+ this.newAccountState(
+ Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object.
+ ));
// This promise waits for storage, but not for verification.
// We're telling the caller that this is durable now (although is that
// really something we should commit to? Why not let the write happen in
// the background? Already does for updateAccountData ;)
await currentAccountState.promiseInitialized;
// Starting point for polling if new user
- if (!this.isUserEmailVerified(credentials)) {
+ if (!this.isUserEmailVerified(credentials) && !lazy.oauthEnabled) {
this.startVerifiedCheck(credentials);
}
await this.notifyObservers(ONLOGIN_NOTIFICATION);
@@ -1478,13 +1486,14 @@ FxAccountsInternal.prototype = {
/** Sets the user to be verified in the account state,
* This prevents any polling for the user's verification state from the FxA server
**/
- setUserVerified() {
- return this.withCurrentAccountState(async currentState => {
+ async setUserVerified() {
+ await this.withCurrentAccountState(async currentState => {
const userData = await currentState.getUserAccountData();
if (!userData.verified) {
- await currentState.updateAccountData({ verified: true });
+ await currentState.updateUserAccountData({ verified: true });
}
});
+ await this.notifyObservers(ONVERIFIED_NOTIFICATION);
},
async _getVerifiedAccountOrReject() {
diff --git a/services/fxaccounts/FxAccountsKeys.sys.mjs b/services/fxaccounts/FxAccountsKeys.sys.mjs
index ad19df31be..9717f010c7 100644
--- a/services/fxaccounts/FxAccountsKeys.sys.mjs
+++ b/services/fxaccounts/FxAccountsKeys.sys.mjs
@@ -132,6 +132,67 @@ export class FxAccountsKeys {
}
/**
+ * Validates if the given scoped keys are valid keys
+ *
+ * @param { Object } scopedKeys: The scopedKeys bundle
+ *
+ * @return { Boolean }: true if the scopedKeys bundle is valid, false otherwise
+ */
+ validScopedKeys(scopedKeys) {
+ for (const expectedScope of Object.keys(scopedKeys)) {
+ const key = scopedKeys[expectedScope];
+ if (
+ !key.hasOwnProperty("scope") ||
+ !key.hasOwnProperty("kid") ||
+ !key.hasOwnProperty("kty") ||
+ !key.hasOwnProperty("k")
+ ) {
+ return false;
+ }
+ const { scope, kid, kty, k } = key;
+ if (scope != expectedScope || kty != "oct") {
+ return false;
+ }
+ // We verify the format of the key id is `timestamp-fingerprint`
+ if (!kid.includes("-")) {
+ return false;
+ }
+ const [keyRotationTimestamp, fingerprint] = kid.split("-");
+ // We then verify that the timestamp is a valid timestamp
+ const keyRotationTimestampNum = Number(keyRotationTimestamp);
+ // If the value we got back is falsy it's not a valid timestamp
+ // note that we treat a 0 timestamp as invalid
+ if (!keyRotationTimestampNum) {
+ return false;
+ }
+ // For extra safety, we validate that the timestamp can be converted into a valid
+ // Date object
+ const date = new Date(keyRotationTimestampNum);
+ if (isNaN(date.getTime()) || date.getTime() <= 0) {
+ return false;
+ }
+
+ // Finally, we validate that the fingerprint and the key itself are valid base64 values
+ // Note that we can't verify the fingerprint is correct here because we don't have kb
+ const validB64String = b64String => {
+ let decoded;
+ try {
+ decoded = ChromeUtils.base64URLDecode(b64String, {
+ padding: "reject",
+ });
+ } catch (e) {
+ return false;
+ }
+ return !!decoded;
+ };
+ if (!validB64String(fingerprint) || !validB64String(k)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
* Format a JWK kid as hex rather than base64.
*
* This is a backwards-compatibility helper for code that needs a raw key fingerprint
diff --git a/services/fxaccounts/FxAccountsOAuth.sys.mjs b/services/fxaccounts/FxAccountsOAuth.sys.mjs
index 1935decff2..e8f186d1f7 100644
--- a/services/fxaccounts/FxAccountsOAuth.sys.mjs
+++ b/services/fxaccounts/FxAccountsOAuth.sys.mjs
@@ -22,6 +22,7 @@ export const ERROR_INVALID_STATE = "INVALID_STATE";
export const ERROR_SYNC_SCOPE_NOT_GRANTED = "ERROR_SYNC_SCOPE_NOT_GRANTED";
export const ERROR_NO_KEYS_JWE = "ERROR_NO_KEYS_JWE";
export const ERROR_OAUTH_FLOW_ABANDONED = "ERROR_OAUTH_FLOW_ABANDONED";
+export const ERROR_INVALID_SCOPED_KEYS = "ERROR_INVALID_SCOPED_KEYS";
/**
* Handles all logic and state related to initializing, and completing OAuth flows
@@ -32,14 +33,16 @@ export const ERROR_OAUTH_FLOW_ABANDONED = "ERROR_OAUTH_FLOW_ABANDONED";
export class FxAccountsOAuth {
#flow;
#fxaClient;
+ #fxaKeys;
/**
* Creates a new FxAccountsOAuth
*
* @param { Object } fxaClient: The fxa client used to send http request to the oauth server
*/
- constructor(fxaClient) {
+ constructor(fxaClient, fxaKeys) {
this.#flow = {};
this.#fxaClient = fxaClient;
+ this.#fxaKeys = fxaKeys;
}
/**
@@ -131,8 +134,10 @@ export class FxAccountsOAuth {
// Generate a 43 byte code verifier for PKCE, in accordance with
// https://datatracker.ietf.org/doc/html/rfc7636#section-7.1 which recommends a
// 43-octet URL safe string
- const codeVerifier = new Uint8Array(43);
+ // The byte array is 32 bytes
+ const codeVerifier = new Uint8Array(32);
crypto.getRandomValues(codeVerifier);
+ // When base64 encoded, it is 43 bytes
const codeVerifierB64 = ChromeUtils.base64URLEncode(codeVerifier, {
pad: false,
});
@@ -147,7 +152,7 @@ export class FxAccountsOAuth {
// Generate a public, private key pair to be used during the oauth flow
// to encrypt scoped-keys as they roundtrip through the auth server
const ECDH_KEY = { name: "ECDH", namedCurve: "P-256" };
- const key = await crypto.subtle.generateKey(ECDH_KEY, true, ["deriveKey"]);
+ const key = await crypto.subtle.generateKey(ECDH_KEY, false, ["deriveKey"]);
const publicKey = await crypto.subtle.exportKey("jwk", key.publicKey);
const privateKey = key.privateKey;
@@ -205,6 +210,9 @@ export class FxAccountsOAuth {
scopedKeys = JSON.parse(
new TextDecoder().decode(await lazy.jwcrypto.decryptJWE(keys_jwe, key))
);
+ if (!this.#fxaKeys.validScopedKeys(scopedKeys)) {
+ throw new Error(ERROR_INVALID_SCOPED_KEYS);
+ }
}
// We make sure no other flow snuck in, and completed before we did
diff --git a/services/fxaccounts/FxAccountsWebChannel.sys.mjs b/services/fxaccounts/FxAccountsWebChannel.sys.mjs
index f8d7a3362d..14ba222a9e 100644
--- a/services/fxaccounts/FxAccountsWebChannel.sys.mjs
+++ b/services/fxaccounts/FxAccountsWebChannel.sys.mjs
@@ -80,6 +80,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
val => Services.io.newURI(val)
);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "oauthEnabled",
+ "identity.fxaccounts.oauth.enabled",
+ false
+);
+
// These engines were added years after Sync had been introduced, they need
// special handling since they are system add-ons and are un-available on
// older versions of Firefox.
@@ -487,16 +494,19 @@ FxAccountsWebChannelHelpers.prototype = {
"webchannel"
);
- const xps = await this._initializeSync();
- await this._fxAccounts._internal.setSignedInUser(accountData);
-
- if (requestedServices) {
- // User has enabled Sync.
- if (requestedServices.sync) {
- const { offeredEngines, declinedEngines } = requestedServices.sync;
- this._setEnabledEngines(offeredEngines, declinedEngines);
- log.debug("Webchannel is enabling sync");
- await xps.Weave.Service.configure();
+ if (lazy.oauthEnabled) {
+ await this._fxAccounts._internal.setSignedInUser(accountData);
+ } else {
+ const xps = await this._initializeSync();
+ await this._fxAccounts._internal.setSignedInUser(accountData);
+ if (requestedServices) {
+ // User has enabled Sync.
+ if (requestedServices.sync) {
+ const { offeredEngines, declinedEngines } = requestedServices.sync;
+ this._setEnabledEngines(offeredEngines, declinedEngines);
+ log.debug("Webchannel is enabling sync");
+ await xps.Weave.Service.configure();
+ }
}
}
},
@@ -628,9 +638,7 @@ FxAccountsWebChannelHelpers.prototype = {
};
},
_getCapabilities() {
- if (
- Services.prefs.getBoolPref("identity.fxaccounts.oauth.enabled", false)
- ) {
+ if (lazy.oauthEnabled) {
return {
multiService: true,
pairing: lazy.pairingEnabled,
diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js
index 239adb206f..1dcbb2d2f2 100644
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -44,7 +44,7 @@ initTestLogging("Trace");
var log = Log.repository.getLogger("Services.FxAccounts.test");
log.level = Log.Level.Debug;
-// See verbose logging from FxAccounts.jsm and jwcrypto.jsm.
+// 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");
@@ -217,7 +217,7 @@ function MockFxAccounts() {
},
});
// and for convenience so we don't have to touch as many lines in this test
- // when we refactored FxAccounts.jsm :)
+ // when we refactored FxAccounts.sys.mjs :)
result.setSignedInUser = function (creds) {
return result._internal.setSignedInUser(creds);
};
diff --git a/services/fxaccounts/tests/xpcshell/test_keys.js b/services/fxaccounts/tests/xpcshell/test_keys.js
index 6e650a1609..9a25ca90f3 100644
--- a/services/fxaccounts/tests/xpcshell/test_keys.js
+++ b/services/fxaccounts/tests/xpcshell/test_keys.js
@@ -99,6 +99,128 @@ add_task(async function test_derive_multiple_keys_at_once() {
});
});
+add_task(function test_check_valid_scoped_keys() {
+ const keys = new FxAccountsKeys(null);
+ add_task(function test_missing_key_data() {
+ const scopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), false);
+ });
+ add_task(function test_unexpected_scope() {
+ const scopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "UnexpectedScope",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), false);
+ });
+ add_task(function test_not_oct_key() {
+ const scopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ // Should be "oct"!
+ kty: "EC",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), false);
+ });
+ add_task(function test_invalid_kid_not_timestamp() {
+ const scopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ // Does not have the timestamp!
+ kid: "IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), false);
+ });
+ add_task(function test_invalid_kid_not_valid_timestamp() {
+ const scopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ // foo is not a valid timestamp!
+ kid: "foo-IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), false);
+ });
+ add_task(function test_invalid_kid_not_b64_fingerprint() {
+ const scopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ // fingerprint not a valid base64 encoded string.
+ kid: "1510726318123-notvalidb64][",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), false);
+ });
+ add_task(function test_invalid_k_not_base64() {
+ const scopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "notavalidb64[]",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), false);
+ });
+
+ add_task(function test_multiple_scoped_keys_one_invalid() {
+ const scopedKeys = {
+ // Valid
+ "https://identity.mozilla.com/apps/otherscope": {
+ kty: "oct",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/otherscope",
+ },
+ // Invalid
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "notavalidb64[]",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), false);
+ });
+
+ add_task(function test_valid_scopedkeys() {
+ const scopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ "https://identity.mozilla.com/apps/otherscope": {
+ kty: "oct",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/otherscope",
+ },
+ };
+ Assert.equal(keys.validScopedKeys(scopedKeys), true);
+ });
+});
+
add_task(async function test_rejects_bad_scoped_key_data() {
const keys = new FxAccountsKeys(null);
const uid = "aeaa1725c7a24ff983c6295725d5fc9b";
diff --git a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
index 5b80035418..1fdfecca61 100644
--- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
@@ -5,7 +5,7 @@
// Tests for FxAccounts, storage and the master password.
-// See verbose logging from FxAccounts.jsm
+// See verbose logging from FxAccounts.sys.mjs
Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace");
const { FxAccounts } = ChromeUtils.importESModule(
diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_flow.js b/services/fxaccounts/tests/xpcshell/test_oauth_flow.js
index ef5102ae17..a5f53b6100 100644
--- a/services/fxaccounts/tests/xpcshell/test_oauth_flow.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_flow.js
@@ -8,6 +8,7 @@
const {
FxAccountsOAuth,
ERROR_INVALID_SCOPES,
+ ERROR_INVALID_SCOPED_KEYS,
ERROR_INVALID_STATE,
ERROR_SYNC_SCOPE_NOT_GRANTED,
ERROR_NO_KEYS_JWE,
@@ -22,6 +23,7 @@ const { SCOPE_PROFILE, FX_OAUTH_CLIENT_ID } = ChromeUtils.importESModule(
ChromeUtils.defineESModuleGetters(this, {
jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs",
+ FxAccountsKeys: "resource://gre/modules/FxAccountsKeys.sys.mjs",
});
initTestLogging("Trace");
@@ -159,17 +161,21 @@ add_task(function test_complete_oauth_flow() {
const oauthCode = "fake oauth code";
const sessionToken = "01abcef12";
const plainTextScopedKeys = {
- kid: "fake key id",
- k: "fake key",
- kty: "oct",
+ "https://identity.mozilla.com/apps/oldsync": {
+ kty: "oct",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
};
const fakeAccessToken = "fake access token";
const fakeRefreshToken = "fake refresh token";
// Then, we initialize a fake http client, we'll add our fake oauthToken call
// once we have started the oauth flow (so we have the public keys!)
const fxaClient = {};
+ const fxaKeys = new FxAccountsKeys(null);
// Then, we initialize our oauth object with the given client and begin a new flow
- const oauth = new FxAccountsOAuth(fxaClient);
+ const oauth = new FxAccountsOAuth(fxaClient, fxaKeys);
const queryParams = await oauth.beginOAuthFlow(scopes);
// Now that we have the public keys in `keys_jwk`, we use it to generate a JWE
// representing our scoped keys
@@ -271,4 +277,72 @@ add_task(function test_complete_oauth_flow() {
// Finally, we verify that all stored flows were cleared
Assert.equal(oauth.numOfFlows(), 0);
});
+ add_task(async function test_complete_oauth_invalid_scoped_keys() {
+ // First, we initialize some fake values we would typically get
+ // from outside our system
+ const scopes = [SCOPE_PROFILE, SCOPE_OLD_SYNC];
+ const oauthCode = "fake oauth code";
+ const sessionToken = "01abcef12";
+ const invalidScopedKeys = {
+ "https://identity.mozilla.com/apps/oldsync": {
+ // ====== This is an invalid key type! Should be "oct", so we will raise an error once we realize
+ kty: "EC",
+ kid: "1510726318123-IqQv4onc7VcVE1kTQkyyOw",
+ k: "DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
+ scope: "https://identity.mozilla.com/apps/oldsync",
+ },
+ };
+ const fakeAccessToken = "fake access token";
+ const fakeRefreshToken = "fake refresh token";
+ // Then, we initialize a fake http client, we'll add our fake oauthToken call
+ // once we have started the oauth flow (so we have the public keys!)
+ const fxaClient = {};
+ const fxaKeys = new FxAccountsKeys(null);
+ // Then, we initialize our oauth object with the given client and begin a new flow
+ const oauth = new FxAccountsOAuth(fxaClient, fxaKeys);
+ const queryParams = await oauth.beginOAuthFlow(scopes);
+ // Now that we have the public keys in `keys_jwk`, we use it to generate a JWE
+ // representing our scoped keys
+ const keysJwk = queryParams.keys_jwk;
+ const decodedKeysJwk = JSON.parse(
+ new TextDecoder().decode(
+ ChromeUtils.base64URLDecode(keysJwk, { padding: "reject" })
+ )
+ );
+ delete decodedKeysJwk.key_ops;
+ const jwe = await jwcrypto.generateJWE(
+ decodedKeysJwk,
+ new TextEncoder().encode(JSON.stringify(invalidScopedKeys))
+ );
+ // We also grab the stored PKCE verifier that the oauth object stored internally
+ // to verify that we correctly send it as a part of our HTTP request
+ const storedVerifier = oauth.getFlow(queryParams.state).verifier;
+
+ // Now we initialize our mock of the HTTP request, it verifies we passed in all the correct
+ // parameters and returns what we'd expect a healthy HTTP Response would look like
+ fxaClient.oauthToken = (sessionTokenHex, code, verifier, clientId) => {
+ Assert.equal(sessionTokenHex, sessionToken);
+ Assert.equal(code, oauthCode);
+ Assert.equal(verifier, storedVerifier);
+ Assert.equal(clientId, queryParams.client_id);
+ const response = {
+ access_token: fakeAccessToken,
+ refresh_token: fakeRefreshToken,
+ scope: scopes.join(" "),
+ keys_jwe: jwe,
+ };
+ return Promise.resolve(response);
+ };
+
+ // Then, we call the completeOAuthFlow function, and get back our access token,
+ // refresh token and scopedKeys
+ try {
+ await oauth.completeOAuthFlow(sessionToken, oauthCode, queryParams.state);
+ Assert.fail(
+ "Should have thrown an error because the scoped keys are not valid"
+ );
+ } catch (err) {
+ Assert.equal(err.message, ERROR_INVALID_SCOPED_KEYS);
+ }
+ });
});