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,
19 COMMAND_CAN_LINK_ACCOUNT,
20 COMMAND_SYNC_PREFERENCES,
21 COMMAND_CHANGE_PASSWORD,
23 COMMAND_PAIR_HEARTBEAT,
24 COMMAND_PAIR_SUPP_METADATA,
25 COMMAND_PAIR_AUTHORIZE,
27 COMMAND_PAIR_COMPLETE,
28 COMMAND_PAIR_PREFERENCES,
31 ON_PROFILE_CHANGE_NOTIFICATION,
36 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
40 ChromeUtils.defineESModuleGetters(lazy, {
41 CryptoUtils: "resource://services-crypto/utils.sys.mjs",
42 FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs",
43 FxAccountsStorageManagerCanStoreField:
44 "resource://gre/modules/FxAccountsStorage.sys.mjs",
45 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
46 Weave: "resource://services-sync/main.sys.mjs",
47 WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
49 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
50 return ChromeUtils.importESModule(
51 "resource://gre/modules/FxAccounts.sys.mjs"
52 ).getFxAccountsSingleton();
54 XPCOMUtils.defineLazyPreferenceGetter(
57 "identity.fxaccounts.pairing.enabled"
59 XPCOMUtils.defineLazyPreferenceGetter(
61 "separatePrivilegedMozillaWebContentProcess",
62 "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
65 XPCOMUtils.defineLazyPreferenceGetter(
67 "separatedMozillaDomains",
68 "browser.tabs.remote.separatedMozillaDomains",
73 XPCOMUtils.defineLazyPreferenceGetter(
76 "identity.fxaccounts.remote.root",
79 val => Services.io.newURI(val)
82 // These engines were added years after Sync had been introduced, they need
83 // special handling since they are system add-ons and are un-available on
84 // older versions of Firefox.
85 const EXTRA_ENGINES = ["addresses", "creditcards"];
88 * A helper function that extracts the message and stack from an error object.
89 * Returns a `{ message, stack }` tuple. `stack` will be null if the error
90 * doesn't have a stack trace.
92 function getErrorDetails(error) {
93 // Replace anything that looks like it might be a filepath on Windows or Unix
94 let cleanMessage = String(error)
95 .replace(/\\.*\\/gm, "[REDACTED]")
96 .replace(/\/.*\//gm, "[REDACTED]");
97 let details = { message: cleanMessage, stack: null };
99 // Adapted from Console.sys.mjs.
102 for (let frame = error.stack; frame; frame = frame.caller) {
103 frames.push(String(frame).padStart(4));
105 details.stack = frames.join("\n");
112 * Create a new FxAccountsWebChannel to listen for account updates
114 * @param {Object} options Options
115 * @param {Object} options
116 * @param {String} options.content_uri
117 * The FxA Content server uri
118 * @param {String} options.channel_id
119 * The ID of the WebChannel
120 * @param {String} options.helpers
121 * Helpers functions. Should only be passed in for testing.
124 export function FxAccountsWebChannel(options) {
126 throw new Error("Missing configuration options");
128 if (!options.content_uri) {
129 throw new Error("Missing 'content_uri' option");
131 this._contentUri = options.content_uri;
133 if (!options.channel_id) {
134 throw new Error("Missing 'channel_id' option");
136 this._webChannelId = options.channel_id;
138 // options.helpers is only specified by tests.
139 ChromeUtils.defineLazyGetter(this, "_helpers", () => {
140 return options.helpers || new FxAccountsWebChannelHelpers(options);
143 this._setupChannel();
146 FxAccountsWebChannel.prototype = {
148 * WebChannel that is used to communicate with content page
153 * Helpers interface that does the heavy lifting.
162 * WebChannel origin, used to validate origin of messages
164 _webChannelOrigin: null,
167 * Release all resources that are in use.
170 this._channel.stopListening();
171 this._channel = null;
172 this._channelCallback = null;
176 * Configures and registers a new WebChannel
181 // if this.contentUri is present but not a valid URI, then this will throw an error.
183 this._webChannelOrigin = Services.io.newURI(this._contentUri);
184 this._registerChannel();
191 _receiveMessage(message, sendingContext) {
192 const { command, data } = message;
193 let shouldCheckRemoteType =
194 lazy.separatePrivilegedMozillaWebContentProcess &&
195 lazy.separatedMozillaDomains.some(function (val) {
197 lazy.accountServer.asciiHost == val ||
198 lazy.accountServer.asciiHost.endsWith("." + val)
201 let { currentRemoteType } = sendingContext.browsingContext;
202 if (shouldCheckRemoteType && currentRemoteType != "privilegedmozilla") {
204 `Rejected FxA webchannel message from remoteType = ${currentRemoteType}`
209 let browser = sendingContext.browsingContext.top.embedderElement;
211 case COMMAND_PROFILE_CHANGE:
212 Services.obs.notifyObservers(
214 ON_PROFILE_CHANGE_NOTIFICATION,
221 .catch(error => this._sendError(error, message, sendingContext));
227 .catch(error => this._sendError(error, message, sendingContext));
229 case COMMAND_CAN_LINK_ACCOUNT:
230 let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
234 messageId: message.messageId,
235 data: { ok: canLinkAccount },
238 log.debug("FxAccountsWebChannel response", response);
239 this._channel.send(response, sendingContext);
241 case COMMAND_SYNC_PREFERENCES:
242 this._helpers.openSyncPreferences(browser, data.entryPoint);
244 case COMMAND_PAIR_PREFERENCES:
245 if (lazy.pairingEnabled) {
246 let window = browser.ownerGlobal;
247 // We should close the FxA tab after we open our pref page
248 let selectedTab = window.gBrowser.selectedTab;
249 window.switchToTabHavingURI(
250 "about:preferences?action=pair#sync",
253 ignoreQueryString: true,
254 replaceQueryString: true,
255 adoptIntoActiveWindow: true,
256 ignoreFragment: "whenComparing",
258 Services.scriptSecurityManager.getSystemPrincipal(),
262 window.gBrowser.removeTab(selectedTab);
265 case COMMAND_FIREFOX_VIEW:
266 this._helpers.openFirefoxView(browser, data.entryPoint);
268 case COMMAND_CHANGE_PASSWORD:
270 .changePassword(data)
271 .catch(error => this._sendError(error, message, sendingContext));
273 case COMMAND_FXA_STATUS:
274 log.debug("fxa_status received");
276 const service = data && data.service;
277 const isPairing = data && data.isPairing;
278 const context = data && data.context;
280 .getFxaStatus(service, sendingContext, isPairing, context)
284 messageId: message.messageId,
287 this._channel.send(response, sendingContext);
289 .catch(error => this._sendError(error, message, sendingContext));
291 case COMMAND_PAIR_HEARTBEAT:
292 case COMMAND_PAIR_SUPP_METADATA:
293 case COMMAND_PAIR_AUTHORIZE:
294 case COMMAND_PAIR_DECLINE:
295 case COMMAND_PAIR_COMPLETE:
296 log.debug(`Pairing command ${command} received`);
297 const { channel_id: channelId } = data;
298 delete data.channel_id;
299 const flow = lazy.FxAccountsPairingFlow.get(channelId);
301 log.warn(`Could not find a pairing flow for ${channelId}`);
304 flow.onWebChannelMessage(command, data).then(replyData => {
308 messageId: message.messageId,
316 log.warn("Unrecognized FxAccountsWebChannel command", command);
317 // As a safety measure we also terminate any pending FxA pairing flow.
318 lazy.FxAccountsPairingFlow.finalizeAll();
323 _sendError(error, incomingMessage, sendingContext) {
324 log.error("Failed to handle FxAccountsWebChannel message", error);
327 command: incomingMessage.command,
328 messageId: incomingMessage.messageId,
330 error: getErrorDetails(error),
338 * Create a new channel with the WebChannelBroker, setup a callback listener
343 * Processes messages that are called back from the FxAccountsChannel
345 * @param webChannelId {String}
346 * Command webChannelId
347 * @param message {Object}
349 * @param sendingContext {Object}
350 * Message sending context.
351 * @param sendingContext.browsingContext {BrowsingContext}
352 * The browsingcontext from which the
353 * WebChannelMessageToChrome was sent.
354 * @param sendingContext.eventTarget {EventTarget}
355 * The <EventTarget> where the message was sent.
356 * @param sendingContext.principal {Principal}
357 * The <Principal> of the EventTarget where the message was sent.
361 let listener = (webChannelId, message, sendingContext) => {
363 log.debug("FxAccountsWebChannel message received", message.command);
365 log.debug("FxAccountsWebChannel message details", message);
368 this._receiveMessage(message, sendingContext);
370 this._sendError(error, message, sendingContext);
375 this._channelCallback = listener;
376 this._channel = new lazy.WebChannel(
378 this._webChannelOrigin
380 this._channel.listen(listener);
382 "FxAccountsWebChannel registered: " +
385 this._webChannelOrigin.prePath
390 export function FxAccountsWebChannelHelpers(options) {
391 options = options || {};
393 this._fxAccounts = options.fxAccounts || lazy.fxAccounts;
394 this._weaveXPCOM = options.weaveXPCOM || null;
395 this._privateBrowsingUtils =
396 options.privateBrowsingUtils || lazy.PrivateBrowsingUtils;
399 FxAccountsWebChannelHelpers.prototype = {
400 // If the last fxa account used for sync isn't this account, we display
401 // a modal dialog checking they really really want to do this...
402 // (This is sync-specific, so ideally would be in sync's identity module,
403 // but it's a little more seamless to do here, and sync is currently the
404 // only fxa consumer, so...
405 shouldAllowRelink(acctName) {
407 !this._needRelinkWarning(acctName) || this._promptForRelink(acctName)
412 * stores sync login info it in the fxaccounts service
414 * @param accountData the user's account data and credentials
416 async login(accountData) {
417 // We don't act on customizeSync anymore, it used to open a dialog inside
418 // the browser to selecte the engines to sync but we do it on the web now.
419 log.debug("Webchannel is logging a user in.");
420 delete accountData.customizeSync;
422 // Save requested services for later.
423 const requestedServices = accountData.services;
424 delete accountData.services;
426 // the user has already been shown the "can link account"
427 // screen. No need to keep this data around.
428 delete accountData.verifiedCanLinkAccount;
430 // Remember who it was so we can log out next time.
431 if (accountData.verified) {
432 this.setPreviousAccountNameHashPref(accountData.email);
435 await this._fxAccounts.telemetry.recordConnection(
436 Object.keys(requestedServices || {}),
440 // A sync-specific hack - we want to ensure sync has been initialized
441 // before we set the signed-in user.
442 // XXX - probably not true any more, especially now we have observerPreloads
443 // in FxAccounts.jsm?
446 Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
448 await xps.whenLoaded();
449 await this._fxAccounts._internal.setSignedInUser(accountData);
451 if (requestedServices) {
452 // User has enabled Sync.
453 if (requestedServices.sync) {
454 const { offeredEngines, declinedEngines } = requestedServices.sync;
455 if (offeredEngines && declinedEngines) {
456 EXTRA_ENGINES.forEach(engine => {
458 offeredEngines.includes(engine) &&
459 !declinedEngines.includes(engine)
461 // These extra engines are disabled by default.
462 Services.prefs.setBoolPref(
463 `services.sync.engine.${engine}`,
468 log.debug("Received declined engines", declinedEngines);
469 lazy.Weave.Service.engineManager.setDeclined(declinedEngines);
470 declinedEngines.forEach(engine => {
471 Services.prefs.setBoolPref(`services.sync.engine.${engine}`, false);
474 log.debug("Webchannel is enabling sync");
475 await xps.Weave.Service.configure();
481 * logout the fxaccounts service
483 * @param the uid of the account which have been logged out
486 let fxa = this._fxAccounts;
487 let userData = await fxa._internal.getUserAccountData(["uid"]);
488 if (userData && userData.uid === uid) {
489 await fxa.telemetry.recordDisconnection(null, "webchannel");
490 // true argument is `localOnly`, because server-side stuff
491 // has already been taken care of by the content server
492 await fxa.signOut(true);
497 * Check if `sendingContext` is in private browsing mode.
499 isPrivateBrowsingMode(sendingContext) {
500 if (!sendingContext) {
501 log.error("Unable to check for private browsing mode, assuming true");
505 let browser = sendingContext.browsingContext.top.embedderElement;
506 const isPrivateBrowsing =
507 this._privateBrowsingUtils.isBrowserPrivate(browser);
508 log.debug("is private browsing", isPrivateBrowsing);
509 return isPrivateBrowsing;
513 * Check whether sending fxa_status data should be allowed.
515 shouldAllowFxaStatus(service, sendingContext, isPairing, context) {
516 // Return user data for any service in non-PB mode. In PB mode,
517 // only return user data if service==="sync" or is in pairing mode
518 // (as service will be equal to the OAuth client ID and not "sync").
520 // This behaviour allows users to click the "Manage Account"
521 // link from about:preferences#sync while in PB mode and things
522 // "just work". While in non-PB mode, users can sign into
523 // Pocket w/o entering their password a 2nd time, while in PB
524 // mode they *will* have to enter their email/password again.
526 // The difference in behaviour is to try to match user
527 // expectations as to what is and what isn't part of the browser.
528 // Sync is viewed as an integral part of the browser, interacting
529 // with FxA as part of a Sync flow should work all the time. If
530 // Sync is broken in PB mode, users will think Firefox is broken.
531 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1323853
532 log.debug("service", service);
534 !this.isPrivateBrowsingMode(sendingContext) ||
535 service === "sync" ||
536 context === "fx_desktop_v3" ||
542 * Get fxa_status information. Resolves to { signedInUser: <user_data> }.
543 * If returning status information is not allowed or no user is signed into
544 * Sync, `user_data` will be null.
546 async getFxaStatus(service, sendingContext, isPairing, context) {
547 let signedInUser = null;
550 this.shouldAllowFxaStatus(service, sendingContext, isPairing, context)
552 const userData = await this._fxAccounts._internal.getUserAccountData([
560 email: userData.email,
561 sessionToken: userData.sessionToken,
563 verified: userData.verified,
570 clientId: FX_OAUTH_CLIENT_ID,
573 pairing: lazy.pairingEnabled,
574 engines: this._getAvailableExtraEngines(),
579 _getAvailableExtraEngines() {
580 return EXTRA_ENGINES.filter(engineName => {
582 return Services.prefs.getBoolPref(
583 `services.sync.engine.${engineName}.available`
591 async changePassword(credentials) {
592 // If |credentials| has fields that aren't handled by accounts storage,
593 // updateUserAccountData will throw - mainly to prevent errors in code
594 // that hard-codes field names.
595 // However, in this case the field names aren't really in our control.
596 // We *could* still insist the server know what fields names are valid,
597 // but that makes life difficult for the server when Firefox adds new
598 // features (ie, new fields) - forcing the server to track a map of
599 // versions to supported field names doesn't buy us much.
600 // So we just remove field names we know aren't handled.
601 let newCredentials = {
602 device: null, // Force a brand new device registration.
603 // We force the re-encryption of the send tab keys using the new sync key after the password change
604 encryptedSendTabKeys: null,
606 for (let name of Object.keys(credentials)) {
610 lazy.FxAccountsStorageManagerCanStoreField(name)
612 newCredentials[name] = credentials[name];
614 log.info("changePassword ignoring unsupported field", name);
617 await this._fxAccounts._internal.updateUserAccountData(newCredentials);
618 await this._fxAccounts._internal.updateDeviceRegistration();
622 * Get the hash of account name of the previously signed in account
624 getPreviousAccountNameHashPref() {
626 return Services.prefs.getStringPref(PREF_LAST_FXA_USER);
633 * Given an account name, set the hash of the previously signed in account
635 * @param acctName the account name of the user's account.
637 setPreviousAccountNameHashPref(acctName) {
638 Services.prefs.setStringPref(
640 lazy.CryptoUtils.sha256Base64(acctName)
645 * Open Sync Preferences in the current tab of the browser
647 * @param {Object} browser the browser in which to open preferences
648 * @param {String} [entryPoint] entryPoint to use for logging
650 openSyncPreferences(browser, entryPoint) {
651 let uri = "about:preferences";
653 uri += "?entrypoint=" + encodeURIComponent(entryPoint);
657 browser.loadURI(Services.io.newURI(uri), {
658 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
663 * Open Firefox View in the browser's window
665 * @param {Object} browser the browser in whose window we'll open Firefox View
666 * @param {String} [entryPoint] entryPoint Optional string to use for logging
668 openFirefoxView(browser, entryPoint) {
669 browser.ownerGlobal.FirefoxViewHandler.openTab(entryPoint);
673 * If a user signs in using a different account, the data from the
674 * previous account and the new account will be merged. Ask the user
675 * if they want to continue.
679 _needRelinkWarning(acctName) {
680 let prevAcctHash = this.getPreviousAccountNameHashPref();
682 prevAcctHash && prevAcctHash != lazy.CryptoUtils.sha256Base64(acctName)
687 * Show the user a warning dialog that the data from the previous account
688 * and the new account will be merged.
692 _promptForRelink(acctName) {
693 let sb = Services.strings.createBundle(
694 "chrome://browser/locale/syncSetup.properties"
696 let continueLabel = sb.GetStringFromName("continue.label");
697 let title = sb.GetStringFromName("relinkVerify.title");
698 let description = sb.formatStringFromName("relinkVerify.description", [
702 sb.GetStringFromName("relinkVerify.heading") + "\n\n" + description;
703 let ps = Services.prompt;
705 ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
706 ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
707 ps.BUTTON_POS_1_DEFAULT;
709 // If running in context of the browser chrome, window does not exist.
710 let pressed = Services.prompt.confirmEx(
721 return pressed === 0; // 0 is the "continue" button
727 // The entry-point for this module, which ensures only one of our channels is
728 // ever created - we require this because the WebChannel is global in scope
729 // (eg, it uses the observer service to tell interested parties of interesting
730 // things) and allowing multiple channels would cause such notifications to be
731 // sent multiple times.
732 export var EnsureFxAccountsWebChannel = () => {
733 let contentUri = Services.urlFormatter.formatURLPref(
734 "identity.fxaccounts.remote.root"
736 if (singleton && singleton._contentUri !== contentUri) {
737 singleton.tearDown();
743 // The FxAccountsWebChannel listens for events and updates
744 // the state machine accordingly.
745 singleton = new FxAccountsWebChannel({
746 content_uri: contentUri,
747 channel_id: WEBCHANNEL_ID,
750 log.warn("FxA WebChannel functionaly is disabled due to no URI pref.");
753 log.error("Failed to create FxA WebChannel", ex);