1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * Firefox Accounts Web Channel.
8 * Uses the WebChannel component to receive messages
9 * about account state changes.
12 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
15 COMMAND_PROFILE_CHANGE,
20 COMMAND_CAN_LINK_ACCOUNT,
21 COMMAND_SYNC_PREFERENCES,
22 COMMAND_CHANGE_PASSWORD,
24 COMMAND_PAIR_HEARTBEAT,
25 COMMAND_PAIR_SUPP_METADATA,
26 COMMAND_PAIR_AUTHORIZE,
28 COMMAND_PAIR_COMPLETE,
29 COMMAND_PAIR_PREFERENCES,
32 ON_PROFILE_CHANGE_NOTIFICATION,
37 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
41 ChromeUtils.defineESModuleGetters(lazy, {
42 CryptoUtils: "resource://services-crypto/utils.sys.mjs",
43 FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs",
44 FxAccountsStorageManagerCanStoreField:
45 "resource://gre/modules/FxAccountsStorage.sys.mjs",
46 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
47 Weave: "resource://services-sync/main.sys.mjs",
48 WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
50 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
51 return ChromeUtils.importESModule(
52 "resource://gre/modules/FxAccounts.sys.mjs"
53 ).getFxAccountsSingleton();
55 XPCOMUtils.defineLazyPreferenceGetter(
58 "identity.fxaccounts.pairing.enabled"
60 XPCOMUtils.defineLazyPreferenceGetter(
62 "separatePrivilegedMozillaWebContentProcess",
63 "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
66 XPCOMUtils.defineLazyPreferenceGetter(
68 "separatedMozillaDomains",
69 "browser.tabs.remote.separatedMozillaDomains",
74 XPCOMUtils.defineLazyPreferenceGetter(
77 "identity.fxaccounts.remote.root",
80 val => Services.io.newURI(val)
83 XPCOMUtils.defineLazyPreferenceGetter(
86 "identity.fxaccounts.oauth.enabled",
90 // These engines were added years after Sync had been introduced, they need
91 // special handling since they are system add-ons and are un-available on
92 // older versions of Firefox.
93 const EXTRA_ENGINES = ["addresses", "creditcards"];
95 // These engines will be displayed to the user to pick which they would like to
97 const CHOOSE_WHAT_TO_SYNC = [
109 * A helper function that extracts the message and stack from an error object.
110 * Returns a `{ message, stack }` tuple. `stack` will be null if the error
111 * doesn't have a stack trace.
113 function getErrorDetails(error) {
114 // Replace anything that looks like it might be a filepath on Windows or Unix
115 let cleanMessage = String(error)
116 .replace(/\\.*\\/gm, "[REDACTED]")
117 .replace(/\/.*\//gm, "[REDACTED]");
118 let details = { message: cleanMessage, stack: null };
120 // Adapted from Console.sys.mjs.
123 for (let frame = error.stack; frame; frame = frame.caller) {
124 frames.push(String(frame).padStart(4));
126 details.stack = frames.join("\n");
133 * Create a new FxAccountsWebChannel to listen for account updates
135 * @param {Object} options Options
136 * @param {Object} options
137 * @param {String} options.content_uri
138 * The FxA Content server uri
139 * @param {String} options.channel_id
140 * The ID of the WebChannel
141 * @param {String} options.helpers
142 * Helpers functions. Should only be passed in for testing.
145 export function FxAccountsWebChannel(options) {
147 throw new Error("Missing configuration options");
149 if (!options.content_uri) {
150 throw new Error("Missing 'content_uri' option");
152 this._contentUri = options.content_uri;
154 if (!options.channel_id) {
155 throw new Error("Missing 'channel_id' option");
157 this._webChannelId = options.channel_id;
159 // options.helpers is only specified by tests.
160 ChromeUtils.defineLazyGetter(this, "_helpers", () => {
161 return options.helpers || new FxAccountsWebChannelHelpers(options);
164 this._setupChannel();
167 FxAccountsWebChannel.prototype = {
169 * WebChannel that is used to communicate with content page
174 * Helpers interface that does the heavy lifting.
183 * WebChannel origin, used to validate origin of messages
185 _webChannelOrigin: null,
188 * Release all resources that are in use.
191 this._channel.stopListening();
192 this._channel = null;
193 this._channelCallback = null;
197 * Configures and registers a new WebChannel
202 // if this.contentUri is present but not a valid URI, then this will throw an error.
204 this._webChannelOrigin = Services.io.newURI(this._contentUri);
205 this._registerChannel();
212 _receiveMessage(message, sendingContext) {
213 const { command, data } = message;
214 let shouldCheckRemoteType =
215 lazy.separatePrivilegedMozillaWebContentProcess &&
216 lazy.separatedMozillaDomains.some(function (val) {
218 lazy.accountServer.asciiHost == val ||
219 lazy.accountServer.asciiHost.endsWith("." + val)
222 let { currentRemoteType } = sendingContext.browsingContext;
223 if (shouldCheckRemoteType && currentRemoteType != "privilegedmozilla") {
225 `Rejected FxA webchannel message from remoteType = ${currentRemoteType}`
230 let browser = sendingContext.browsingContext.top.embedderElement;
232 case COMMAND_PROFILE_CHANGE:
233 Services.obs.notifyObservers(
235 ON_PROFILE_CHANGE_NOTIFICATION,
242 .catch(error => this._sendError(error, message, sendingContext));
247 .catch(error => this._sendError(error, message, sendingContext));
253 .catch(error => this._sendError(error, message, sendingContext));
255 case COMMAND_CAN_LINK_ACCOUNT:
256 let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
260 messageId: message.messageId,
261 data: { ok: canLinkAccount },
264 log.debug("FxAccountsWebChannel response", response);
265 this._channel.send(response, sendingContext);
267 case COMMAND_SYNC_PREFERENCES:
268 this._helpers.openSyncPreferences(browser, data.entryPoint);
270 case COMMAND_PAIR_PREFERENCES:
271 if (lazy.pairingEnabled) {
272 let window = browser.ownerGlobal;
273 // We should close the FxA tab after we open our pref page
274 let selectedTab = window.gBrowser.selectedTab;
275 window.switchToTabHavingURI(
276 "about:preferences?action=pair#sync",
279 ignoreQueryString: true,
280 replaceQueryString: true,
281 adoptIntoActiveWindow: true,
282 ignoreFragment: "whenComparing",
284 Services.scriptSecurityManager.getSystemPrincipal(),
288 window.gBrowser.removeTab(selectedTab);
291 case COMMAND_FIREFOX_VIEW:
292 this._helpers.openFirefoxView(browser, data.entryPoint);
294 case COMMAND_CHANGE_PASSWORD:
296 .changePassword(data)
297 .catch(error => this._sendError(error, message, sendingContext));
299 case COMMAND_FXA_STATUS:
300 log.debug("fxa_status received");
302 const service = data && data.service;
303 const isPairing = data && data.isPairing;
304 const context = data && data.context;
306 .getFxaStatus(service, sendingContext, isPairing, context)
310 messageId: message.messageId,
313 this._channel.send(response, sendingContext);
315 .catch(error => this._sendError(error, message, sendingContext));
317 case COMMAND_PAIR_HEARTBEAT:
318 case COMMAND_PAIR_SUPP_METADATA:
319 case COMMAND_PAIR_AUTHORIZE:
320 case COMMAND_PAIR_DECLINE:
321 case COMMAND_PAIR_COMPLETE:
322 log.debug(`Pairing command ${command} received`);
323 const { channel_id: channelId } = data;
324 delete data.channel_id;
325 const flow = lazy.FxAccountsPairingFlow.get(channelId);
327 log.warn(`Could not find a pairing flow for ${channelId}`);
330 flow.onWebChannelMessage(command, data).then(replyData => {
334 messageId: message.messageId,
342 log.warn("Unrecognized FxAccountsWebChannel command", command);
343 // As a safety measure we also terminate any pending FxA pairing flow.
344 lazy.FxAccountsPairingFlow.finalizeAll();
349 _sendError(error, incomingMessage, sendingContext) {
350 log.error("Failed to handle FxAccountsWebChannel message", error);
353 command: incomingMessage.command,
354 messageId: incomingMessage.messageId,
356 error: getErrorDetails(error),
364 * Create a new channel with the WebChannelBroker, setup a callback listener
369 * Processes messages that are called back from the FxAccountsChannel
371 * @param webChannelId {String}
372 * Command webChannelId
373 * @param message {Object}
375 * @param sendingContext {Object}
376 * Message sending context.
377 * @param sendingContext.browsingContext {BrowsingContext}
378 * The browsingcontext from which the
379 * WebChannelMessageToChrome was sent.
380 * @param sendingContext.eventTarget {EventTarget}
381 * The <EventTarget> where the message was sent.
382 * @param sendingContext.principal {Principal}
383 * The <Principal> of the EventTarget where the message was sent.
387 let listener = (webChannelId, message, sendingContext) => {
389 log.debug("FxAccountsWebChannel message received", message.command);
391 log.debug("FxAccountsWebChannel message details", message);
394 this._receiveMessage(message, sendingContext);
396 this._sendError(error, message, sendingContext);
401 this._channelCallback = listener;
402 this._channel = new lazy.WebChannel(
404 this._webChannelOrigin
406 this._channel.listen(listener);
408 "FxAccountsWebChannel registered: " +
411 this._webChannelOrigin.prePath
416 export function FxAccountsWebChannelHelpers(options) {
417 options = options || {};
419 this._fxAccounts = options.fxAccounts || lazy.fxAccounts;
420 this._weaveXPCOM = options.weaveXPCOM || null;
421 this._privateBrowsingUtils =
422 options.privateBrowsingUtils || lazy.PrivateBrowsingUtils;
425 FxAccountsWebChannelHelpers.prototype = {
426 // If the last fxa account used for sync isn't this account, we display
427 // a modal dialog checking they really really want to do this...
428 // (This is sync-specific, so ideally would be in sync's identity module,
429 // but it's a little more seamless to do here, and sync is currently the
430 // only fxa consumer, so...
431 shouldAllowRelink(acctName) {
433 !this._needRelinkWarning(acctName) || this._promptForRelink(acctName)
437 async _initializeSync() {
438 // A sync-specific hack - we want to ensure sync has been initialized
439 // before we set the signed-in user.
440 // XXX - probably not true any more, especially now we have observerPreloads
441 // in FxAccounts.sys.mjs?
444 Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
446 await xps.whenLoaded();
450 _setEnabledEngines(offeredEngines, declinedEngines) {
451 if (offeredEngines && declinedEngines) {
452 EXTRA_ENGINES.forEach(engine => {
454 offeredEngines.includes(engine) &&
455 !declinedEngines.includes(engine)
457 // These extra engines are disabled by default.
458 Services.prefs.setBoolPref(`services.sync.engine.${engine}`, true);
461 log.debug("Received declined engines", declinedEngines);
462 lazy.Weave.Service.engineManager.setDeclined(declinedEngines);
463 declinedEngines.forEach(engine => {
464 Services.prefs.setBoolPref(`services.sync.engine.${engine}`, false);
469 * stores sync login info it in the fxaccounts service
471 * @param accountData the user's account data and credentials
473 async login(accountData) {
474 // We don't act on customizeSync anymore, it used to open a dialog inside
475 // the browser to selecte the engines to sync but we do it on the web now.
476 log.debug("Webchannel is logging a user in.");
477 delete accountData.customizeSync;
479 // Save requested services for later.
480 const requestedServices = accountData.services;
481 delete accountData.services;
483 // the user has already been shown the "can link account"
484 // screen. No need to keep this data around.
485 delete accountData.verifiedCanLinkAccount;
487 // Remember who it was so we can log out next time.
488 if (accountData.verified) {
489 this.setPreviousAccountNameHashPref(accountData.email);
492 await this._fxAccounts.telemetry.recordConnection(
493 Object.keys(requestedServices || {}),
497 if (lazy.oauthEnabled) {
498 await this._fxAccounts._internal.setSignedInUser(accountData);
500 const xps = await this._initializeSync();
501 await this._fxAccounts._internal.setSignedInUser(accountData);
502 if (requestedServices) {
503 // User has enabled Sync.
504 if (requestedServices.sync) {
505 const { offeredEngines, declinedEngines } = requestedServices.sync;
506 this._setEnabledEngines(offeredEngines, declinedEngines);
507 log.debug("Webchannel is enabling sync");
508 await xps.Weave.Service.configure();
515 * Logins in to sync by completing an OAuth flow
516 * @param { Object } oauthData: The oauth code and state as returned by the server */
517 async oauthLogin(oauthData) {
518 log.debug("Webchannel is completing the oauth flow");
519 const xps = await this._initializeSync();
520 const { code, state, declinedSyncEngines, offeredSyncEngines } = oauthData;
521 const { sessionToken } =
522 await this._fxAccounts._internal.getUserAccountData(["sessionToken"]);
523 // First we finish the ongoing oauth flow
524 const { scopedKeys, refreshToken } =
525 await this._fxAccounts._internal.completeOAuthFlow(
531 // We don't currently use the refresh token in Firefox Desktop, lets be good citizens and revoke it.
532 await this._fxAccounts._internal.destroyOAuthToken({ token: refreshToken });
534 // Then, we persist the sync keys
535 await this._fxAccounts._internal.setScopedKeys(scopedKeys);
537 // Now that we have the scoped keys, we set our status to verified
538 await this._fxAccounts._internal.setUserVerified();
539 this._setEnabledEngines(offeredSyncEngines, declinedSyncEngines);
540 log.debug("Webchannel is enabling sync");
541 xps.Weave.Service.configure();
545 * logout the fxaccounts service
547 * @param the uid of the account which have been logged out
550 let fxa = this._fxAccounts;
551 let userData = await fxa._internal.getUserAccountData(["uid"]);
552 if (userData && userData.uid === uid) {
553 await fxa.telemetry.recordDisconnection(null, "webchannel");
554 // true argument is `localOnly`, because server-side stuff
555 // has already been taken care of by the content server
556 await fxa.signOut(true);
561 * Check if `sendingContext` is in private browsing mode.
563 isPrivateBrowsingMode(sendingContext) {
564 if (!sendingContext) {
565 log.error("Unable to check for private browsing mode, assuming true");
569 let browser = sendingContext.browsingContext.top.embedderElement;
570 const isPrivateBrowsing =
571 this._privateBrowsingUtils.isBrowserPrivate(browser);
572 log.debug("is private browsing", isPrivateBrowsing);
573 return isPrivateBrowsing;
577 * Check whether sending fxa_status data should be allowed.
579 shouldAllowFxaStatus(service, sendingContext, isPairing, context) {
580 // Return user data for any service in non-PB mode. In PB mode,
581 // only return user data if service==="sync" or is in pairing mode
582 // (as service will be equal to the OAuth client ID and not "sync").
584 // This behaviour allows users to click the "Manage Account"
585 // link from about:preferences#sync while in PB mode and things
586 // "just work". While in non-PB mode, users can sign into
587 // Pocket w/o entering their password a 2nd time, while in PB
588 // mode they *will* have to enter their email/password again.
590 // The difference in behaviour is to try to match user
591 // expectations as to what is and what isn't part of the browser.
592 // Sync is viewed as an integral part of the browser, interacting
593 // with FxA as part of a Sync flow should work all the time. If
594 // Sync is broken in PB mode, users will think Firefox is broken.
595 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1323853
596 log.debug("service", service);
598 !this.isPrivateBrowsingMode(sendingContext) ||
599 service === "sync" ||
600 context === "fx_desktop_v3" ||
606 * Get fxa_status information. Resolves to { signedInUser: <user_data> }.
607 * If returning status information is not allowed or no user is signed into
608 * Sync, `user_data` will be null.
610 async getFxaStatus(service, sendingContext, isPairing, context) {
611 let signedInUser = null;
614 this.shouldAllowFxaStatus(service, sendingContext, isPairing, context)
616 const userData = await this._fxAccounts._internal.getUserAccountData([
624 email: userData.email,
625 sessionToken: userData.sessionToken,
627 verified: userData.verified,
632 const capabilities = this._getCapabilities();
636 clientId: FX_OAUTH_CLIENT_ID,
641 if (lazy.oauthEnabled) {
644 pairing: lazy.pairingEnabled,
645 choose_what_to_sync: true,
646 engines: CHOOSE_WHAT_TO_SYNC,
651 pairing: lazy.pairingEnabled,
652 engines: this._getAvailableExtraEngines(),
656 _getAvailableExtraEngines() {
657 return EXTRA_ENGINES.filter(engineName => {
659 return Services.prefs.getBoolPref(
660 `services.sync.engine.${engineName}.available`
668 async changePassword(credentials) {
669 // If |credentials| has fields that aren't handled by accounts storage,
670 // updateUserAccountData will throw - mainly to prevent errors in code
671 // that hard-codes field names.
672 // However, in this case the field names aren't really in our control.
673 // We *could* still insist the server know what fields names are valid,
674 // but that makes life difficult for the server when Firefox adds new
675 // features (ie, new fields) - forcing the server to track a map of
676 // versions to supported field names doesn't buy us much.
677 // So we just remove field names we know aren't handled.
678 let newCredentials = {
679 device: null, // Force a brand new device registration.
680 // We force the re-encryption of the send tab keys using the new sync key after the password change
681 encryptedSendTabKeys: null,
683 for (let name of Object.keys(credentials)) {
687 lazy.FxAccountsStorageManagerCanStoreField(name)
689 newCredentials[name] = credentials[name];
691 log.info("changePassword ignoring unsupported field", name);
694 await this._fxAccounts._internal.updateUserAccountData(newCredentials);
695 await this._fxAccounts._internal.updateDeviceRegistration();
699 * Get the hash of account name of the previously signed in account
701 getPreviousAccountNameHashPref() {
703 return Services.prefs.getStringPref(PREF_LAST_FXA_USER);
710 * Given an account name, set the hash of the previously signed in account
712 * @param acctName the account name of the user's account.
714 setPreviousAccountNameHashPref(acctName) {
715 Services.prefs.setStringPref(
717 lazy.CryptoUtils.sha256Base64(acctName)
722 * Open Sync Preferences in the current tab of the browser
724 * @param {Object} browser the browser in which to open preferences
725 * @param {String} [entryPoint] entryPoint to use for logging
727 openSyncPreferences(browser, entryPoint) {
728 let uri = "about:preferences";
730 uri += "?entrypoint=" + encodeURIComponent(entryPoint);
734 browser.loadURI(Services.io.newURI(uri), {
735 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
740 * Open Firefox View in the browser's window
742 * @param {Object} browser the browser in whose window we'll open Firefox View
744 openFirefoxView(browser) {
745 browser.ownerGlobal.FirefoxViewHandler.openTab("syncedtabs");
749 * If a user signs in using a different account, the data from the
750 * previous account and the new account will be merged. Ask the user
751 * if they want to continue.
755 _needRelinkWarning(acctName) {
756 let prevAcctHash = this.getPreviousAccountNameHashPref();
758 prevAcctHash && prevAcctHash != lazy.CryptoUtils.sha256Base64(acctName)
763 * Show the user a warning dialog that the data from the previous account
764 * and the new account will be merged.
768 _promptForRelink(acctName) {
769 let sb = Services.strings.createBundle(
770 "chrome://browser/locale/syncSetup.properties"
772 let continueLabel = sb.GetStringFromName("continue.label");
773 let title = sb.GetStringFromName("relinkVerify.title");
774 let description = sb.formatStringFromName("relinkVerify.description", [
778 sb.GetStringFromName("relinkVerify.heading") + "\n\n" + description;
779 let ps = Services.prompt;
781 ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
782 ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
783 ps.BUTTON_POS_1_DEFAULT;
785 // If running in context of the browser chrome, window does not exist.
786 let pressed = Services.prompt.confirmEx(
797 return pressed === 0; // 0 is the "continue" button
803 // The entry-point for this module, which ensures only one of our channels is
804 // ever created - we require this because the WebChannel is global in scope
805 // (eg, it uses the observer service to tell interested parties of interesting
806 // things) and allowing multiple channels would cause such notifications to be
807 // sent multiple times.
808 export var EnsureFxAccountsWebChannel = () => {
809 let contentUri = Services.urlFormatter.formatURLPref(
810 "identity.fxaccounts.remote.root"
812 if (singleton && singleton._contentUri !== contentUri) {
813 singleton.tearDown();
819 // The FxAccountsWebChannel listens for events and updates
820 // the state machine accordingly.
821 singleton = new FxAccountsWebChannel({
822 content_uri: contentUri,
823 channel_id: WEBCHANNEL_ID,
826 log.warn("FxA WebChannel functionaly is disabled due to no URI pref.");
829 log.error("Failed to create FxA WebChannel", ex);