commit 393d6e23e9653e1e48310ad31c53dd6b9b4896c2
parent 6948c472ebaa49506d5c6dacdb3f81fec217ce96
Author: Sammy Khamis <skhamis@mozilla.com>
Date: Fri, 21 Nov 2025 22:50:58 +0000
Bug 1997219: Allow sync to be tolerant for missing Sync Keys r=markh
Differential Revision: https://phabricator.services.mozilla.com/D270628
Diffstat:
5 files changed, 66 insertions(+), 19 deletions(-)
diff --git a/services/fxaccounts/FxAccounts.sys.mjs b/services/fxaccounts/FxAccounts.sys.mjs
@@ -587,15 +587,6 @@ export class FxAccounts {
await this.signOut();
return null;
}
- // XXX - these comments reflect old baggage, we should clean this up.
- // data.verified is the sessionToken status. oauth cares only about whether it has the keys.
- // (Note that this never forces `.verified` to `true` even if we *do* have the keys, which
- // seems slightly odd)
- // Note that is the primary-password is locked we can't get the scopedKeys even if they exist, so
- // we don't want to pretend the user is unverified in that case.
- if (Services.logins.isLoggedIn && !data.scopedKeys) {
- data.verified = false;
- }
delete data.scopedKeys;
let profileData = null;
diff --git a/services/fxaccounts/FxAccountsOAuth.sys.mjs b/services/fxaccounts/FxAccountsOAuth.sys.mjs
@@ -200,13 +200,12 @@ export class FxAccountsOAuth {
verifier,
OAUTH_CLIENT_ID
);
- if (
- requestedScopes.includes(SCOPE_APP_SYNC) &&
- !scope.includes(SCOPE_APP_SYNC)
- ) {
+ const requestedSync = requestedScopes.includes(SCOPE_APP_SYNC);
+ const grantedSync = scope.includes(SCOPE_APP_SYNC);
+ if (requestedSync && !grantedSync) {
throw new Error(ERROR_SYNC_SCOPE_NOT_GRANTED);
}
- if (scope.includes(SCOPE_APP_SYNC) && !keys_jwe) {
+ if (grantedSync && !keys_jwe) {
throw new Error(ERROR_NO_KEYS_JWE);
}
let scopedKeys;
diff --git a/services/fxaccounts/FxAccountsWebChannel.sys.mjs b/services/fxaccounts/FxAccountsWebChannel.sys.mjs
@@ -697,8 +697,14 @@ FxAccountsWebChannelHelpers.prototype = {
// Remember the account for future merge warnings etc.
this.setPreviousAccountNameHashPref(email);
- // Then, we persist the sync keys
- await this._fxAccounts._internal.setScopedKeys(scopedKeys);
+ if (!scopedKeys) {
+ log.info(
+ "OAuth login completed without scoped keys; skipping Sync key storage"
+ );
+ } else {
+ // Then, we persist the sync keys
+ await this._fxAccounts._internal.setScopedKeys(scopedKeys);
+ }
try {
let parsedRequestedServices;
@@ -846,6 +852,9 @@ FxAccountsWebChannelHelpers.prototype = {
multiService: true,
pairing: lazy.pairingEnabled,
choose_what_to_sync: true,
+ // This capability is for telling FxA that the current build can accept
+ // accounts without passwords/sync keys (third-party auth)
+ keys_optional: true,
engines,
};
},
diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -412,9 +412,8 @@ add_test(function test_getKeyForScope() {
add_task(async function test_oauth_verification() {
let fxa = new MockFxAccounts();
- let user = getTestUser("eusebius");
- user.verified = true;
-
+ let user = getTestUser("foo");
+ user.verified = false;
await fxa.setSignedInUser(user);
let fetched = await fxa.getSignedInUser();
Assert.ok(!fetched.verified);
@@ -424,6 +423,15 @@ add_task(async function test_oauth_verification() {
});
fetched = await fxa.getSignedInUser();
+ Assert.ok(!fetched.verified); // keys alone don’t flip verification
+
+ // Simulate the follow-up login message that marks the account verified.
+ await fxa._internal.updateUserAccountData({
+ uid: user.uid,
+ verified: true,
+ });
+
+ fetched = await fxa.getSignedInUser();
Assert.ok(fetched.verified);
});
diff --git a/services/fxaccounts/tests/xpcshell/test_web_channel.js b/services/fxaccounts/tests/xpcshell/test_web_channel.js
@@ -773,6 +773,46 @@ add_task(async function test_helpers_persist_requested_services() {
});
});
+add_task(async function test_helpers_oauth_login_defers_sync_without_keys() {
+ const accountState = {
+ uid: "uid123",
+ sessionToken: "session-token",
+ email: "user@example.com",
+ requestedServices: "",
+ };
+ const destroyOAuthToken = sinon.stub().resolves();
+ const completeOAuthFlow = sinon
+ .stub()
+ .resolves({ scopedKeys: null, refreshToken: "refresh-token" });
+ const setScopedKeys = sinon.spy();
+ const setUserVerified = sinon.spy();
+ const updateUserAccountData = sinon.stub().resolves();
+
+ const helpers = new FxAccountsWebChannelHelpers({
+ fxAccounts: {
+ _internal: {
+ async getUserAccountData() {
+ return accountState;
+ },
+ completeOAuthFlow,
+ destroyOAuthToken,
+ setScopedKeys,
+ updateUserAccountData,
+ setUserVerified,
+ },
+ },
+ });
+
+ await helpers.oauthLogin({ code: "code", state: "state" });
+
+ Assert.ok(setScopedKeys.notCalled);
+ Assert.ok(updateUserAccountData.calledOnce);
+ Assert.deepEqual(
+ JSON.parse(updateUserAccountData.firstCall.args[0].requestedServices),
+ null
+ );
+});
+
add_test(function test_helpers_open_sync_preferences() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {},