Bug 1859954 - Use XP_DARWIN rather than XP_MACOS in PHC r=glandium
[gecko.git] / services / fxaccounts / FxAccounts.sys.mjs
blob07b2b331737fa82de1f3cb6f607f12b8bbc07b0c
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/. */
5 import { PromiseUtils } from "resource://gre/modules/PromiseUtils.sys.mjs";
7 import { CryptoUtils } from "resource://services-crypto/utils.sys.mjs";
8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
11 import { FxAccountsStorageManager } from "resource://gre/modules/FxAccountsStorage.sys.mjs";
13 import {
14   ERRNO_INVALID_AUTH_TOKEN,
15   ERROR_AUTH_ERROR,
16   ERROR_INVALID_PARAMETER,
17   ERROR_NO_ACCOUNT,
18   ERROR_TO_GENERAL_ERROR_CLASS,
19   ERROR_UNKNOWN,
20   ERROR_UNVERIFIED_ACCOUNT,
21   FXA_PWDMGR_PLAINTEXT_FIELDS,
22   FXA_PWDMGR_REAUTH_ALLOWLIST,
23   FXA_PWDMGR_SECURE_FIELDS,
24   FX_OAUTH_CLIENT_ID,
25   ON_ACCOUNT_STATE_CHANGE_NOTIFICATION,
26   ONLOGIN_NOTIFICATION,
27   ONLOGOUT_NOTIFICATION,
28   ON_PRELOGOUT_NOTIFICATION,
29   ONVERIFIED_NOTIFICATION,
30   ON_DEVICE_DISCONNECTED_NOTIFICATION,
31   POLL_SESSION,
32   PREF_ACCOUNT_ROOT,
33   PREF_LAST_FXA_USER,
34   SERVER_ERRNO_TO_ERROR,
35   log,
36   logPII,
37   logManager,
38 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
40 const lazy = {};
42 ChromeUtils.defineESModuleGetters(lazy, {
43   FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs",
44   FxAccountsCommands: "resource://gre/modules/FxAccountsCommands.sys.mjs",
45   FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs",
46   FxAccountsDevice: "resource://gre/modules/FxAccountsDevice.sys.mjs",
47   FxAccountsKeys: "resource://gre/modules/FxAccountsKeys.sys.mjs",
48   FxAccountsProfile: "resource://gre/modules/FxAccountsProfile.sys.mjs",
49   FxAccountsTelemetry: "resource://gre/modules/FxAccountsTelemetry.sys.mjs",
50 });
52 ChromeUtils.defineLazyGetter(lazy, "mpLocked", () => {
53   return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
54     .Utils.mpLocked;
55 });
57 ChromeUtils.defineLazyGetter(lazy, "ensureMPUnlocked", () => {
58   return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
59     .Utils.ensureMPUnlocked;
60 });
62 XPCOMUtils.defineLazyPreferenceGetter(
63   lazy,
64   "FXA_ENABLED",
65   "identity.fxaccounts.enabled",
66   true
69 // An AccountState object holds all state related to one specific account.
70 // It is considered "private" to the FxAccounts modules.
71 // Only one AccountState is ever "current" in the FxAccountsInternal object -
72 // whenever a user logs out or logs in, the current AccountState is discarded,
73 // making it impossible for the wrong state or state data to be accidentally
74 // used.
75 // In addition, it has some promise-related helpers to ensure that if an
76 // attempt is made to resolve a promise on a "stale" state (eg, if an
77 // operation starts, but a different user logs in before the operation
78 // completes), the promise will be rejected.
79 // It is intended to be used thusly:
80 // somePromiseBasedFunction: function() {
81 //   let currentState = this.currentAccountState;
82 //   return someOtherPromiseFunction().then(
83 //     data => currentState.resolve(data)
84 //   );
85 // }
86 // If the state has changed between the function being called and the promise
87 // being resolved, the .resolve() call will actually be rejected.
88 export function AccountState(storageManager) {
89   this.storageManager = storageManager;
90   this.inFlightTokenRequests = new Map();
91   this.promiseInitialized = this.storageManager
92     .getAccountData()
93     .then(data => {
94       this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {};
95     })
96     .catch(err => {
97       log.error("Failed to initialize the storage manager", err);
98       // Things are going to fall apart, but not much we can do about it here.
99     });
102 AccountState.prototype = {
103   oauthTokens: null,
104   whenVerifiedDeferred: null,
105   whenKeysReadyDeferred: null,
107   // If the storage manager has been nuked then we are no longer current.
108   get isCurrent() {
109     return this.storageManager != null;
110   },
112   abort() {
113     if (this.whenVerifiedDeferred) {
114       this.whenVerifiedDeferred.reject(
115         new Error("Verification aborted; Another user signing in")
116       );
117       this.whenVerifiedDeferred = null;
118     }
119     if (this.whenKeysReadyDeferred) {
120       this.whenKeysReadyDeferred.reject(
121         new Error("Key fetching aborted; Another user signing in")
122       );
123       this.whenKeysReadyDeferred = null;
124     }
125     this.inFlightTokenRequests.clear();
126     return this.signOut();
127   },
129   // Clobber all cached data and write that empty data to storage.
130   async signOut() {
131     this.cert = null;
132     this.keyPair = null;
133     this.oauthTokens = null;
134     this.inFlightTokenRequests.clear();
136     // Avoid finalizing the storageManager multiple times (ie, .signOut()
137     // followed by .abort())
138     if (!this.storageManager) {
139       return;
140     }
141     const storageManager = this.storageManager;
142     this.storageManager = null;
144     await storageManager.deleteAccountData();
145     await storageManager.finalize();
146   },
148   // Get user account data. Optionally specify explicit field names to fetch
149   // (and note that if you require an in-memory field you *must* specify the
150   // field name(s).)
151   getUserAccountData(fieldNames = null) {
152     if (!this.isCurrent) {
153       return Promise.reject(new Error("Another user has signed in"));
154     }
155     return this.storageManager.getAccountData(fieldNames).then(result => {
156       return this.resolve(result);
157     });
158   },
160   async updateUserAccountData(updatedFields) {
161     if ("uid" in updatedFields) {
162       const existing = await this.getUserAccountData(["uid"]);
163       if (existing.uid != updatedFields.uid) {
164         throw new Error(
165           "The specified credentials aren't for the current user"
166         );
167       }
168       // We need to nuke uid as storage will complain if we try and
169       // update it (even when the value is the same)
170       updatedFields = Cu.cloneInto(updatedFields, {}); // clone it first
171       delete updatedFields.uid;
172     }
173     if (!this.isCurrent) {
174       return Promise.reject(new Error("Another user has signed in"));
175     }
176     return this.storageManager.updateAccountData(updatedFields);
177   },
179   resolve(result) {
180     if (!this.isCurrent) {
181       log.info(
182         "An accountState promise was resolved, but was actually rejected" +
183           " due to a different user being signed in. Originally resolved" +
184           " with",
185         result
186       );
187       return Promise.reject(new Error("A different user signed in"));
188     }
189     return Promise.resolve(result);
190   },
192   reject(error) {
193     // It could be argued that we should just let it reject with the original
194     // error - but this runs the risk of the error being (eg) a 401, which
195     // might cause the consumer to attempt some remediation and cause other
196     // problems.
197     if (!this.isCurrent) {
198       log.info(
199         "An accountState promise was rejected, but we are ignoring that " +
200           "reason and rejecting it due to a different user being signed in. " +
201           "Originally rejected with",
202         error
203       );
204       return Promise.reject(new Error("A different user signed in"));
205     }
206     return Promise.reject(error);
207   },
209   // Abstractions for storage of cached tokens - these are all sync, and don't
210   // handle revocation etc - it's just storage (and the storage itself is async,
211   // but we don't return the storage promises, so it *looks* sync)
212   // These functions are sync simply so we can handle "token races" - when there
213   // are multiple in-flight requests for the same scope, we can detect this
214   // and revoke the redundant token.
216   // A preamble for the cache helpers...
217   _cachePreamble() {
218     if (!this.isCurrent) {
219       throw new Error("Another user has signed in");
220     }
221   },
223   // Set a cached token. |tokenData| must have a 'token' element, but may also
224   // have additional fields.
225   // The 'get' functions below return the entire |tokenData| value.
226   setCachedToken(scopeArray, tokenData) {
227     this._cachePreamble();
228     if (!tokenData.token) {
229       throw new Error("No token");
230     }
231     let key = getScopeKey(scopeArray);
232     this.oauthTokens[key] = tokenData;
233     // And a background save...
234     this._persistCachedTokens();
235   },
237   // Return data for a cached token or null (or throws on bad state etc)
238   getCachedToken(scopeArray) {
239     this._cachePreamble();
240     let key = getScopeKey(scopeArray);
241     let result = this.oauthTokens[key];
242     if (result) {
243       // later we might want to check an expiry date - but we currently
244       // have no such concept, so just return it.
245       log.trace("getCachedToken returning cached token");
246       return result;
247     }
248     return null;
249   },
251   // Remove a cached token from the cache.  Does *not* revoke it from anywhere.
252   // Returns the entire token entry if found, null otherwise.
253   removeCachedToken(token) {
254     this._cachePreamble();
255     let data = this.oauthTokens;
256     for (let [key, tokenValue] of Object.entries(data)) {
257       if (tokenValue.token == token) {
258         delete data[key];
259         // And a background save...
260         this._persistCachedTokens();
261         return tokenValue;
262       }
263     }
264     return null;
265   },
267   // A hook-point for tests.  Returns a promise that's ignored in most cases
268   // (notable exceptions are tests and when we explicitly are saving the entire
269   // set of user data.)
270   _persistCachedTokens() {
271     this._cachePreamble();
272     return this.updateUserAccountData({ oauthTokens: this.oauthTokens }).catch(
273       err => {
274         log.error("Failed to update cached tokens", err);
275       }
276     );
277   },
280 /* Given an array of scopes, make a string key by normalizing. */
281 function getScopeKey(scopeArray) {
282   let normalizedScopes = scopeArray.map(item => item.toLowerCase());
283   return normalizedScopes.sort().join("|");
286 function getPropertyDescriptor(obj, prop) {
287   return (
288     Object.getOwnPropertyDescriptor(obj, prop) ||
289     getPropertyDescriptor(Object.getPrototypeOf(obj), prop)
290   );
294  * Copies properties from a given object to another object.
296  * @param from (object)
297  *        The object we read property descriptors from.
298  * @param to (object)
299  *        The object that we set property descriptors on.
300  * @param thisObj (object)
301  *        The object that will be used to .bind() all function properties we find to.
302  * @param keys ([...])
303  *        The names of all properties to be copied.
304  */
305 function copyObjectProperties(from, to, thisObj, keys) {
306   for (let prop of keys) {
307     // Look for the prop in the prototype chain.
308     let desc = getPropertyDescriptor(from, prop);
310     if (typeof desc.value == "function") {
311       desc.value = desc.value.bind(thisObj);
312     }
314     if (desc.get) {
315       desc.get = desc.get.bind(thisObj);
316     }
318     if (desc.set) {
319       desc.set = desc.set.bind(thisObj);
320     }
322     Object.defineProperty(to, prop, desc);
323   }
327  * The public API.
329  * TODO - *all* non-underscore stuff here should have sphinx docstrings so
330  * that docs magically appear on https://firefox-source-docs.mozilla.org/
331  * (although |./mach doc| is broken on windows (bug 1232403) and on Linux for
332  * markh (some obscure npm issue he gave up on) - so later...)
333  */
334 export class FxAccounts {
335   constructor(mocks = null) {
336     this._internal = new FxAccountsInternal();
337     if (mocks) {
338       // it's slightly unfortunate that we need to mock the main "internal" object
339       // before calling initialize, primarily so a mock `newAccountState` is in
340       // place before initialize calls it, but we need to initialize the
341       // "sub-object" mocks after. This can probably be fixed, but whatever...
342       copyObjectProperties(
343         mocks,
344         this._internal,
345         this._internal,
346         Object.keys(mocks).filter(key => !["device", "commands"].includes(key))
347       );
348     }
349     this._internal.initialize();
350     // allow mocking our "sub-objects" too.
351     if (mocks) {
352       for (let subobject of [
353         "currentAccountState",
354         "keys",
355         "fxaPushService",
356         "device",
357         "commands",
358       ]) {
359         if (typeof mocks[subobject] == "object") {
360           copyObjectProperties(
361             mocks[subobject],
362             this._internal[subobject],
363             this._internal[subobject],
364             Object.keys(mocks[subobject])
365           );
366         }
367       }
368     }
369   }
371   get commands() {
372     return this._internal.commands;
373   }
375   static get config() {
376     return lazy.FxAccountsConfig;
377   }
379   get device() {
380     return this._internal.device;
381   }
383   get keys() {
384     return this._internal.keys;
385   }
387   get telemetry() {
388     return this._internal.telemetry;
389   }
391   _withCurrentAccountState(func) {
392     return this._internal.withCurrentAccountState(func);
393   }
395   _withVerifiedAccountState(func) {
396     return this._internal.withVerifiedAccountState(func);
397   }
399   _withSessionToken(func, mustBeVerified = true) {
400     return this._internal.withSessionToken(func, mustBeVerified);
401   }
403   /**
404    * Returns an array listing all the OAuth clients connected to the
405    * authenticated user's account. This includes browsers and web sessions - no
406    * filtering is done of the set returned by the FxA server.
407    *
408    * @typedef {Object} AttachedClient
409    * @property {String} id - OAuth `client_id` of the client.
410    * @property {Number} lastAccessedDaysAgo - How many days ago the client last
411    *    accessed the FxA server APIs.
412    *
413    * @returns {Array.<AttachedClient>} A list of attached clients.
414    */
415   async listAttachedOAuthClients() {
416     // We expose last accessed times in 'days ago'
417     const ONE_DAY = 24 * 60 * 60 * 1000;
419     return this._withSessionToken(async sessionToken => {
420       const response = await this._internal.fxAccountsClient.attachedClients(
421         sessionToken
422       );
423       const attachedClients = response.body;
424       const timestamp = response.headers["x-timestamp"];
425       const now =
426         timestamp !== undefined
427           ? new Date(parseInt(timestamp, 10))
428           : Date.now();
429       return attachedClients.map(client => {
430         const daysAgo = client.lastAccessTime
431           ? Math.max(Math.floor((now - client.lastAccessTime) / ONE_DAY), 0)
432           : null;
433         return {
434           id: client.clientId,
435           lastAccessedDaysAgo: daysAgo,
436         };
437       });
438     });
439   }
441   /**
442    * Get an OAuth token for the user.
443    *
444    * @param options
445    *        {
446    *          scope: (string/array) the oauth scope(s) being requested. As a
447    *                 convenience, you may pass a string if only one scope is
448    *                 required, or an array of strings if multiple are needed.
449    *          ttl: (number) OAuth token TTL in seconds.
450    *        }
451    *
452    * @return Promise.<string | Error>
453    *        The promise resolves the oauth token as a string or rejects with
454    *        an error object ({error: ERROR, details: {}}) of the following:
455    *          INVALID_PARAMETER
456    *          NO_ACCOUNT
457    *          UNVERIFIED_ACCOUNT
458    *          NETWORK_ERROR
459    *          AUTH_ERROR
460    *          UNKNOWN_ERROR
461    */
462   async getOAuthToken(options = {}) {
463     try {
464       return await this._internal.getOAuthToken(options);
465     } catch (err) {
466       throw this._internal._errorToErrorClass(err);
467     }
468   }
470   /**
471    * Remove an OAuth token from the token cache. Callers should call this
472    * after they determine a token is invalid, so a new token will be fetched
473    * on the next call to getOAuthToken().
474    *
475    * @param options
476    *        {
477    *          token: (string) A previously fetched token.
478    *        }
479    * @return Promise.<undefined> This function will always resolve, even if
480    *         an unknown token is passed.
481    */
482   removeCachedOAuthToken(options) {
483     return this._internal.removeCachedOAuthToken(options);
484   }
486   /**
487    * Get details about the user currently signed in to Firefox Accounts.
488    *
489    * @return Promise
490    *        The promise resolves to the credentials object of the signed-in user:
491    *        {
492    *          email: String: The user's email address
493    *          uid: String: The user's unique id
494    *          verified: Boolean: email verification status
495    *          displayName: String or null if not known.
496    *          avatar: URL of the avatar for the user. May be the default
497    *                  avatar, or null in edge-cases (eg, if there's an account
498    *                  issue, etc
499    *          avatarDefault: boolean - whether `avatar` is specific to the user
500    *                         or the default avatar.
501    *        }
502    *
503    *        or null if no user is signed in. This function never fails except
504    *        in pathological cases (eg, file-system errors, etc)
505    */
506   getSignedInUser() {
507     // Note we don't return the session token, but use it to see if we
508     // should fetch the profile.
509     const ACCT_DATA_FIELDS = ["email", "uid", "verified", "sessionToken"];
510     const PROFILE_FIELDS = ["displayName", "avatar", "avatarDefault"];
511     return this._withCurrentAccountState(async currentState => {
512       const data = await currentState.getUserAccountData(ACCT_DATA_FIELDS);
513       if (!data) {
514         return null;
515       }
516       if (!lazy.FXA_ENABLED) {
517         await this.signOut();
518         return null;
519       }
520       if (!this._internal.isUserEmailVerified(data)) {
521         // If the email is not verified, start polling for verification,
522         // but return null right away.  We don't want to return a promise
523         // that might not be fulfilled for a long time.
524         this._internal.startVerifiedCheck(data);
525       }
527       let profileData = null;
528       if (data.sessionToken) {
529         delete data.sessionToken;
530         try {
531           profileData = await this._internal.profile.getProfile();
532         } catch (error) {
533           log.error("Could not retrieve profile data", error);
534         }
535       }
536       for (let field of PROFILE_FIELDS) {
537         data[field] = profileData ? profileData[field] : null;
538       }
539       // and email is a special case - if we have profile data we prefer the
540       // email from that, as the email we stored for the account itself might
541       // not have been updated if the email changed since the user signed in.
542       if (profileData && profileData.email) {
543         data.email = profileData.email;
544       }
545       return data;
546     });
547   }
549   /**
550    * Checks the status of the account. Resolves with Promise<boolean>, where
551    * true indicates the account status is OK and false indicates there's some
552    * issue with the account - either that there's no user currently signed in,
553    * the entire account has been deleted (in which case there will be no user
554    * signed in after this call returns), or that the user must reauthenticate (in
555    * which case `this.hasLocalSession()` will return `false` after this call
556    * returns).
557    *
558    * Typically used when some external code which uses, for example, oauth tokens
559    * received a 401 error using the token, or that this external code has some
560    * other reason to believe the account status may be bad. Note that this will
561    * be called automatically in many cases - for example, if calls to fetch the
562    * profile, or fetch keys, etc return a 401, there's no need to call this
563    * function.
564    *
565    * Because this hits the server, you should only call this method when you have
566    * good reason to believe the session very recently became invalid (eg, because
567    * you saw an auth related exception from a remote service.)
568    */
569   checkAccountStatus() {
570     // Note that we don't use _withCurrentAccountState here because that will
571     // cause an exception to be thrown if we end up signing out due to the
572     // account not existing, which isn't what we want here.
573     let state = this._internal.currentAccountState;
574     return this._internal.checkAccountStatus(state);
575   }
577   /**
578    * Checks if we have a valid local session state for the current account.
579    *
580    * @return Promise
581    *        Resolves with a boolean, with true indicating that we appear to
582    *        have a valid local session, or false if we need to reauthenticate
583    *        with the content server to obtain one.
584    *        Note that this only checks local state, although typically that's
585    *        OK, because we drop the local session information whenever we detect
586    *        we are in this state. However, see checkAccountStatus() for a way to
587    *        check the account and session status with the server, which can be
588    *        considered the canonical, albiet expensive, way to determine the
589    *        status of the account.
590    */
591   hasLocalSession() {
592     return this._withCurrentAccountState(async state => {
593       let data = await state.getUserAccountData(["sessionToken"]);
594       return !!(data && data.sessionToken);
595     });
596   }
598   /** Returns a promise that resolves to true if we can currently connect (ie,
599    *  sign in, or re-connect after a password change) to a Firefox Account.
600    *  If this returns false, the caller can assume that some UI was shown
601    *  which tells the user why we could not connect.
602    *
603    *  Currently, the primary password being locked is the only reason why
604    *  this returns false, and in this scenario, the primary password unlock
605    *  dialog will have been shown.
606    *
607    *  This currently doesn't need to return a promise, but does so that
608    *  future enhancements, such as other explanatory UI which requires
609    *  async can work without modification of the call-sites.
610    */
611   static canConnectAccount() {
612     return Promise.resolve(!lazy.mpLocked() || lazy.ensureMPUnlocked());
613   }
615   /**
616    * Send a message to a set of devices in the same account
617    *
618    * @param deviceIds: (null/string/array) The device IDs to send the message to.
619    *                   If null, will be sent to all devices.
620    *
621    * @param excludedIds: (null/string/array) If deviceIds is null, this may
622    *                     list device IDs which should not receive the message.
623    *
624    * @param payload: (object) The payload, which will be JSON.stringified.
625    *
626    * @param TTL: How long the message should be retained before it is discarded.
627    */
628   // XXX - used only by sync to tell other devices that the clients collection
629   // has changed so they should sync asap. The API here is somewhat vague (ie,
630   // "an object"), but to be useful across devices, the payload really needs
631   // formalizing. We should try and do something better here.
632   notifyDevices(deviceIds, excludedIds, payload, TTL) {
633     return this._internal.notifyDevices(deviceIds, excludedIds, payload, TTL);
634   }
636   /**
637    * Resend the verification email for the currently signed-in user.
638    *
639    */
640   resendVerificationEmail() {
641     return this._withSessionToken((token, currentState) => {
642       this._internal.startPollEmailStatus(currentState, token, "start");
643       return this._internal.fxAccountsClient.resendVerificationEmail(token);
644     }, false);
645   }
647   async signOut(localOnly) {
648     // Note that we do not use _withCurrentAccountState here, otherwise we
649     // end up with an exception due to the user signing out before the call is
650     // complete - but that's the entire point of this method :)
651     return this._internal.signOut(localOnly);
652   }
654   // XXX - we should consider killing this - the only reason it is public is
655   // so that sync can change it when it notices the device name being changed,
656   // and that could probably be replaced with a pref observer.
657   updateDeviceRegistration() {
658     return this._withCurrentAccountState(_ => {
659       return this._internal.updateDeviceRegistration();
660     });
661   }
663   // we should try and kill this too.
664   whenVerified(data) {
665     return this._withCurrentAccountState(_ => {
666       return this._internal.whenVerified(data);
667     });
668   }
670   /**
671    * Generate a log file for the FxA action that just completed
672    * and refresh the input & output streams.
673    */
674   async flushLogFile() {
675     const logType = await logManager.resetFileLog();
676     if (logType == logManager.ERROR_LOG_WRITTEN) {
677       console.error(
678         "FxA encountered an error - see about:sync-log for the log file."
679       );
680     }
681     Services.obs.notifyObservers(null, "service:log-manager:flush-log-file");
682   }
685 var FxAccountsInternal = function () {};
688  * The internal API's prototype.
689  */
690 FxAccountsInternal.prototype = {
691   // Make a local copy of this constant so we can mock it in testing
692   POLL_SESSION,
694   // The timeout (in ms) we use to poll for a verified mail for the first
695   // VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD minutes if the user has
696   // logged-in in this session.
697   VERIFICATION_POLL_TIMEOUT_INITIAL: 60000, // 1 minute.
698   // All the other cases (> 5 min, on restart etc).
699   VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 5 * 60000, // 5 minutes.
700   // After X minutes, the polling will slow down to _SUBSEQUENT if we have
701   // logged-in in this session.
702   VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD: 5,
704   _fxAccountsClient: null,
706   // All significant initialization should be done in this initialize() method
707   // to help with our mocking story.
708   initialize() {
709     ChromeUtils.defineLazyGetter(this, "fxaPushService", function () {
710       return Cc["@mozilla.org/fxaccounts/push;1"].getService(
711         Ci.nsISupports
712       ).wrappedJSObject;
713     });
715     this.keys = new lazy.FxAccountsKeys(this);
717     if (!this.observerPreloads) {
718       // A registry of promise-returning functions that `notifyObservers` should
719       // call before sending notifications. Primarily used so parts of Firefox
720       // which have yet to load for performance reasons can be force-loaded, and
721       // thus not miss notifications.
722       this.observerPreloads = [
723         // Sync
724         () => {
725           let { Weave } = ChromeUtils.importESModule(
726             "resource://services-sync/main.sys.mjs"
727           );
728           return Weave.Service.promiseInitialized;
729         },
730       ];
731     }
733     this.currentTimer = null;
734     // This object holds details about, and storage for, the current user. It
735     // is replaced when a different user signs in. Instead of using it directly,
736     // you should try and use `withCurrentAccountState`.
737     this.currentAccountState = this.newAccountState();
738   },
740   async withCurrentAccountState(func) {
741     const state = this.currentAccountState;
742     let result;
743     try {
744       result = await func(state);
745     } catch (ex) {
746       return state.reject(ex);
747     }
748     return state.resolve(result);
749   },
751   async withVerifiedAccountState(func) {
752     return this.withCurrentAccountState(async state => {
753       let data = await state.getUserAccountData();
754       if (!data) {
755         // No signed-in user
756         throw this._error(ERROR_NO_ACCOUNT);
757       }
759       if (!this.isUserEmailVerified(data)) {
760         // Signed-in user has not verified email
761         throw this._error(ERROR_UNVERIFIED_ACCOUNT);
762       }
763       return func(state);
764     });
765   },
767   async withSessionToken(func, mustBeVerified = true) {
768     const state = this.currentAccountState;
769     let data = await state.getUserAccountData();
770     if (!data) {
771       // No signed-in user
772       throw this._error(ERROR_NO_ACCOUNT);
773     }
775     if (mustBeVerified && !this.isUserEmailVerified(data)) {
776       // Signed-in user has not verified email
777       throw this._error(ERROR_UNVERIFIED_ACCOUNT);
778     }
780     if (!data.sessionToken) {
781       throw this._error(ERROR_AUTH_ERROR, "no session token");
782     }
783     try {
784       // Anyone who needs the session token is going to send it to the server,
785       // so there's a chance we'll see an auth related error - so handle that
786       // here rather than requiring each caller to remember to.
787       let result = await func(data.sessionToken, state);
788       return state.resolve(result);
789     } catch (err) {
790       return this._handleTokenError(err);
791     }
792   },
794   get fxAccountsClient() {
795     if (!this._fxAccountsClient) {
796       this._fxAccountsClient = new lazy.FxAccountsClient();
797     }
798     return this._fxAccountsClient;
799   },
801   // The profile object used to fetch the actual user profile.
802   _profile: null,
803   get profile() {
804     if (!this._profile) {
805       let profileServerUrl = Services.urlFormatter.formatURLPref(
806         "identity.fxaccounts.remote.profile.uri"
807       );
808       this._profile = new lazy.FxAccountsProfile({
809         fxa: this,
810         profileServerUrl,
811       });
812     }
813     return this._profile;
814   },
816   _commands: null,
817   get commands() {
818     if (!this._commands) {
819       this._commands = new lazy.FxAccountsCommands(this);
820     }
821     return this._commands;
822   },
824   _device: null,
825   get device() {
826     if (!this._device) {
827       this._device = new lazy.FxAccountsDevice(this);
828     }
829     return this._device;
830   },
832   _telemetry: null,
833   get telemetry() {
834     if (!this._telemetry) {
835       this._telemetry = new lazy.FxAccountsTelemetry(this);
836     }
837     return this._telemetry;
838   },
840   // A hook-point for tests who may want a mocked AccountState or mocked storage.
841   newAccountState(credentials) {
842     let storage = new FxAccountsStorageManager();
843     storage.initialize(credentials);
844     return new AccountState(storage);
845   },
847   notifyDevices(deviceIds, excludedIds, payload, TTL) {
848     if (typeof deviceIds == "string") {
849       deviceIds = [deviceIds];
850     }
851     return this.withSessionToken(sessionToken => {
852       return this.fxAccountsClient.notifyDevices(
853         sessionToken,
854         deviceIds,
855         excludedIds,
856         payload,
857         TTL
858       );
859     });
860   },
862   /**
863    * Return the current time in milliseconds as an integer.  Allows tests to
864    * manipulate the date to simulate token expiration.
865    */
866   now() {
867     return this.fxAccountsClient.now();
868   },
870   /**
871    * Return clock offset in milliseconds, as reported by the fxAccountsClient.
872    * This can be overridden for testing.
873    *
874    * The offset is the number of milliseconds that must be added to the client
875    * clock to make it equal to the server clock.  For example, if the client is
876    * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
877    */
878   get localtimeOffsetMsec() {
879     return this.fxAccountsClient.localtimeOffsetMsec;
880   },
882   /**
883    * Ask the server whether the user's email has been verified
884    */
885   checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) {
886     if (!sessionToken) {
887       return Promise.reject(
888         new Error("checkEmailStatus called without a session token")
889       );
890     }
891     return this.fxAccountsClient
892       .recoveryEmailStatus(sessionToken, options)
893       .catch(error => this._handleTokenError(error));
894   },
896   // set() makes sure that polling is happening, if necessary.
897   // get() does not wait for verification, and returns an object even if
898   // unverified. The caller of get() must check .verified .
899   // The "fxaccounts:onverified" event will fire only when the verified
900   // state goes from false to true, so callers must register their observer
901   // and then call get(). In particular, it will not fire when the account
902   // was found to be verified in a previous boot: if our stored state says
903   // the account is verified, the event will never fire. So callers must do:
904   //   register notification observer (go)
905   //   userdata = get()
906   //   if (userdata.verified()) {go()}
908   /**
909    * Set the current user signed in to Firefox Accounts.
910    *
911    * @param credentials
912    *        The credentials object obtained by logging in or creating
913    *        an account on the FxA server:
914    *        {
915    *          authAt: The time (seconds since epoch) that this record was
916    *                  authenticated
917    *          email: The users email address
918    *          keyFetchToken: a keyFetchToken which has not yet been used
919    *          sessionToken: Session for the FxA server
920    *          uid: The user's unique id
921    *          unwrapBKey: used to unwrap kB, derived locally from the
922    *                      password (not revealed to the FxA server)
923    *          verified: true/false
924    *        }
925    * @return Promise
926    *         The promise resolves to null when the data is saved
927    *         successfully and is rejected on error.
928    */
929   async setSignedInUser(credentials) {
930     if (!lazy.FXA_ENABLED) {
931       throw new Error("Cannot call setSignedInUser when FxA is disabled.");
932     }
933     for (const pref of Services.prefs.getChildList(PREF_ACCOUNT_ROOT)) {
934       Services.prefs.clearUserPref(pref);
935     }
936     log.debug("setSignedInUser - aborting any existing flows");
937     const signedInUser = await this.currentAccountState.getUserAccountData();
938     if (signedInUser) {
939       await this._signOutServer(
940         signedInUser.sessionToken,
941         signedInUser.oauthTokens
942       );
943     }
944     await this.abortExistingFlow();
945     let currentAccountState = (this.currentAccountState = this.newAccountState(
946       Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object.
947     ));
948     // This promise waits for storage, but not for verification.
949     // We're telling the caller that this is durable now (although is that
950     // really something we should commit to? Why not let the write happen in
951     // the background? Already does for updateAccountData ;)
952     await currentAccountState.promiseInitialized;
953     // Starting point for polling if new user
954     if (!this.isUserEmailVerified(credentials)) {
955       this.startVerifiedCheck(credentials);
956     }
957     await this.notifyObservers(ONLOGIN_NOTIFICATION);
958     await this.updateDeviceRegistration();
959     return currentAccountState.resolve();
960   },
962   /**
963    * Update account data for the currently signed in user.
964    *
965    * @param credentials
966    *        The credentials object containing the fields to be updated.
967    *        This object must contain the |uid| field and it must
968    *        match the currently signed in user.
969    */
970   updateUserAccountData(credentials) {
971     log.debug(
972       "updateUserAccountData called with fields",
973       Object.keys(credentials)
974     );
975     if (logPII()) {
976       log.debug("updateUserAccountData called with data", credentials);
977     }
978     let currentAccountState = this.currentAccountState;
979     return currentAccountState.promiseInitialized.then(() => {
980       if (!credentials.uid) {
981         throw new Error("The specified credentials have no uid");
982       }
983       return currentAccountState.updateUserAccountData(credentials);
984     });
985   },
987   /*
988    * Reset state such that any previous flow is canceled.
989    */
990   abortExistingFlow() {
991     if (this.currentTimer) {
992       log.debug("Polling aborted; Another user signing in");
993       clearTimeout(this.currentTimer);
994       this.currentTimer = 0;
995     }
996     if (this._profile) {
997       this._profile.tearDown();
998       this._profile = null;
999     }
1000     if (this._commands) {
1001       this._commands = null;
1002     }
1003     if (this._device) {
1004       this._device.reset();
1005     }
1006     // We "abort" the accountState and assume our caller is about to throw it
1007     // away and replace it with a new one.
1008     return this.currentAccountState.abort();
1009   },
1011   async checkVerificationStatus() {
1012     log.trace("checkVerificationStatus");
1013     let state = this.currentAccountState;
1014     let data = await state.getUserAccountData();
1015     if (!data) {
1016       log.trace("checkVerificationStatus - no user data");
1017       return null;
1018     }
1020     // Always check the verification status, even if the local state indicates
1021     // we're already verified. If the user changed their password, the check
1022     // will fail, and we'll enter the reauth state.
1023     log.trace("checkVerificationStatus - forcing verification status check");
1024     return this.startPollEmailStatus(state, data.sessionToken, "push");
1025   },
1027   _destroyOAuthToken(tokenData) {
1028     return this.fxAccountsClient.oauthDestroy(
1029       FX_OAUTH_CLIENT_ID,
1030       tokenData.token
1031     );
1032   },
1034   _destroyAllOAuthTokens(tokenInfos) {
1035     if (!tokenInfos) {
1036       return Promise.resolve();
1037     }
1038     // let's just destroy them all in parallel...
1039     let promises = [];
1040     for (let tokenInfo of Object.values(tokenInfos)) {
1041       promises.push(this._destroyOAuthToken(tokenInfo));
1042     }
1043     return Promise.all(promises);
1044   },
1046   async signOut(localOnly) {
1047     let sessionToken;
1048     let tokensToRevoke;
1049     const data = await this.currentAccountState.getUserAccountData();
1050     // Save the sessionToken, tokens before resetting them in _signOutLocal().
1051     if (data) {
1052       sessionToken = data.sessionToken;
1053       tokensToRevoke = data.oauthTokens;
1054     }
1055     await this.notifyObservers(ON_PRELOGOUT_NOTIFICATION);
1056     await this._signOutLocal();
1057     if (!localOnly) {
1058       // Do this in the background so *any* slow request won't
1059       // block the local sign out.
1060       Services.tm.dispatchToMainThread(async () => {
1061         await this._signOutServer(sessionToken, tokensToRevoke);
1062         lazy.FxAccountsConfig.resetConfigURLs();
1063         this.notifyObservers("testhelper-fxa-signout-complete");
1064       });
1065     } else {
1066       // We want to do this either way -- but if we're signing out remotely we
1067       // need to wait until we destroy the oauth tokens if we want that to succeed.
1068       lazy.FxAccountsConfig.resetConfigURLs();
1069     }
1070     return this.notifyObservers(ONLOGOUT_NOTIFICATION);
1071   },
1073   async _signOutLocal() {
1074     for (const pref of Services.prefs.getChildList(PREF_ACCOUNT_ROOT)) {
1075       Services.prefs.clearUserPref(pref);
1076     }
1077     await this.currentAccountState.signOut();
1078     // this "aborts" this.currentAccountState but doesn't make a new one.
1079     await this.abortExistingFlow();
1080     this.currentAccountState = this.newAccountState();
1081     return this.currentAccountState.promiseInitialized;
1082   },
1084   async _signOutServer(sessionToken, tokensToRevoke) {
1085     log.debug("Unsubscribing from FxA push.");
1086     try {
1087       await this.fxaPushService.unsubscribe();
1088     } catch (err) {
1089       log.error("Could not unsubscribe from push.", err);
1090     }
1091     if (sessionToken) {
1092       log.debug("Destroying session and device.");
1093       try {
1094         await this.fxAccountsClient.signOut(sessionToken, { service: "sync" });
1095       } catch (err) {
1096         log.error("Error during remote sign out of Firefox Accounts", err);
1097       }
1098     } else {
1099       log.warn("Missing session token; skipping remote sign out");
1100     }
1101     log.debug("Destroying all OAuth tokens.");
1102     try {
1103       await this._destroyAllOAuthTokens(tokensToRevoke);
1104     } catch (err) {
1105       log.error("Error during destruction of oauth tokens during signout", err);
1106     }
1107   },
1109   getUserAccountData(fieldNames = null) {
1110     return this.currentAccountState.getUserAccountData(fieldNames);
1111   },
1113   isUserEmailVerified: function isUserEmailVerified(data) {
1114     return !!(data && data.verified);
1115   },
1117   /**
1118    * Setup for and if necessary do email verification polling.
1119    */
1120   loadAndPoll() {
1121     let currentState = this.currentAccountState;
1122     return currentState.getUserAccountData().then(data => {
1123       if (data) {
1124         if (!this.isUserEmailVerified(data)) {
1125           this.startPollEmailStatus(
1126             currentState,
1127             data.sessionToken,
1128             "browser-startup"
1129           );
1130         }
1131       }
1132       return data;
1133     });
1134   },
1136   startVerifiedCheck(data) {
1137     log.debug("startVerifiedCheck", data && data.verified);
1138     if (logPII()) {
1139       log.debug("startVerifiedCheck with user data", data);
1140     }
1142     // Get us to the verified state. This returns a promise that will fire when
1143     // verification is complete.
1145     // The callers of startVerifiedCheck never consume a returned promise (ie,
1146     // this is simply kicking off a background fetch) so we must add a rejection
1147     // handler to avoid runtime warnings about the rejection not being handled.
1148     this.whenVerified(data).catch(err =>
1149       log.info("startVerifiedCheck promise was rejected: " + err)
1150     );
1151   },
1153   whenVerified(data) {
1154     let currentState = this.currentAccountState;
1155     if (data.verified) {
1156       log.debug("already verified");
1157       return currentState.resolve(data);
1158     }
1159     if (!currentState.whenVerifiedDeferred) {
1160       log.debug("whenVerified promise starts polling for verified email");
1161       this.startPollEmailStatus(currentState, data.sessionToken, "start");
1162     }
1163     return currentState.whenVerifiedDeferred.promise.then(result =>
1164       currentState.resolve(result)
1165     );
1166   },
1168   async notifyObservers(topic, data) {
1169     for (let f of this.observerPreloads) {
1170       try {
1171         await f();
1172       } catch (O_o) {}
1173     }
1174     log.debug("Notifying observers of " + topic);
1175     Services.obs.notifyObservers(null, topic, data);
1176   },
1178   startPollEmailStatus(currentState, sessionToken, why) {
1179     log.debug("entering startPollEmailStatus: " + why);
1180     // If we were already polling, stop and start again.  This could happen
1181     // if the user requested the verification email to be resent while we
1182     // were already polling for receipt of an earlier email.
1183     if (this.currentTimer) {
1184       log.debug(
1185         "startPollEmailStatus starting while existing timer is running"
1186       );
1187       clearTimeout(this.currentTimer);
1188       this.currentTimer = null;
1189     }
1191     this.pollStartDate = Date.now();
1192     if (!currentState.whenVerifiedDeferred) {
1193       currentState.whenVerifiedDeferred = PromiseUtils.defer();
1194       // This deferred might not end up with any handlers (eg, if sync
1195       // is yet to start up.)  This might cause "A promise chain failed to
1196       // handle a rejection" messages, so add an error handler directly
1197       // on the promise to log the error.
1198       currentState.whenVerifiedDeferred.promise.then(
1199         () => {
1200           log.info("the user became verified");
1201           // We are now ready for business. This should only be invoked once
1202           // per setSignedInUser(), regardless of whether we've rebooted since
1203           // setSignedInUser() was called.
1204           this.notifyObservers(ONVERIFIED_NOTIFICATION);
1205         },
1206         err => {
1207           log.info("the wait for user verification was stopped: " + err);
1208         }
1209       );
1210     }
1211     return this.pollEmailStatus(currentState, sessionToken, why);
1212   },
1214   // We return a promise for testing only. Other callers can ignore this,
1215   // since verification polling continues in the background.
1216   async pollEmailStatus(currentState, sessionToken, why) {
1217     log.debug("entering pollEmailStatus: " + why);
1218     let nextPollMs;
1219     try {
1220       const response = await this.checkEmailStatus(sessionToken, {
1221         reason: why,
1222       });
1223       log.debug("checkEmailStatus -> " + JSON.stringify(response));
1224       if (response && response.verified) {
1225         await this.onPollEmailSuccess(currentState);
1226         return;
1227       }
1228     } catch (error) {
1229       if (error && error.code && error.code == 401) {
1230         let error = new Error("Verification status check failed");
1231         this._rejectWhenVerified(currentState, error);
1232         return;
1233       }
1234       if (error && error.retryAfter) {
1235         // If the server told us to back off, back off the requested amount.
1236         nextPollMs = (error.retryAfter + 3) * 1000;
1237         log.warn(
1238           `the server rejected our email status check and told us to try again in ${nextPollMs}ms`
1239         );
1240       } else {
1241         log.error(`checkEmailStatus failed to poll`, error);
1242       }
1243     }
1244     if (why == "push") {
1245       return;
1246     }
1247     let pollDuration = Date.now() - this.pollStartDate;
1248     // Polling session expired.
1249     if (pollDuration >= this.POLL_SESSION) {
1250       if (currentState.whenVerifiedDeferred) {
1251         let error = new Error("User email verification timed out.");
1252         this._rejectWhenVerified(currentState, error);
1253       }
1254       log.debug("polling session exceeded, giving up");
1255       return;
1256     }
1257     // Poll email status again after a short delay.
1258     if (nextPollMs === undefined) {
1259       let currentMinute = Math.ceil(pollDuration / 60000);
1260       nextPollMs =
1261         why == "start" &&
1262         currentMinute < this.VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD
1263           ? this.VERIFICATION_POLL_TIMEOUT_INITIAL
1264           : this.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT;
1265     }
1266     this._scheduleNextPollEmailStatus(
1267       currentState,
1268       sessionToken,
1269       nextPollMs,
1270       why
1271     );
1272   },
1274   // Easy-to-mock testable method
1275   _scheduleNextPollEmailStatus(currentState, sessionToken, nextPollMs, why) {
1276     log.debug("polling with timeout = " + nextPollMs);
1277     this.currentTimer = setTimeout(() => {
1278       this.pollEmailStatus(currentState, sessionToken, why);
1279     }, nextPollMs);
1280   },
1282   async onPollEmailSuccess(currentState) {
1283     try {
1284       await currentState.updateUserAccountData({ verified: true });
1285       const accountData = await currentState.getUserAccountData();
1286       this._setLastUserPref(accountData.email);
1287       // Now that the user is verified, we can proceed to fetch keys
1288       if (currentState.whenVerifiedDeferred) {
1289         currentState.whenVerifiedDeferred.resolve(accountData);
1290         delete currentState.whenVerifiedDeferred;
1291       }
1292     } catch (e) {
1293       log.error(e);
1294     }
1295   },
1297   _rejectWhenVerified(currentState, error) {
1298     currentState.whenVerifiedDeferred.reject(error);
1299     delete currentState.whenVerifiedDeferred;
1300   },
1302   /**
1303    * Does the actual fetch of an oauth token for getOAuthToken()
1304    * using the account session token.
1305    *
1306    * It's split out into a separate method so that we can easily
1307    * stash in-flight calls in a cache.
1308    *
1309    * @param {String} scopeString
1310    * @param {Number} ttl
1311    * @returns {Promise<string>}
1312    * @private
1313    */
1314   async _doTokenFetchWithSessionToken(sessionToken, scopeString, ttl) {
1315     const result = await this.fxAccountsClient.accessTokenWithSessionToken(
1316       sessionToken,
1317       FX_OAUTH_CLIENT_ID,
1318       scopeString,
1319       ttl
1320     );
1321     return result.access_token;
1322   },
1324   getOAuthToken(options = {}) {
1325     log.debug("getOAuthToken enter");
1326     let scope = options.scope;
1327     if (typeof scope === "string") {
1328       scope = [scope];
1329     }
1331     if (!scope || !scope.length) {
1332       return Promise.reject(
1333         this._error(
1334           ERROR_INVALID_PARAMETER,
1335           "Missing or invalid 'scope' option"
1336         )
1337       );
1338     }
1340     return this.withSessionToken(async (sessionToken, currentState) => {
1341       // Early exit for a cached token.
1342       let cached = currentState.getCachedToken(scope);
1343       if (cached) {
1344         log.debug("getOAuthToken returning a cached token");
1345         return cached.token;
1346       }
1348       // Build the string we use in our "inflight" map and that we send to the
1349       // server. Because it's used as a key in the map we sort the scopes.
1350       let scopeString = scope.sort().join(" ");
1352       // We keep a map of in-flight requests to avoid multiple promise-based
1353       // consumers concurrently requesting the same token.
1354       let maybeInFlight = currentState.inFlightTokenRequests.get(scopeString);
1355       if (maybeInFlight) {
1356         log.debug("getOAuthToken has an in-flight request for this scope");
1357         return maybeInFlight;
1358       }
1360       // We need to start a new fetch and stick the promise in our in-flight map
1361       // and remove it when it resolves.
1362       let promise = this._doTokenFetchWithSessionToken(
1363         sessionToken,
1364         scopeString,
1365         options.ttl
1366       )
1367         .then(token => {
1368           // As a sanity check, ensure something else hasn't raced getting a token
1369           // of the same scope. If something has we just make noise rather than
1370           // taking any concrete action because it should never actually happen.
1371           if (currentState.getCachedToken(scope)) {
1372             log.error(`detected a race for oauth token with scope ${scope}`);
1373           }
1374           // If we got one, cache it.
1375           if (token) {
1376             let entry = { token };
1377             currentState.setCachedToken(scope, entry);
1378           }
1379           return token;
1380         })
1381         .finally(() => {
1382           // Remove ourself from the in-flight map. There's no need to check the
1383           // result of .delete() to handle a signout race, because setCachedToken
1384           // above will fail in that case and cause the entire call to fail.
1385           currentState.inFlightTokenRequests.delete(scopeString);
1386         });
1388       currentState.inFlightTokenRequests.set(scopeString, promise);
1389       return promise;
1390     });
1391   },
1393   /**
1394    * Remove an OAuth token from the token cache
1395    * and makes a network request to FxA server to destroy the token.
1396    *
1397    * @param options
1398    *        {
1399    *          token: (string) A previously fetched token.
1400    *        }
1401    * @return Promise.<undefined> This function will always resolve, even if
1402    *         an unknown token is passed.
1403    */
1404   removeCachedOAuthToken(options) {
1405     if (!options.token || typeof options.token !== "string") {
1406       throw this._error(
1407         ERROR_INVALID_PARAMETER,
1408         "Missing or invalid 'token' option"
1409       );
1410     }
1411     return this.withCurrentAccountState(currentState => {
1412       let existing = currentState.removeCachedToken(options.token);
1413       if (existing) {
1414         // background destroy.
1415         this._destroyOAuthToken(existing).catch(err => {
1416           log.warn("FxA failed to revoke a cached token", err);
1417         });
1418       }
1419     });
1420   },
1422   async _getVerifiedAccountOrReject() {
1423     let data = await this.currentAccountState.getUserAccountData();
1424     if (!data) {
1425       // No signed-in user
1426       throw this._error(ERROR_NO_ACCOUNT);
1427     }
1428     if (!this.isUserEmailVerified(data)) {
1429       // Signed-in user has not verified email
1430       throw this._error(ERROR_UNVERIFIED_ACCOUNT);
1431     }
1432     return data;
1433   },
1435   // _handle* methods used by push, used when the account/device status is
1436   // changed on a different device.
1437   async _handleAccountDestroyed(uid) {
1438     let state = this.currentAccountState;
1439     const accountData = await state.getUserAccountData();
1440     const localUid = accountData ? accountData.uid : null;
1441     if (!localUid) {
1442       log.info(
1443         `Account destroyed push notification received, but we're already logged-out`
1444       );
1445       return null;
1446     }
1447     if (uid == localUid) {
1448       const data = JSON.stringify({ isLocalDevice: true });
1449       await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
1450       return this.signOut(true);
1451     }
1452     log.info(
1453       `The destroyed account uid doesn't match with the local uid. ` +
1454         `Local: ${localUid}, account uid destroyed: ${uid}`
1455     );
1456     return null;
1457   },
1459   async _handleDeviceDisconnection(deviceId) {
1460     let state = this.currentAccountState;
1461     const accountData = await state.getUserAccountData();
1462     if (!accountData || !accountData.device) {
1463       // Nothing we can do here.
1464       return;
1465     }
1466     const localDeviceId = accountData.device.id;
1467     const isLocalDevice = deviceId == localDeviceId;
1468     if (isLocalDevice) {
1469       this.signOut(true);
1470     }
1471     const data = JSON.stringify({ isLocalDevice });
1472     await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
1473   },
1475   _setLastUserPref(newEmail) {
1476     Services.prefs.setStringPref(
1477       PREF_LAST_FXA_USER,
1478       CryptoUtils.sha256Base64(newEmail)
1479     );
1480   },
1482   async _handleEmailUpdated(newEmail) {
1483     this._setLastUserPref(newEmail);
1484     await this.currentAccountState.updateUserAccountData({ email: newEmail });
1485   },
1487   /*
1488    * Coerce an error into one of the general error cases:
1489    *          NETWORK_ERROR
1490    *          AUTH_ERROR
1491    *          UNKNOWN_ERROR
1492    *
1493    * These errors will pass through:
1494    *          INVALID_PARAMETER
1495    *          NO_ACCOUNT
1496    *          UNVERIFIED_ACCOUNT
1497    */
1498   _errorToErrorClass(aError) {
1499     if (aError.errno) {
1500       let error = SERVER_ERRNO_TO_ERROR[aError.errno];
1501       return this._error(
1502         ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN,
1503         aError
1504       );
1505     } else if (
1506       aError.message &&
1507       (aError.message === "INVALID_PARAMETER" ||
1508         aError.message === "NO_ACCOUNT" ||
1509         aError.message === "UNVERIFIED_ACCOUNT" ||
1510         aError.message === "AUTH_ERROR")
1511     ) {
1512       return aError;
1513     }
1514     return this._error(ERROR_UNKNOWN, aError);
1515   },
1517   _error(aError, aDetails) {
1518     log.error("FxA rejecting with error ${aError}, details: ${aDetails}", {
1519       aError,
1520       aDetails,
1521     });
1522     let reason = new Error(aError);
1523     if (aDetails) {
1524       reason.details = aDetails;
1525     }
1526     return reason;
1527   },
1529   // Attempt to update the auth server with whatever device details are stored
1530   // in the account data. Returns a promise that always resolves, never rejects.
1531   // If the promise resolves to a value, that value is the device id.
1532   updateDeviceRegistration() {
1533     return this.device.updateDeviceRegistration();
1534   },
1536   /**
1537    * Delete all the persisted credentials we store for FxA. After calling
1538    * this, the user will be forced to re-authenticate to continue.
1539    *
1540    * @return Promise resolves when the user data has been persisted
1541    */
1542   dropCredentials(state) {
1543     // Delete all fields except those required for the user to
1544     // reauthenticate.
1545     let updateData = {};
1546     let clearField = field => {
1547       if (!FXA_PWDMGR_REAUTH_ALLOWLIST.has(field)) {
1548         updateData[field] = null;
1549       }
1550     };
1551     FXA_PWDMGR_PLAINTEXT_FIELDS.forEach(clearField);
1552     FXA_PWDMGR_SECURE_FIELDS.forEach(clearField);
1554     return state.updateUserAccountData(updateData);
1555   },
1557   async checkAccountStatus(state) {
1558     log.info("checking account status...");
1559     let data = await state.getUserAccountData(["uid", "sessionToken"]);
1560     if (!data) {
1561       log.info("account status: no user");
1562       return false;
1563     }
1564     // If we have a session token, then check if that remains valid - if this
1565     // works we know the account must also be OK.
1566     if (data.sessionToken) {
1567       if (await this.fxAccountsClient.sessionStatus(data.sessionToken)) {
1568         log.info("account status: ok");
1569         return true;
1570       }
1571     }
1572     let exists = await this.fxAccountsClient.accountStatus(data.uid);
1573     if (!exists) {
1574       // Delete all local account data. Since the account no longer
1575       // exists, we can skip the remote calls.
1576       log.info("account status: deleted");
1577       await this._handleAccountDestroyed(data.uid);
1578     } else {
1579       // Note that we may already have been in a "needs reauth" state (ie, if
1580       // this function was called when we already had no session token), but
1581       // that's OK - re-notifying etc should cause no harm.
1582       log.info("account status: needs reauthentication");
1583       await this.dropCredentials(this.currentAccountState);
1584       // Notify the account state has changed so the UI updates.
1585       await this.notifyObservers(ON_ACCOUNT_STATE_CHANGE_NOTIFICATION);
1586     }
1587     return false;
1588   },
1590   async _handleTokenError(err) {
1591     if (!err || err.code != 401 || err.errno != ERRNO_INVALID_AUTH_TOKEN) {
1592       throw err;
1593     }
1594     log.warn("handling invalid token error", err);
1595     // Note that we don't use `withCurrentAccountState` here as that will cause
1596     // an error to be thrown if we sign out due to the account not existing.
1597     let state = this.currentAccountState;
1598     let ok = await this.checkAccountStatus(state);
1599     if (ok) {
1600       log.warn("invalid token error, but account state appears ok?");
1601     }
1602     // always re-throw the error.
1603     throw err;
1604   },
1607 let fxAccountsSingleton = null;
1609 export function getFxAccountsSingleton() {
1610   if (fxAccountsSingleton) {
1611     return fxAccountsSingleton;
1612   }
1614   fxAccountsSingleton = new FxAccounts();
1616   // XXX Bug 947061 - We need a strategy for resuming email verification after
1617   // browser restart
1618   fxAccountsSingleton._internal.loadAndPoll();
1620   return fxAccountsSingleton;
1623 // `AccountState` is exported for tests.