Source code

Revision control

Other Tools

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/. */
4
"use strict";
5
6
/**
7
* Firefox Accounts Web Channel.
8
*
9
* Uses the WebChannel component to receive messages
10
* about account state changes.
11
*/
12
13
var EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
14
15
const { XPCOMUtils } = ChromeUtils.import(
17
);
18
const {
19
COMMAND_PROFILE_CHANGE,
20
COMMAND_LOGIN,
21
COMMAND_LOGOUT,
22
COMMAND_DELETE,
23
COMMAND_CAN_LINK_ACCOUNT,
24
COMMAND_SYNC_PREFERENCES,
25
COMMAND_CHANGE_PASSWORD,
26
COMMAND_FXA_STATUS,
27
COMMAND_PAIR_HEARTBEAT,
28
COMMAND_PAIR_SUPP_METADATA,
29
COMMAND_PAIR_AUTHORIZE,
30
COMMAND_PAIR_DECLINE,
31
COMMAND_PAIR_COMPLETE,
32
COMMAND_PAIR_PREFERENCES,
33
ON_PROFILE_CHANGE_NOTIFICATION,
34
PREF_LAST_FXA_USER,
35
WEBCHANNEL_ID,
36
log,
37
logPII,
38
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
39
40
ChromeUtils.defineModuleGetter(
41
this,
42
"Services",
44
);
45
ChromeUtils.defineModuleGetter(
46
this,
47
"WebChannel",
49
);
50
ChromeUtils.defineModuleGetter(
51
this,
52
"fxAccounts",
54
);
55
ChromeUtils.defineModuleGetter(
56
this,
57
"FxAccountsStorageManagerCanStoreField",
59
);
60
ChromeUtils.defineModuleGetter(
61
this,
62
"PrivateBrowsingUtils",
64
);
65
ChromeUtils.defineModuleGetter(
66
this,
67
"Weave",
69
);
70
ChromeUtils.defineModuleGetter(
71
this,
72
"CryptoUtils",
74
);
75
ChromeUtils.defineModuleGetter(
76
this,
77
"FxAccountsPairingFlow",
79
);
80
XPCOMUtils.defineLazyPreferenceGetter(
81
this,
82
"pairingEnabled",
83
"identity.fxaccounts.pairing.enabled"
84
);
85
XPCOMUtils.defineLazyPreferenceGetter(
86
this,
87
"separatePrivilegedMozillaWebContentProcess",
88
"browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
89
false
90
);
91
XPCOMUtils.defineLazyPreferenceGetter(
92
this,
93
"separatedMozillaDomains",
94
"browser.tabs.remote.separatedMozillaDomains",
95
false,
96
false,
97
val => val.split(",")
98
);
99
XPCOMUtils.defineLazyPreferenceGetter(
100
this,
101
"accountServer",
102
"identity.fxaccounts.remote.root",
103
false,
104
false,
105
val => Services.io.newURI(val)
106
);
107
108
// These engines were added years after Sync had been introduced, they need
109
// special handling since they are system add-ons and are un-available on
110
// older versions of Firefox.
111
const EXTRA_ENGINES = ["addresses", "creditcards"];
112
113
/**
114
* A helper function that extracts the message and stack from an error object.
115
* Returns a `{ message, stack }` tuple. `stack` will be null if the error
116
* doesn't have a stack trace.
117
*/
118
function getErrorDetails(error) {
119
let details = { message: String(error), stack: null };
120
121
// Adapted from Console.jsm.
122
if (error.stack) {
123
let frames = [];
124
for (let frame = error.stack; frame; frame = frame.caller) {
125
frames.push(String(frame).padStart(4));
126
}
127
details.stack = frames.join("\n");
128
}
129
130
return details;
131
}
132
133
/**
134
* Create a new FxAccountsWebChannel to listen for account updates
135
*
136
* @param {Object} options Options
137
* @param {Object} options
138
* @param {String} options.content_uri
139
* The FxA Content server uri
140
* @param {String} options.channel_id
141
* The ID of the WebChannel
142
* @param {String} options.helpers
143
* Helpers functions. Should only be passed in for testing.
144
* @constructor
145
*/
146
this.FxAccountsWebChannel = function(options) {
147
if (!options) {
148
throw new Error("Missing configuration options");
149
}
150
if (!options.content_uri) {
151
throw new Error("Missing 'content_uri' option");
152
}
153
this._contentUri = options.content_uri;
154
155
if (!options.channel_id) {
156
throw new Error("Missing 'channel_id' option");
157
}
158
this._webChannelId = options.channel_id;
159
160
// options.helpers is only specified by tests.
161
XPCOMUtils.defineLazyGetter(this, "_helpers", () => {
162
return options.helpers || new FxAccountsWebChannelHelpers(options);
163
});
164
165
this._setupChannel();
166
};
167
168
this.FxAccountsWebChannel.prototype = {
169
/**
170
* WebChannel that is used to communicate with content page
171
*/
172
_channel: null,
173
174
/**
175
* Helpers interface that does the heavy lifting.
176
*/
177
_helpers: null,
178
179
/**
180
* WebChannel ID.
181
*/
182
_webChannelId: null,
183
/**
184
* WebChannel origin, used to validate origin of messages
185
*/
186
_webChannelOrigin: null,
187
188
/**
189
* Release all resources that are in use.
190
*/
191
tearDown() {
192
this._channel.stopListening();
193
this._channel = null;
194
this._channelCallback = null;
195
},
196
197
/**
198
* Configures and registers a new WebChannel
199
*
200
* @private
201
*/
202
_setupChannel() {
203
// if this.contentUri is present but not a valid URI, then this will throw an error.
204
try {
205
this._webChannelOrigin = Services.io.newURI(this._contentUri);
206
this._registerChannel();
207
} catch (e) {
208
log.error(e);
209
throw e;
210
}
211
},
212
213
_receiveMessage(message, sendingContext) {
214
const { command, data } = message;
215
216
let shouldCheckRemoteType =
217
separatePrivilegedMozillaWebContentProcess &&
218
separatedMozillaDomains.some(function(val) {
219
return (
220
accountServer.asciiHost == val ||
221
accountServer.asciiHost.endsWith("." + val)
222
);
223
});
224
if (
225
shouldCheckRemoteType &&
226
sendingContext.browser.remoteType != "privilegedmozilla"
227
) {
228
log.error(
229
"Rejected FxA webchannel message from remoteType = " +
230
sendingContext.browser.remoteType
231
);
232
return;
233
}
234
235
switch (command) {
236
case COMMAND_PROFILE_CHANGE:
237
Services.obs.notifyObservers(
238
null,
239
ON_PROFILE_CHANGE_NOTIFICATION,
240
data.uid
241
);
242
break;
243
case COMMAND_LOGIN:
244
this._helpers
245
.login(data)
246
.catch(error => this._sendError(error, message, sendingContext));
247
break;
248
case COMMAND_LOGOUT:
249
case COMMAND_DELETE:
250
this._helpers
251
.logout(data.uid)
252
.catch(error => this._sendError(error, message, sendingContext));
253
break;
254
case COMMAND_CAN_LINK_ACCOUNT:
255
let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
256
257
let response = {
258
command,
259
messageId: message.messageId,
260
data: { ok: canLinkAccount },
261
};
262
263
log.debug("FxAccountsWebChannel response", response);
264
this._channel.send(response, sendingContext);
265
break;
266
case COMMAND_SYNC_PREFERENCES:
267
this._helpers.openSyncPreferences(
268
sendingContext.browser,
269
data.entryPoint
270
);
271
break;
272
case COMMAND_PAIR_PREFERENCES:
273
if (pairingEnabled) {
274
sendingContext.browser.loadURI("about:preferences?action=pair#sync", {
275
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
276
});
277
}
278
break;
279
case COMMAND_CHANGE_PASSWORD:
280
this._helpers
281
.changePassword(data)
282
.catch(error => this._sendError(error, message, sendingContext));
283
break;
284
case COMMAND_FXA_STATUS:
285
log.debug("fxa_status received");
286
287
const service = data && data.service;
288
const isPairing = data && data.isPairing;
289
this._helpers
290
.getFxaStatus(service, sendingContext, isPairing)
291
.then(fxaStatus => {
292
let response = {
293
command,
294
messageId: message.messageId,
295
data: fxaStatus,
296
};
297
this._channel.send(response, sendingContext);
298
})
299
.catch(error => this._sendError(error, message, sendingContext));
300
break;
301
case COMMAND_PAIR_HEARTBEAT:
302
case COMMAND_PAIR_SUPP_METADATA:
303
case COMMAND_PAIR_AUTHORIZE:
304
case COMMAND_PAIR_DECLINE:
305
case COMMAND_PAIR_COMPLETE:
306
log.debug(`Pairing command ${command} received`);
307
const { channel_id: channelId } = data;
308
delete data.channel_id;
309
const flow = FxAccountsPairingFlow.get(channelId);
310
if (!flow) {
311
log.warn(`Could not find a pairing flow for ${channelId}`);
312
return;
313
}
314
flow.onWebChannelMessage(command, data).then(replyData => {
315
this._channel.send(
316
{
317
command,
318
messageId: message.messageId,
319
data: replyData,
320
},
321
sendingContext
322
);
323
});
324
break;
325
default:
326
log.warn("Unrecognized FxAccountsWebChannel command", command);
327
// As a safety measure we also terminate any pending FxA pairing flow.
328
FxAccountsPairingFlow.finalizeAll();
329
break;
330
}
331
},
332
333
_sendError(error, incomingMessage, sendingContext) {
334
log.error("Failed to handle FxAccountsWebChannel message", error);
335
this._channel.send(
336
{
337
command: incomingMessage.command,
338
messageId: incomingMessage.messageId,
339
data: {
340
error: getErrorDetails(error),
341
},
342
},
343
sendingContext
344
);
345
},
346
347
/**
348
* Create a new channel with the WebChannelBroker, setup a callback listener
349
* @private
350
*/
351
_registerChannel() {
352
/**
353
* Processes messages that are called back from the FxAccountsChannel
354
*
355
* @param webChannelId {String}
356
* Command webChannelId
357
* @param message {Object}
358
* Command message
359
* @param sendingContext {Object}
360
* Message sending context.
361
* @param sendingContext.browser {browser}
362
* The <browser> object that captured the
363
* WebChannelMessageToChrome.
364
* @param sendingContext.eventTarget {EventTarget}
365
* The <EventTarget> where the message was sent.
366
* @param sendingContext.principal {Principal}
367
* The <Principal> of the EventTarget where the message was sent.
368
* @private
369
*
370
*/
371
let listener = (webChannelId, message, sendingContext) => {
372
if (message) {
373
log.debug("FxAccountsWebChannel message received", message.command);
374
if (logPII) {
375
log.debug("FxAccountsWebChannel message details", message);
376
}
377
try {
378
this._receiveMessage(message, sendingContext);
379
} catch (error) {
380
this._sendError(error, message, sendingContext);
381
}
382
}
383
};
384
385
this._channelCallback = listener;
386
this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
387
this._channel.listen(listener);
388
log.debug(
389
"FxAccountsWebChannel registered: " +
390
this._webChannelId +
391
" with origin " +
392
this._webChannelOrigin.prePath
393
);
394
},
395
};
396
397
this.FxAccountsWebChannelHelpers = function(options) {
398
options = options || {};
399
400
this._fxAccounts = options.fxAccounts || fxAccounts;
401
this._privateBrowsingUtils =
402
options.privateBrowsingUtils || PrivateBrowsingUtils;
403
};
404
405
this.FxAccountsWebChannelHelpers.prototype = {
406
// If the last fxa account used for sync isn't this account, we display
407
// a modal dialog checking they really really want to do this...
408
// (This is sync-specific, so ideally would be in sync's identity module,
409
// but it's a little more seamless to do here, and sync is currently the
410
// only fxa consumer, so...
411
shouldAllowRelink(acctName) {
412
return (
413
!this._needRelinkWarning(acctName) || this._promptForRelink(acctName)
414
);
415
},
416
417
/**
418
* stores sync login info it in the fxaccounts service
419
*
420
* @param accountData the user's account data and credentials
421
*/
422
login(accountData) {
423
// We don't act on customizeSync anymore, it used to open a dialog inside
424
// the browser to selecte the engines to sync but we do it on the web now.
425
delete accountData.customizeSync;
426
427
if (accountData.offeredSyncEngines) {
428
EXTRA_ENGINES.forEach(engine => {
429
if (
430
accountData.offeredSyncEngines.includes(engine) &&
431
!accountData.declinedSyncEngines.includes(engine)
432
) {
433
// These extra engines are disabled by default.
434
Services.prefs.setBoolPref(`services.sync.engine.${engine}`, true);
435
}
436
});
437
delete accountData.offeredSyncEngines;
438
}
439
440
if (accountData.declinedSyncEngines) {
441
let declinedSyncEngines = accountData.declinedSyncEngines;
442
log.debug("Received declined engines", declinedSyncEngines);
443
Weave.Service.engineManager.setDeclined(declinedSyncEngines);
444
declinedSyncEngines.forEach(engine => {
445
Services.prefs.setBoolPref("services.sync.engine." + engine, false);
446
});
447
delete accountData.declinedSyncEngines;
448
}
449
450
// the user has already been shown the "can link account"
451
// screen. No need to keep this data around.
452
delete accountData.verifiedCanLinkAccount;
453
454
// Remember who it was so we can log out next time.
455
this.setPreviousAccountNameHashPref(accountData.email);
456
457
// A sync-specific hack - we want to ensure sync has been initialized
458
// before we set the signed-in user.
459
let xps = Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
460
.wrappedJSObject;
461
return xps.whenLoaded().then(() => {
462
return this._fxAccounts._internal.setSignedInUser(accountData);
463
});
464
},
465
466
/**
467
* logout the fxaccounts service
468
*
469
* @param the uid of the account which have been logged out
470
*/
471
logout(uid) {
472
return fxAccounts.getSignedInUser().then(userData => {
473
if (userData && userData.uid === uid) {
474
// true argument is `localOnly`, because server-side stuff
475
// has already been taken care of by the content server
476
return fxAccounts.signOut(true);
477
}
478
return null;
479
});
480
},
481
482
/**
483
* Check if `sendingContext` is in private browsing mode.
484
*/
485
isPrivateBrowsingMode(sendingContext) {
486
if (!sendingContext) {
487
log.error("Unable to check for private browsing mode, assuming true");
488
return true;
489
}
490
491
const isPrivateBrowsing = this._privateBrowsingUtils.isBrowserPrivate(
492
sendingContext.browser
493
);
494
log.debug("is private browsing", isPrivateBrowsing);
495
return isPrivateBrowsing;
496
},
497
498
/**
499
* Check whether sending fxa_status data should be allowed.
500
*/
501
shouldAllowFxaStatus(service, sendingContext, isPairing) {
502
// Return user data for any service in non-PB mode. In PB mode,
503
// only return user data if service==="sync" or is in pairing mode
504
// (as service will be equal to the OAuth client ID and not "sync").
505
//
506
// This behaviour allows users to click the "Manage Account"
507
// link from about:preferences#sync while in PB mode and things
508
// "just work". While in non-PB mode, users can sign into
509
// Pocket w/o entering their password a 2nd time, while in PB
510
// mode they *will* have to enter their email/password again.
511
//
512
// The difference in behaviour is to try to match user
513
// expectations as to what is and what isn't part of the browser.
514
// Sync is viewed as an integral part of the browser, interacting
515
// with FxA as part of a Sync flow should work all the time. If
516
// Sync is broken in PB mode, users will think Firefox is broken.
518
log.debug("service", service);
519
return (
520
!this.isPrivateBrowsingMode(sendingContext) ||
521
service === "sync" ||
522
isPairing
523
);
524
},
525
526
/**
527
* Get fxa_status information. Resolves to { signedInUser: <user_data> }.
528
* If returning status information is not allowed or no user is signed into
529
* Sync, `user_data` will be null.
530
*/
531
async getFxaStatus(service, sendingContext, isPairing) {
532
let signedInUser = null;
533
534
if (this.shouldAllowFxaStatus(service, sendingContext, isPairing)) {
535
const userData = await this._fxAccounts.getSignedInUser();
536
if (userData) {
537
signedInUser = {
538
email: userData.email,
539
sessionToken: userData.sessionToken,
540
uid: userData.uid,
541
verified: userData.verified,
542
};
543
}
544
}
545
546
return {
547
signedInUser,
548
capabilities: {
549
pairing: pairingEnabled,
550
engines: this._getAvailableExtraEngines(),
551
},
552
};
553
},
554
555
_getAvailableExtraEngines() {
556
return EXTRA_ENGINES.filter(engineName => {
557
try {
558
return Services.prefs.getBoolPref(
559
`services.sync.engine.${engineName}.available`
560
);
561
} catch (e) {
562
return false;
563
}
564
});
565
},
566
567
async changePassword(credentials) {
568
// If |credentials| has fields that aren't handled by accounts storage,
569
// updateUserAccountData will throw - mainly to prevent errors in code
570
// that hard-codes field names.
571
// However, in this case the field names aren't really in our control.
572
// We *could* still insist the server know what fields names are valid,
573
// but that makes life difficult for the server when Firefox adds new
574
// features (ie, new fields) - forcing the server to track a map of
575
// versions to supported field names doesn't buy us much.
576
// So we just remove field names we know aren't handled.
577
let newCredentials = {
578
device: null, // Force a brand new device registration.
579
};
580
for (let name of Object.keys(credentials)) {
581
if (
582
name == "email" ||
583
name == "uid" ||
584
FxAccountsStorageManagerCanStoreField(name)
585
) {
586
newCredentials[name] = credentials[name];
587
} else {
588
log.info("changePassword ignoring unsupported field", name);
589
}
590
}
591
await this._fxAccounts._internal.updateUserAccountData(newCredentials);
592
// Force the keys derivation, to be able to register a send-tab command
593
// in updateDeviceRegistration (but it's not clear we really do need to
594
// force keys here - see bug 1580398 for more)
595
try {
596
await this._fxAccounts.keys.getKeys();
597
} catch (e) {
598
log.error("getKeys errored", e);
599
}
600
await this._fxAccounts._internal.updateDeviceRegistration();
601
},
602
603
/**
604
* Get the hash of account name of the previously signed in account
605
*/
606
getPreviousAccountNameHashPref() {
607
try {
608
return Services.prefs.getStringPref(PREF_LAST_FXA_USER);
609
} catch (_) {
610
return "";
611
}
612
},
613
614
/**
615
* Given an account name, set the hash of the previously signed in account
616
*
617
* @param acctName the account name of the user's account.
618
*/
619
setPreviousAccountNameHashPref(acctName) {
620
Services.prefs.setStringPref(
621
PREF_LAST_FXA_USER,
622
CryptoUtils.sha256Base64(acctName)
623
);
624
},
625
626
/**
627
* Open Sync Preferences in the current tab of the browser
628
*
629
* @param {Object} browser the browser in which to open preferences
630
* @param {String} [entryPoint] entryPoint to use for logging
631
*/
632
openSyncPreferences(browser, entryPoint) {
633
let uri = "about:preferences";
634
if (entryPoint) {
635
uri += "?entrypoint=" + encodeURIComponent(entryPoint);
636
}
637
uri += "#sync";
638
639
browser.loadURI(uri, {
640
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
641
});
642
},
643
644
/**
645
* If a user signs in using a different account, the data from the
646
* previous account and the new account will be merged. Ask the user
647
* if they want to continue.
648
*
649
* @private
650
*/
651
_needRelinkWarning(acctName) {
652
let prevAcctHash = this.getPreviousAccountNameHashPref();
653
return prevAcctHash && prevAcctHash != CryptoUtils.sha256Base64(acctName);
654
},
655
656
/**
657
* Show the user a warning dialog that the data from the previous account
658
* and the new account will be merged.
659
*
660
* @private
661
*/
662
_promptForRelink(acctName) {
663
let sb = Services.strings.createBundle(
665
);
666
let continueLabel = sb.GetStringFromName("continue.label");
667
let title = sb.GetStringFromName("relinkVerify.title");
668
let description = sb.formatStringFromName("relinkVerify.description", [
669
acctName,
670
]);
671
let body =
672
sb.GetStringFromName("relinkVerify.heading") + "\n\n" + description;
673
let ps = Services.prompt;
674
let buttonFlags =
675
ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
676
ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
677
ps.BUTTON_POS_1_DEFAULT;
678
679
// If running in context of the browser chrome, window does not exist.
680
let pressed = Services.prompt.confirmEx(
681
null,
682
title,
683
body,
684
buttonFlags,
685
continueLabel,
686
null,
687
null,
688
null,
689
{}
690
);
691
return pressed === 0; // 0 is the "continue" button
692
},
693
};
694
695
var singleton;
696
// The entry-point for this module, which ensures only one of our channels is
697
// ever created - we require this because the WebChannel is global in scope
698
// (eg, it uses the observer service to tell interested parties of interesting
699
// things) and allowing multiple channels would cause such notifications to be
700
// sent multiple times.
701
var EnsureFxAccountsWebChannel = () => {
702
let contentUri = Services.urlFormatter.formatURLPref(
703
"identity.fxaccounts.remote.root"
704
);
705
if (singleton && singleton._contentUri !== contentUri) {
706
singleton.tearDown();
707
singleton = null;
708
}
709
if (!singleton) {
710
try {
711
if (contentUri) {
712
// The FxAccountsWebChannel listens for events and updates
713
// the state machine accordingly.
714
singleton = new this.FxAccountsWebChannel({
715
content_uri: contentUri,
716
channel_id: WEBCHANNEL_ID,
717
});
718
} else {
719
log.warn("FxA WebChannel functionaly is disabled due to no URI pref.");
720
}
721
} catch (ex) {
722
log.error("Failed to create FxA WebChannel", ex);
723
}
724
}
725
};