Bug 1928165 - Update desktop oauth flows to get the service info from the login message. r=lina,skhamis

Differential Revision: https://phabricator.services.mozilla.com/D227388
This commit is contained in:
Mark Hammond 2024-10-31 20:25:54 +00:00
Родитель 523d20562d
Коммит 0cb88f76d6
4 изменённых файлов: 206 добавлений и 115 удалений

Просмотреть файл

@ -545,7 +545,7 @@ export class FxAccounts {
* or null if no user is signed in. This function never fails except
* in pathological cases (eg, file-system errors, etc)
*/
getSignedInUser() {
getSignedInUser(addnFields = []) {
// Note we don't return the session token, but use it to see if we
// should fetch the profile. Ditto scopedKeys re verified.
const ACCT_DATA_FIELDS = [
@ -557,7 +557,9 @@ export class FxAccounts {
];
const PROFILE_FIELDS = ["displayName", "avatar", "avatarDefault"];
return this._withCurrentAccountState(async currentState => {
const data = await currentState.getUserAccountData(ACCT_DATA_FIELDS);
const data = await currentState.getUserAccountData(
ACCT_DATA_FIELDS.concat(addnFields)
);
if (!data) {
return null;
}

Просмотреть файл

@ -289,6 +289,7 @@ export let FXA_PWDMGR_PLAINTEXT_FIELDS = new Set([
"profileCache",
"encryptedSendTabKeys",
"encryptedCloseTabKeys",
"requestedServices", // info about services which should be configured, used during oauth flows.
]);
// Fields we store in secure storage if it exists.

Просмотреть файл

@ -92,26 +92,20 @@ ChromeUtils.defineLazyGetter(lazy, "l10n", function () {
return new Localization(["browser/sync.ftl"], true);
});
// 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.
// These are only used in the non-oauth flows.
const EXTRA_ENGINES = ["addresses", "creditcards"];
// These engines will be displayed to the user to pick which they would like to
// use.
const CHOOSE_WHAT_TO_SYNC_ALWAYS_AVAILABLE = [
"addons",
"bookmarks",
"creditcards",
"history",
"passwords",
"prefs",
"tabs",
];
// Engines which we need to inspect a pref to see if they are available in the oauth flows.
const CHOOSE_WHAT_TO_SYNC_OPTIONALLY_AVAILABLE = ["addresses"];
// Engines which we need to inspect a pref to see if they are available, and
// possibly have their default preference value to disabled.
const CHOOSE_WHAT_TO_SYNC_OPTIONALLY_AVAILABLE = ["addresses", "creditcards"];
/**
* A helper function that extracts the message and stack from an error object.
@ -324,7 +318,6 @@ FxAccountsWebChannel.prototype = {
break;
case COMMAND_FXA_STATUS:
log.debug("fxa_status received");
const service = data && data.service;
const isPairing = data && data.isPairing;
const context = data && data.context;
@ -478,12 +471,14 @@ FxAccountsWebChannelHelpers.prototype = {
_setEnabledEngines(offeredEngines, declinedEngines) {
if (offeredEngines && declinedEngines) {
EXTRA_ENGINES.forEach(engine => {
log.debug("Received offered engines", offeredEngines);
CHOOSE_WHAT_TO_SYNC_OPTIONALLY_AVAILABLE.forEach(engine => {
if (
offeredEngines.includes(engine) &&
!declinedEngines.includes(engine)
) {
// These extra engines are disabled by default.
log.debug(`Enabling optional engine '${engine}'`);
Services.prefs.setBoolPref(`services.sync.engine.${engine}`, true);
}
});
@ -492,83 +487,124 @@ FxAccountsWebChannelHelpers.prototype = {
declinedEngines.forEach(engine => {
Services.prefs.setBoolPref(`services.sync.engine.${engine}`, false);
});
} else {
log.debug("Did not receive any engine selection information");
}
},
/**
* stores sync login info it in the fxaccounts service
/** Internal function used to configure the requested services.
*
* The "services" param is an object as received from the FxA server.
*/
async _enableRequestedServices(requestedServices) {
if (!requestedServices) {
log.warn(
"fxa login completed but we don't have a record of which services were enabled."
);
return;
}
log.debug(`services requested are ${Object.keys(requestedServices)}`);
if (requestedServices.sync) {
const xps = await this._initializeSync();
const { offeredEngines, declinedEngines } = requestedServices.sync;
this._setEnabledEngines(offeredEngines, declinedEngines);
log.debug("Webchannel is enabling sync");
await xps.Weave.Service.configure();
}
},
/**
* The login message is sent when the user user has initially logged in but may not be fully connected.
* * In the non-oauth flows, if the user is verified, then the browser itself is able to transition the
* user to fully connected.
* * In the oauth flows, we will need an `oauth_login` message with our scoped keys to be fully connected.
* @param accountData the user's account data and credentials
*/
async login(accountData) {
const signedInUser = await this._fxAccounts.getSignedInUser();
// This is delicate for oauth flows and edge-cases. Consider (a) user logs in but does not verify,
// (b) browser restarts, (c) user select "finish setup", at which point they are again prompted for their password.
// In that scenario, we've been sent this `login` message *both* at (a) and at (c).
// Importantly, the message from (a) is the one that actually has the service information we care about
// (eg, the sync engine selections) - (c) *will* have `services.sync` but it will be an empty object.
// This means we need to take care to not lose the services from (a) when processing (c).
const signedInUser = await this._fxAccounts.getSignedInUser([
"requestedServices",
]);
let existingServices;
if (signedInUser) {
await this._disconnect();
if (signedInUser.uid != accountData.uid) {
log.warn(
"the webchannel found a different user signed in - signing them out."
);
await this._disconnect();
} else {
existingServices = JSON.parse(signedInUser.requestedServices);
log.debug(
"Webchannel is updating the info for an already logged in user."
);
}
} else {
log.debug("Webchannel is logging new a user in.");
}
// We don't act on customizeSync anymore, it used to open a dialog inside
// the browser to selecte the engines to sync but we do it on the web now.
log.debug("Webchannel is logging a user in.");
// There are (or were) extra fields here we don't want to actually store.
delete accountData.customizeSync;
delete accountData.verifiedCanLinkAccount;
if (lazy.oauthEnabled) {
// We once accidentally saw these from the server and got confused about who owned the key fetching.
delete accountData.keyFetchToken;
delete accountData.unwrapBKey;
}
// Save requested services for later.
const requestedServices = accountData.services;
// The "services" being connected - see above re our careful handling of existing data.
// Note that we don't attempt to merge any data - we keep the first value we see for a service
// and ignore that service subsequently (as it will be common for subsequent messages to
// name a service but not supply any data for it)
const requestedServices = {
...(accountData.services ?? {}),
...existingServices,
};
delete accountData.services;
// the user has already been shown the "can link account"
// screen. No need to keep this data around.
delete accountData.verifiedCanLinkAccount;
// Remember who it was so we can log out next time.
// This `verified` check is really just for our tests and pre-oauth flows.
// However, in all cases it's misplaced - we should set it as soon as *sync*
// starts or is configured, as it's the merging done by sync it protects against.
// We should clean up handling of this pref in a followup.
if (accountData.verified) {
this.setPreviousAccountNameHashPref(accountData.email);
}
await this._fxAccounts.telemetry.recordConnection(
Object.keys(requestedServices || {}),
Object.keys(requestedServices),
"webchannel"
);
if (lazy.oauthEnabled) {
// XXX - work around a server issue - we should never be handed these items in oauth flows.
// Fixed in https://github.com/mozilla/fxa/pull/17892, so this can probably be made more
// aggressive (eg, refuse to connect if they are present or similar)
delete accountData.keyFetchToken;
delete accountData.unwrapBKey;
// We need to remember the requested services because we can't act on them until we get the `oauth_login` message.
// And because we might not get that message in this browser session (eg, the browser might restart before the
// user enters their verification code), they are persisted with the account state.
log.debug(`storing info for services ${Object.keys(requestedServices)}`);
accountData.requestedServices = JSON.stringify(requestedServices);
await this._fxAccounts._internal.setSignedInUser(accountData);
} else {
const xps = await this._initializeSync();
// Note we don't persist anything in requestedServices for non oauth flows because we act on them now.
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();
}
}
await this._enableRequestedServices(requestedServices);
}
log.debug("Webchannel finished logging a user in.");
},
/**
* Disconnects the user from Sync and FxA
*
*/
_disconnect() {
return SyncDisconnect.disconnect(false);
},
/**
* Logins in to sync by completing an OAuth flow
* @param { Object } oauthData: The oauth code and state as returned by the server */
* @param { Object } oauthData: The oauth code and state as returned by the server
*/
async oauthLogin(oauthData) {
log.debug("Webchannel is completing the oauth flow");
const xps = await this._initializeSync();
const { sessionToken, email } =
const { uid, sessionToken, email, requestedServices } =
await this._fxAccounts._internal.getUserAccountData([
"uid",
"sessionToken",
"email",
"requestedServices",
]);
// First we finish the ongoing oauth flow
const { scopedKeys, refreshToken } =
@ -587,28 +623,31 @@ FxAccountsWebChannelHelpers.prototype = {
// Then, we persist the sync keys
await this._fxAccounts._internal.setScopedKeys(scopedKeys);
// Now that we have the scoped keys, we set our status to verified
await this._fxAccounts._internal.setUserVerified();
// And work out what should be enabled.
const services = oauthData.services;
log.trace("Webchannel is enabling services", services);
if (services) {
if (services.sync) {
// User has enabled Sync.
log.debug("Webchannel is enabling sync");
const { offeredEngines, declinedEngines } = services.sync;
this._setEnabledEngines(offeredEngines, declinedEngines);
xps.Weave.Service.configure();
try {
let parsedRequestedServices;
if (requestedServices) {
parsedRequestedServices = JSON.parse(requestedServices);
}
} else {
// To support fxa before it knew to send `services`, we assume it missing means "enable sync" and that the top-level
// payload has `offeredSyncEngines` and `declinedSyncEngines`
// TODO: remove this branch as soon as the above branch starts working - only old FxA servers need this!
const { declinedSyncEngines, offeredSyncEngines } = oauthData;
log.debug("Webchannel is enabling sync (deprecated message format");
this._setEnabledEngines(offeredSyncEngines, declinedSyncEngines);
xps.Weave.Service.configure();
await this._enableRequestedServices(parsedRequestedServices);
} finally {
// We don't want them hanging around in storage.
await this._fxAccounts._internal.updateUserAccountData({
uid,
requestedServices: null,
});
}
// Now that we have the scoped keys, we set our status to verified.
// This will kick off Sync or other services we configured.
await this._fxAccounts._internal.setUserVerified();
log.debug("Webchannel completed oauth flows");
},
/**
* Disconnects the user from Sync and FxA
*/
_disconnect() {
return SyncDisconnect.disconnect(false);
},
/**
@ -639,7 +678,6 @@ FxAccountsWebChannelHelpers.prototype = {
let browser = sendingContext.browsingContext.top.embedderElement;
const isPrivateBrowsing =
this._privateBrowsingUtils.isBrowserPrivate(browser);
log.debug("is private browsing", isPrivateBrowsing);
return isPrivateBrowsing;
},
@ -663,13 +701,15 @@ FxAccountsWebChannelHelpers.prototype = {
// with FxA as part of a Sync flow should work all the time. If
// Sync is broken in PB mode, users will think Firefox is broken.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1323853
log.debug("service", service);
return (
!this.isPrivateBrowsingMode(sendingContext) ||
service === "sync" ||
context === "fx_desktop_v3" ||
isPairing
//
// XXX - This hard-coded context seems bad?
let pb = this.isPrivateBrowsingMode(sendingContext);
let ok =
!pb || service === "sync" || context === "fx_desktop_v3" || isPairing;
log.debug(
`fxa status ok=${ok} - private=${pb}, service=${service}, context=${context}, pairing=${isPairing}`
);
return ok;
},
/**
@ -707,45 +747,31 @@ FxAccountsWebChannelHelpers.prototype = {
capabilities,
};
},
_getCapabilities() {
if (lazy.oauthEnabled) {
let engines = Array.from(CHOOSE_WHAT_TO_SYNC_ALWAYS_AVAILABLE);
for (let optionalEngine of CHOOSE_WHAT_TO_SYNC_OPTIONALLY_AVAILABLE) {
if (
Services.prefs.getBoolPref(
`services.sync.engine.${optionalEngine}.available`,
false
)
) {
engines.push(optionalEngine);
}
// pre-oauth flows there we a strange setup where we just supplied the "extra" engines,
// whereas oauth flows want them all.
let engines = lazy.oauthEnabled
? Array.from(CHOOSE_WHAT_TO_SYNC_ALWAYS_AVAILABLE)
: [];
for (let optionalEngine of CHOOSE_WHAT_TO_SYNC_OPTIONALLY_AVAILABLE) {
if (
Services.prefs.getBoolPref(
`services.sync.engine.${optionalEngine}.available`,
false
)
) {
engines.push(optionalEngine);
}
return {
multiService: true,
pairing: lazy.pairingEnabled,
choose_what_to_sync: true,
engines,
};
}
return {
multiService: true,
pairing: lazy.pairingEnabled,
engines: this._getAvailableExtraEngines(),
choose_what_to_sync: true,
engines,
};
},
_getAvailableExtraEngines() {
return EXTRA_ENGINES.filter(engineName => {
try {
return Services.prefs.getBoolPref(
`services.sync.engine.${engineName}.available`
);
} catch (e) {
return false;
}
});
},
async changePassword(credentials) {
// If |credentials| has fields that aren't handled by accounts storage,
// updateUserAccountData will throw - mainly to prevent errors in code

Просмотреть файл

@ -919,6 +919,68 @@ add_task(async function test_helpers_login_nothing_offered() {
Assert.ok(configured);
});
add_task(async function test_helpers_persist_requested_services() {
ensureOauthConfigured();
let accountData = null;
const helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
_internal: {
async setSignedInUser(newAccountData) {
accountData = newAccountData;
return accountData;
},
},
async getSignedInUser() {
return accountData;
},
telemetry: {
recordConnection() {},
},
},
weaveXPCOM: {
whenLoaded() {},
Weave: {
Service: {},
},
},
});
await helpers.login({
uid: "auid",
email: "testuser@testuser.com",
verifiedCanLinkAccount: true,
services: {
first_only: { x: 10 }, // this data is not in the update below.
sync: { important: true },
},
});
Assert.deepEqual(JSON.parse(accountData.requestedServices), {
first_only: { x: 10 },
sync: { important: true },
});
// A second "login" message without the services.
await helpers.login({
uid: "auid",
email: "testuser@testuser.com",
verifiedCanLinkAccount: true,
services: {
// the service is mentioned, but data is empty, so it's the old version of the data we want.
sync: {},
// a new service we never saw before, but we still want it.
new: { name: "opted in" }, // not in original, but we want in the final.
},
});
// the version with the data should remain.
Assert.deepEqual(JSON.parse(accountData.requestedServices), {
first_only: { x: 10 },
sync: { important: true },
new: { name: "opted in" },
});
resetOauthConfig();
});
add_test(function test_helpers_open_sync_preferences() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {},
@ -997,7 +1059,7 @@ add_task(async function test_helpers_getFxAStatus_engines_oauth() {
ok(!!fxaStatus);
ok(!!fxaStatus.signedInUser);
// in the oauth flows we expect all engines.
deepEqual(fxaStatus.capabilities.engines, [
deepEqual(fxaStatus.capabilities.engines.toSorted(), [
"addons",
"bookmarks",
"creditcards",
@ -1010,15 +1072,15 @@ add_task(async function test_helpers_getFxAStatus_engines_oauth() {
// try again with addresses enabled.
Services.prefs.setBoolPref("services.sync.engine.addresses.available", true);
fxaStatus = await helpers.getFxaStatus("sync", mockSendingContext);
deepEqual(fxaStatus.capabilities.engines, [
deepEqual(fxaStatus.capabilities.engines.toSorted(), [
"addons",
"addresses",
"bookmarks",
"creditcards",
"history",
"passwords",
"prefs",
"tabs",
"addresses",
]);
resetOauthConfig();