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