tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mservices/fxaccounts/FxAccounts.sys.mjs | 9---------
Mservices/fxaccounts/FxAccountsOAuth.sys.mjs | 9++++-----
Mservices/fxaccounts/FxAccountsWebChannel.sys.mjs | 13+++++++++++--
Mservices/fxaccounts/tests/xpcshell/test_accounts.js | 14+++++++++++---
Mservices/fxaccounts/tests/xpcshell/test_web_channel.js | 40++++++++++++++++++++++++++++++++++++++++
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: {},