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 { CryptoUtils } from "resource://services-crypto/utils.sys.mjs";
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
9 import { FxAccountsStorageManager } from "resource://gre/modules/FxAccountsStorage.sys.mjs";
12 ERRNO_INVALID_AUTH_TOKEN,
14 ERROR_INVALID_PARAMETER,
16 ERROR_TO_GENERAL_ERROR_CLASS,
18 ERROR_UNVERIFIED_ACCOUNT,
19 FXA_PWDMGR_PLAINTEXT_FIELDS,
20 FXA_PWDMGR_REAUTH_ALLOWLIST,
21 FXA_PWDMGR_SECURE_FIELDS,
23 ON_ACCOUNT_STATE_CHANGE_NOTIFICATION,
25 ONLOGOUT_NOTIFICATION,
26 ON_PRELOGOUT_NOTIFICATION,
27 ONVERIFIED_NOTIFICATION,
28 ON_DEVICE_DISCONNECTED_NOTIFICATION,
32 SERVER_ERRNO_TO_ERROR,
36 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
40 ChromeUtils.defineESModuleGetters(lazy, {
41 FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs",
42 FxAccountsCommands: "resource://gre/modules/FxAccountsCommands.sys.mjs",
43 FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs",
44 FxAccountsDevice: "resource://gre/modules/FxAccountsDevice.sys.mjs",
45 FxAccountsKeys: "resource://gre/modules/FxAccountsKeys.sys.mjs",
46 FxAccountsOAuth: "resource://gre/modules/FxAccountsOAuth.sys.mjs",
47 FxAccountsProfile: "resource://gre/modules/FxAccountsProfile.sys.mjs",
48 FxAccountsTelemetry: "resource://gre/modules/FxAccountsTelemetry.sys.mjs",
51 ChromeUtils.defineLazyGetter(lazy, "mpLocked", () => {
52 return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
56 ChromeUtils.defineLazyGetter(lazy, "ensureMPUnlocked", () => {
57 return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
58 .Utils.ensureMPUnlocked;
61 XPCOMUtils.defineLazyPreferenceGetter(
64 "identity.fxaccounts.enabled",
68 XPCOMUtils.defineLazyPreferenceGetter(
71 "identity.fxaccounts.oauth.enabled",
75 export const ERROR_INVALID_ACCOUNT_STATE = "ERROR_INVALID_ACCOUNT_STATE";
77 // An AccountState object holds all state related to one specific account.
78 // It is considered "private" to the FxAccounts modules.
79 // Only one AccountState is ever "current" in the FxAccountsInternal object -
80 // whenever a user logs out or logs in, the current AccountState is discarded,
81 // making it impossible for the wrong state or state data to be accidentally
83 // In addition, it has some promise-related helpers to ensure that if an
84 // attempt is made to resolve a promise on a "stale" state (eg, if an
85 // operation starts, but a different user logs in before the operation
86 // completes), the promise will be rejected.
87 // It is intended to be used thusly:
88 // somePromiseBasedFunction: function() {
89 // let currentState = this.currentAccountState;
90 // return someOtherPromiseFunction().then(
91 // data => currentState.resolve(data)
94 // If the state has changed between the function being called and the promise
95 // being resolved, the .resolve() call will actually be rejected.
96 export function AccountState(storageManager) {
97 this.storageManager = storageManager;
98 this.inFlightTokenRequests = new Map();
99 this.promiseInitialized = this.storageManager
102 this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {};
105 log.error("Failed to initialize the storage manager", err);
106 // Things are going to fall apart, but not much we can do about it here.
110 AccountState.prototype = {
112 whenVerifiedDeferred: null,
113 whenKeysReadyDeferred: null,
115 // If the storage manager has been nuked then we are no longer current.
117 return this.storageManager != null;
121 if (this.whenVerifiedDeferred) {
122 this.whenVerifiedDeferred.reject(
123 new Error("Verification aborted; Another user signing in")
125 this.whenVerifiedDeferred = null;
127 if (this.whenKeysReadyDeferred) {
128 this.whenKeysReadyDeferred.reject(
129 new Error("Key fetching aborted; Another user signing in")
131 this.whenKeysReadyDeferred = null;
133 this.inFlightTokenRequests.clear();
134 return this.signOut();
137 // Clobber all cached data and write that empty data to storage.
141 this.oauthTokens = null;
142 this.inFlightTokenRequests.clear();
144 // Avoid finalizing the storageManager multiple times (ie, .signOut()
145 // followed by .abort())
146 if (!this.storageManager) {
149 const storageManager = this.storageManager;
150 this.storageManager = null;
152 await storageManager.deleteAccountData();
153 await storageManager.finalize();
156 // Get user account data. Optionally specify explicit field names to fetch
157 // (and note that if you require an in-memory field you *must* specify the
159 getUserAccountData(fieldNames = null) {
160 if (!this.isCurrent) {
161 return Promise.reject(new Error("Another user has signed in"));
163 return this.storageManager.getAccountData(fieldNames).then(result => {
164 return this.resolve(result);
168 async updateUserAccountData(updatedFields) {
169 if ("uid" in updatedFields) {
170 const existing = await this.getUserAccountData(["uid"]);
171 if (existing.uid != updatedFields.uid) {
173 "The specified credentials aren't for the current user"
176 // We need to nuke uid as storage will complain if we try and
177 // update it (even when the value is the same)
178 updatedFields = Cu.cloneInto(updatedFields, {}); // clone it first
179 delete updatedFields.uid;
181 if (!this.isCurrent) {
182 return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
184 return this.storageManager.updateAccountData(updatedFields);
188 if (!this.isCurrent) {
190 "An accountState promise was resolved, but was actually rejected" +
191 " due to the account state changing. This can happen if a new account signed in, or" +
192 " the account was signed out. Originally resolved with, ",
195 return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
197 return Promise.resolve(result);
201 // It could be argued that we should just let it reject with the original
202 // error - but this runs the risk of the error being (eg) a 401, which
203 // might cause the consumer to attempt some remediation and cause other
205 if (!this.isCurrent) {
207 "An accountState promise was rejected, but we are ignoring that" +
208 " reason and rejecting it due to the account state changing. This can happen if" +
209 " a different account signed in or the account was signed out" +
210 " originally resolved with, ",
213 return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
215 return Promise.reject(error);
218 // Abstractions for storage of cached tokens - these are all sync, and don't
219 // handle revocation etc - it's just storage (and the storage itself is async,
220 // but we don't return the storage promises, so it *looks* sync)
221 // These functions are sync simply so we can handle "token races" - when there
222 // are multiple in-flight requests for the same scope, we can detect this
223 // and revoke the redundant token.
225 // A preamble for the cache helpers...
227 if (!this.isCurrent) {
228 throw new Error(ERROR_INVALID_ACCOUNT_STATE);
232 // Set a cached token. |tokenData| must have a 'token' element, but may also
233 // have additional fields.
234 // The 'get' functions below return the entire |tokenData| value.
235 setCachedToken(scopeArray, tokenData) {
236 this._cachePreamble();
237 if (!tokenData.token) {
238 throw new Error("No token");
240 let key = getScopeKey(scopeArray);
241 this.oauthTokens[key] = tokenData;
242 // And a background save...
243 this._persistCachedTokens();
246 // Return data for a cached token or null (or throws on bad state etc)
247 getCachedToken(scopeArray) {
248 this._cachePreamble();
249 let key = getScopeKey(scopeArray);
250 let result = this.oauthTokens[key];
252 // later we might want to check an expiry date - but we currently
253 // have no such concept, so just return it.
254 log.trace("getCachedToken returning cached token");
260 // Remove a cached token from the cache. Does *not* revoke it from anywhere.
261 // Returns the entire token entry if found, null otherwise.
262 removeCachedToken(token) {
263 this._cachePreamble();
264 let data = this.oauthTokens;
265 for (let [key, tokenValue] of Object.entries(data)) {
266 if (tokenValue.token == token) {
268 // And a background save...
269 this._persistCachedTokens();
276 // A hook-point for tests. Returns a promise that's ignored in most cases
277 // (notable exceptions are tests and when we explicitly are saving the entire
278 // set of user data.)
279 _persistCachedTokens() {
280 this._cachePreamble();
281 return this.updateUserAccountData({ oauthTokens: this.oauthTokens }).catch(
283 log.error("Failed to update cached tokens", err);
289 /* Given an array of scopes, make a string key by normalizing. */
290 function getScopeKey(scopeArray) {
291 let normalizedScopes = scopeArray.map(item => item.toLowerCase());
292 return normalizedScopes.sort().join("|");
295 function getPropertyDescriptor(obj, prop) {
297 Object.getOwnPropertyDescriptor(obj, prop) ||
298 getPropertyDescriptor(Object.getPrototypeOf(obj), prop)
303 * Copies properties from a given object to another object.
305 * @param from (object)
306 * The object we read property descriptors from.
308 * The object that we set property descriptors on.
309 * @param thisObj (object)
310 * The object that will be used to .bind() all function properties we find to.
311 * @param keys ([...])
312 * The names of all properties to be copied.
314 function copyObjectProperties(from, to, thisObj, keys) {
315 for (let prop of keys) {
316 // Look for the prop in the prototype chain.
317 let desc = getPropertyDescriptor(from, prop);
319 if (typeof desc.value == "function") {
320 desc.value = desc.value.bind(thisObj);
324 desc.get = desc.get.bind(thisObj);
328 desc.set = desc.set.bind(thisObj);
331 Object.defineProperty(to, prop, desc);
338 * TODO - *all* non-underscore stuff here should have sphinx docstrings so
339 * that docs magically appear on https://firefox-source-docs.mozilla.org/
340 * (although |./mach doc| is broken on windows (bug 1232403) and on Linux for
341 * markh (some obscure npm issue he gave up on) - so later...)
343 export class FxAccounts {
344 constructor(mocks = null) {
345 this._internal = new FxAccountsInternal();
347 // it's slightly unfortunate that we need to mock the main "internal" object
348 // before calling initialize, primarily so a mock `newAccountState` is in
349 // place before initialize calls it, but we need to initialize the
350 // "sub-object" mocks after. This can probably be fixed, but whatever...
351 copyObjectProperties(
355 Object.keys(mocks).filter(key => !["device", "commands"].includes(key))
358 this._internal.initialize();
359 // allow mocking our "sub-objects" too.
361 for (let subobject of [
362 "currentAccountState",
368 if (typeof mocks[subobject] == "object") {
369 copyObjectProperties(
371 this._internal[subobject],
372 this._internal[subobject],
373 Object.keys(mocks[subobject])
381 return this._internal.commands;
384 static get config() {
385 return lazy.FxAccountsConfig;
389 return this._internal.device;
393 return this._internal.keys;
397 return this._internal.telemetry;
400 _withCurrentAccountState(func) {
401 return this._internal.withCurrentAccountState(func);
404 _withVerifiedAccountState(func) {
405 return this._internal.withVerifiedAccountState(func);
408 _withSessionToken(func, mustBeVerified = true) {
409 return this._internal.withSessionToken(func, mustBeVerified);
413 * Returns an array listing all the OAuth clients connected to the
414 * authenticated user's account. This includes browsers and web sessions - no
415 * filtering is done of the set returned by the FxA server.
417 * @typedef {Object} AttachedClient
418 * @property {String} id - OAuth `client_id` of the client.
419 * @property {Number} lastAccessedDaysAgo - How many days ago the client last
420 * accessed the FxA server APIs.
422 * @returns {Array.<AttachedClient>} A list of attached clients.
424 async listAttachedOAuthClients() {
425 // We expose last accessed times in 'days ago'
426 const ONE_DAY = 24 * 60 * 60 * 1000;
428 return this._withSessionToken(async sessionToken => {
429 const response = await this._internal.fxAccountsClient.attachedClients(
432 const attachedClients = response.body;
433 const timestamp = response.headers["x-timestamp"];
435 timestamp !== undefined
436 ? new Date(parseInt(timestamp, 10))
438 return attachedClients.map(client => {
439 const daysAgo = client.lastAccessTime
440 ? Math.max(Math.floor((now - client.lastAccessTime) / ONE_DAY), 0)
444 lastAccessedDaysAgo: daysAgo,
451 * Get an OAuth token for the user.
455 * scope: (string/array) the oauth scope(s) being requested. As a
456 * convenience, you may pass a string if only one scope is
457 * required, or an array of strings if multiple are needed.
458 * ttl: (number) OAuth token TTL in seconds.
461 * @return Promise.<string | Error>
462 * The promise resolves the oauth token as a string or rejects with
463 * an error object ({error: ERROR, details: {}}) of the following:
471 async getOAuthToken(options = {}) {
473 return await this._internal.getOAuthToken(options);
475 throw this._internal._errorToErrorClass(err);
479 /** Gets both the OAuth token and the users scoped keys for that token
480 * and verifies that both operations were done for the same user,
481 * preventing race conditions where a caller
482 * can get the key for one user, and the id of another if the user
483 * is rapidly switching between accounts
487 * scope: string the oauth scope being requested. This must
488 * be a scope with an associated key, otherwise an error
489 * will be thrown that the key is not available.
490 * ttl: (number) OAuth token TTL in seconds
493 * @return Promise.<Object | Error>
494 * The promise resolve to both the access token being requested, and the scoped key
496 * token: (string) access token
497 * key: (object) the scoped key object
499 * The promise can reject, with one of the errors `getOAuthToken`, `FxAccountKeys.getKeyForScope`, or
500 * error if the user changed in-between operations
502 getOAuthTokenAndKey(options = {}) {
503 return this._withCurrentAccountState(async () => {
504 const key = await this.keys.getKeyForScope(options.scope);
505 const token = await this.getOAuthToken(options);
506 return { token, key };
511 * Remove an OAuth token from the token cache. Callers should call this
512 * after they determine a token is invalid, so a new token will be fetched
513 * on the next call to getOAuthToken().
517 * token: (string) A previously fetched token.
519 * @return Promise.<undefined> This function will always resolve, even if
520 * an unknown token is passed.
522 removeCachedOAuthToken(options) {
523 return this._internal.removeCachedOAuthToken(options);
527 * Get details about the user currently signed in to Firefox Accounts.
530 * The promise resolves to the credentials object of the signed-in user:
532 * email: String: The user's email address
533 * uid: String: The user's unique id
534 * verified: Boolean: email verification status
535 * displayName: String or null if not known.
536 * avatar: URL of the avatar for the user. May be the default
537 * avatar, or null in edge-cases (eg, if there's an account
539 * avatarDefault: boolean - whether `avatar` is specific to the user
540 * or the default avatar.
543 * or null if no user is signed in. This function never fails except
544 * in pathological cases (eg, file-system errors, etc)
547 // Note we don't return the session token, but use it to see if we
548 // should fetch the profile.
549 const ACCT_DATA_FIELDS = ["email", "uid", "verified", "sessionToken"];
550 const PROFILE_FIELDS = ["displayName", "avatar", "avatarDefault"];
551 return this._withCurrentAccountState(async currentState => {
552 const data = await currentState.getUserAccountData(ACCT_DATA_FIELDS);
556 if (!lazy.FXA_ENABLED) {
557 await this.signOut();
560 if (!this._internal.isUserEmailVerified(data) && !lazy.oauthEnabled) {
561 // If the email is not verified, start polling for verification,
562 // but return null right away. We don't want to return a promise
563 // that might not be fulfilled for a long time.
564 this._internal.startVerifiedCheck(data);
567 let profileData = null;
568 if (data.sessionToken) {
569 delete data.sessionToken;
571 profileData = await this._internal.profile.getProfile();
573 log.error("Could not retrieve profile data", error);
576 for (let field of PROFILE_FIELDS) {
577 data[field] = profileData ? profileData[field] : null;
579 // and email is a special case - if we have profile data we prefer the
580 // email from that, as the email we stored for the account itself might
581 // not have been updated if the email changed since the user signed in.
582 if (profileData && profileData.email) {
583 data.email = profileData.email;
590 * Checks the status of the account. Resolves with Promise<boolean>, where
591 * true indicates the account status is OK and false indicates there's some
592 * issue with the account - either that there's no user currently signed in,
593 * the entire account has been deleted (in which case there will be no user
594 * signed in after this call returns), or that the user must reauthenticate (in
595 * which case `this.hasLocalSession()` will return `false` after this call
598 * Typically used when some external code which uses, for example, oauth tokens
599 * received a 401 error using the token, or that this external code has some
600 * other reason to believe the account status may be bad. Note that this will
601 * be called automatically in many cases - for example, if calls to fetch the
602 * profile, or fetch keys, etc return a 401, there's no need to call this
605 * Because this hits the server, you should only call this method when you have
606 * good reason to believe the session very recently became invalid (eg, because
607 * you saw an auth related exception from a remote service.)
609 checkAccountStatus() {
610 // Note that we don't use _withCurrentAccountState here because that will
611 // cause an exception to be thrown if we end up signing out due to the
612 // account not existing, which isn't what we want here.
613 let state = this._internal.currentAccountState;
614 return this._internal.checkAccountStatus(state);
618 * Checks if we have a valid local session state for the current account.
621 * Resolves with a boolean, with true indicating that we appear to
622 * have a valid local session, or false if we need to reauthenticate
623 * with the content server to obtain one.
624 * Note that this only checks local state, although typically that's
625 * OK, because we drop the local session information whenever we detect
626 * we are in this state. However, see checkAccountStatus() for a way to
627 * check the account and session status with the server, which can be
628 * considered the canonical, albiet expensive, way to determine the
629 * status of the account.
632 return this._withCurrentAccountState(async state => {
633 let data = await state.getUserAccountData(["sessionToken"]);
634 return !!(data && data.sessionToken);
638 /** Returns a promise that resolves to true if we can currently connect (ie,
639 * sign in, or re-connect after a password change) to a Firefox Account.
640 * If this returns false, the caller can assume that some UI was shown
641 * which tells the user why we could not connect.
643 * Currently, the primary password being locked is the only reason why
644 * this returns false, and in this scenario, the primary password unlock
645 * dialog will have been shown.
647 * This currently doesn't need to return a promise, but does so that
648 * future enhancements, such as other explanatory UI which requires
649 * async can work without modification of the call-sites.
651 static canConnectAccount() {
652 return Promise.resolve(!lazy.mpLocked() || lazy.ensureMPUnlocked());
656 * Send a message to a set of devices in the same account
658 * @param deviceIds: (null/string/array) The device IDs to send the message to.
659 * If null, will be sent to all devices.
661 * @param excludedIds: (null/string/array) If deviceIds is null, this may
662 * list device IDs which should not receive the message.
664 * @param payload: (object) The payload, which will be JSON.stringified.
666 * @param TTL: How long the message should be retained before it is discarded.
668 // XXX - used only by sync to tell other devices that the clients collection
669 // has changed so they should sync asap. The API here is somewhat vague (ie,
670 // "an object"), but to be useful across devices, the payload really needs
671 // formalizing. We should try and do something better here.
672 notifyDevices(deviceIds, excludedIds, payload, TTL) {
673 return this._internal.notifyDevices(deviceIds, excludedIds, payload, TTL);
677 * Resend the verification email for the currently signed-in user.
680 resendVerificationEmail() {
681 return this._withSessionToken((token, currentState) => {
682 this._internal.startPollEmailStatus(currentState, token, "start");
683 return this._internal.fxAccountsClient.resendVerificationEmail(token);
687 async signOut(localOnly) {
688 // Note that we do not use _withCurrentAccountState here, otherwise we
689 // end up with an exception due to the user signing out before the call is
690 // complete - but that's the entire point of this method :)
691 return this._internal.signOut(localOnly);
694 // XXX - we should consider killing this - the only reason it is public is
695 // so that sync can change it when it notices the device name being changed,
696 // and that could probably be replaced with a pref observer.
697 updateDeviceRegistration() {
698 return this._withCurrentAccountState(_ => {
699 return this._internal.updateDeviceRegistration();
703 // we should try and kill this too.
705 return this._withCurrentAccountState(_ => {
706 return this._internal.whenVerified(data);
711 * Generate a log file for the FxA action that just completed
712 * and refresh the input & output streams.
714 async flushLogFile() {
715 const logType = await logManager.resetFileLog();
716 if (logType == logManager.ERROR_LOG_WRITTEN) {
718 "FxA encountered an error - see about:sync-log for the log file."
721 Services.obs.notifyObservers(null, "service:log-manager:flush-log-file");
725 var FxAccountsInternal = function () {};
728 * The internal API's prototype.
730 FxAccountsInternal.prototype = {
731 // Make a local copy of this constant so we can mock it in testing
734 // The timeout (in ms) we use to poll for a verified mail for the first
735 // VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD minutes if the user has
736 // logged-in in this session.
737 VERIFICATION_POLL_TIMEOUT_INITIAL: 60000, // 1 minute.
738 // All the other cases (> 5 min, on restart etc).
739 VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 5 * 60000, // 5 minutes.
740 // After X minutes, the polling will slow down to _SUBSEQUENT if we have
741 // logged-in in this session.
742 VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD: 5,
744 _fxAccountsClient: null,
746 // All significant initialization should be done in this initialize() method
747 // to help with our mocking story.
749 ChromeUtils.defineLazyGetter(this, "fxaPushService", function () {
750 return Cc["@mozilla.org/fxaccounts/push;1"].getService(
755 this.keys = new lazy.FxAccountsKeys(this);
757 if (!this.observerPreloads) {
758 // A registry of promise-returning functions that `notifyObservers` should
759 // call before sending notifications. Primarily used so parts of Firefox
760 // which have yet to load for performance reasons can be force-loaded, and
761 // thus not miss notifications.
762 this.observerPreloads = [
765 let { Weave } = ChromeUtils.importESModule(
766 "resource://services-sync/main.sys.mjs"
768 return Weave.Service.promiseInitialized;
773 this.currentTimer = null;
774 // This object holds details about, and storage for, the current user. It
775 // is replaced when a different user signs in. Instead of using it directly,
776 // you should try and use `withCurrentAccountState`.
777 this.currentAccountState = this.newAccountState();
780 async withCurrentAccountState(func) {
781 const state = this.currentAccountState;
784 result = await func(state);
786 return state.reject(ex);
788 return state.resolve(result);
791 async withVerifiedAccountState(func) {
792 return this.withCurrentAccountState(async state => {
793 let data = await state.getUserAccountData();
796 throw this._error(ERROR_NO_ACCOUNT);
799 if (!this.isUserEmailVerified(data)) {
800 // Signed-in user has not verified email
801 throw this._error(ERROR_UNVERIFIED_ACCOUNT);
807 async withSessionToken(func, mustBeVerified = true) {
808 const state = this.currentAccountState;
809 let data = await state.getUserAccountData();
812 throw this._error(ERROR_NO_ACCOUNT);
815 if (mustBeVerified && !this.isUserEmailVerified(data)) {
816 // Signed-in user has not verified email
817 throw this._error(ERROR_UNVERIFIED_ACCOUNT);
820 if (!data.sessionToken) {
821 throw this._error(ERROR_AUTH_ERROR, "no session token");
824 // Anyone who needs the session token is going to send it to the server,
825 // so there's a chance we'll see an auth related error - so handle that
826 // here rather than requiring each caller to remember to.
827 let result = await func(data.sessionToken, state);
828 return state.resolve(result);
830 return this._handleTokenError(err);
834 get fxAccountsClient() {
835 if (!this._fxAccountsClient) {
836 this._fxAccountsClient = new lazy.FxAccountsClient();
838 return this._fxAccountsClient;
841 // The profile object used to fetch the actual user profile.
844 if (!this._profile) {
845 let profileServerUrl = Services.urlFormatter.formatURLPref(
846 "identity.fxaccounts.remote.profile.uri"
848 this._profile = new lazy.FxAccountsProfile({
853 return this._profile;
858 if (!this._commands) {
859 this._commands = new lazy.FxAccountsCommands(this);
861 return this._commands;
867 this._device = new lazy.FxAccountsDevice(this);
875 this._oauth = new lazy.FxAccountsOAuth(this.fxAccountsClient, this.keys);
882 if (!this._telemetry) {
883 this._telemetry = new lazy.FxAccountsTelemetry(this);
885 return this._telemetry;
888 beginOAuthFlow(scopes) {
889 return this.oauth.beginOAuthFlow(scopes);
892 completeOAuthFlow(sessionToken, code, state) {
893 return this.oauth.completeOAuthFlow(sessionToken, code, state);
896 setScopedKeys(scopedKeys) {
897 return this.keys.setScopedKeys(scopedKeys);
900 // A hook-point for tests who may want a mocked AccountState or mocked storage.
901 newAccountState(credentials) {
902 let storage = new FxAccountsStorageManager();
903 storage.initialize(credentials);
904 return new AccountState(storage);
907 notifyDevices(deviceIds, excludedIds, payload, TTL) {
908 if (typeof deviceIds == "string") {
909 deviceIds = [deviceIds];
911 return this.withSessionToken(sessionToken => {
912 return this.fxAccountsClient.notifyDevices(
923 * Return the current time in milliseconds as an integer. Allows tests to
924 * manipulate the date to simulate token expiration.
927 return this.fxAccountsClient.now();
931 * Return clock offset in milliseconds, as reported by the fxAccountsClient.
932 * This can be overridden for testing.
934 * The offset is the number of milliseconds that must be added to the client
935 * clock to make it equal to the server clock. For example, if the client is
936 * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
938 get localtimeOffsetMsec() {
939 return this.fxAccountsClient.localtimeOffsetMsec;
943 * Ask the server whether the user's email has been verified
945 checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) {
947 return Promise.reject(
948 new Error("checkEmailStatus called without a session token")
951 return this.fxAccountsClient
952 .recoveryEmailStatus(sessionToken, options)
953 .catch(error => this._handleTokenError(error));
956 // set() makes sure that polling is happening, if necessary.
957 // get() does not wait for verification, and returns an object even if
958 // unverified. The caller of get() must check .verified .
959 // The "fxaccounts:onverified" event will fire only when the verified
960 // state goes from false to true, so callers must register their observer
961 // and then call get(). In particular, it will not fire when the account
962 // was found to be verified in a previous boot: if our stored state says
963 // the account is verified, the event will never fire. So callers must do:
964 // register notification observer (go)
966 // if (userdata.verified()) {go()}
969 * Set the current user signed in to Firefox Accounts.
972 * The credentials object obtained by logging in or creating
973 * an account on the FxA server:
975 * authAt: The time (seconds since epoch) that this record was
977 * email: The users email address
978 * keyFetchToken: a keyFetchToken which has not yet been used
979 * sessionToken: Session for the FxA server
980 * uid: The user's unique id
981 * unwrapBKey: used to unwrap kB, derived locally from the
982 * password (not revealed to the FxA server)
983 * verified: true/false
986 * The promise resolves to null when the data is saved
987 * successfully and is rejected on error.
989 async setSignedInUser(credentials) {
990 if (!lazy.FXA_ENABLED) {
991 throw new Error("Cannot call setSignedInUser when FxA is disabled.");
993 for (const pref of Services.prefs.getChildList(PREF_ACCOUNT_ROOT)) {
994 Services.prefs.clearUserPref(pref);
996 log.debug("setSignedInUser - aborting any existing flows");
997 const signedInUser = await this.currentAccountState.getUserAccountData();
999 await this._signOutServer(
1000 signedInUser.sessionToken,
1001 signedInUser.oauthTokens
1004 await this.abortExistingFlow();
1005 const currentAccountState = (this.currentAccountState =
1006 this.newAccountState(
1007 Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object.
1009 // This promise waits for storage, but not for verification.
1010 // We're telling the caller that this is durable now (although is that
1011 // really something we should commit to? Why not let the write happen in
1012 // the background? Already does for updateAccountData ;)
1013 await currentAccountState.promiseInitialized;
1014 // Starting point for polling if new user
1015 if (!this.isUserEmailVerified(credentials) && !lazy.oauthEnabled) {
1016 this.startVerifiedCheck(credentials);
1018 await this.notifyObservers(ONLOGIN_NOTIFICATION);
1019 await this.updateDeviceRegistration();
1020 return currentAccountState.resolve();
1024 * Update account data for the currently signed in user.
1026 * @param credentials
1027 * The credentials object containing the fields to be updated.
1028 * This object must contain the |uid| field and it must
1029 * match the currently signed in user.
1031 updateUserAccountData(credentials) {
1033 "updateUserAccountData called with fields",
1034 Object.keys(credentials)
1037 log.debug("updateUserAccountData called with data", credentials);
1039 let currentAccountState = this.currentAccountState;
1040 return currentAccountState.promiseInitialized.then(() => {
1041 if (!credentials.uid) {
1042 throw new Error("The specified credentials have no uid");
1044 return currentAccountState.updateUserAccountData(credentials);
1049 * Reset state such that any previous flow is canceled.
1051 abortExistingFlow() {
1052 if (this.currentTimer) {
1053 log.debug("Polling aborted; Another user signing in");
1054 clearTimeout(this.currentTimer);
1055 this.currentTimer = 0;
1057 if (this._profile) {
1058 this._profile.tearDown();
1059 this._profile = null;
1061 if (this._commands) {
1062 this._commands = null;
1065 this._device.reset();
1067 // We "abort" the accountState and assume our caller is about to throw it
1068 // away and replace it with a new one.
1069 return this.currentAccountState.abort();
1072 async checkVerificationStatus() {
1073 log.trace("checkVerificationStatus");
1074 let state = this.currentAccountState;
1075 let data = await state.getUserAccountData();
1077 log.trace("checkVerificationStatus - no user data");
1081 // Always check the verification status, even if the local state indicates
1082 // we're already verified. If the user changed their password, the check
1083 // will fail, and we'll enter the reauth state.
1084 log.trace("checkVerificationStatus - forcing verification status check");
1085 return this.startPollEmailStatus(state, data.sessionToken, "push");
1088 /** Destroyes an OAuth Token by sending a request to the FxA server
1089 * @param { Object } tokenData: The token's data, with `tokenData.token` being the token itself
1091 destroyOAuthToken(tokenData) {
1092 return this.fxAccountsClient.oauthDestroy(OAUTH_CLIENT_ID, tokenData.token);
1095 _destroyAllOAuthTokens(tokenInfos) {
1097 return Promise.resolve();
1099 // let's just destroy them all in parallel...
1101 for (let tokenInfo of Object.values(tokenInfos)) {
1102 promises.push(this.destroyOAuthToken(tokenInfo));
1104 return Promise.all(promises);
1107 async signOut(localOnly) {
1110 const data = await this.currentAccountState.getUserAccountData();
1111 // Save the sessionToken, tokens before resetting them in _signOutLocal().
1113 sessionToken = data.sessionToken;
1114 tokensToRevoke = data.oauthTokens;
1116 await this.notifyObservers(ON_PRELOGOUT_NOTIFICATION);
1117 await this._signOutLocal();
1119 // Do this in the background so *any* slow request won't
1120 // block the local sign out.
1121 Services.tm.dispatchToMainThread(async () => {
1122 await this._signOutServer(sessionToken, tokensToRevoke);
1123 lazy.FxAccountsConfig.resetConfigURLs();
1124 this.notifyObservers("testhelper-fxa-signout-complete");
1127 // We want to do this either way -- but if we're signing out remotely we
1128 // need to wait until we destroy the oauth tokens if we want that to succeed.
1129 lazy.FxAccountsConfig.resetConfigURLs();
1131 return this.notifyObservers(ONLOGOUT_NOTIFICATION);
1134 async _signOutLocal() {
1135 for (const pref of Services.prefs.getChildList(PREF_ACCOUNT_ROOT)) {
1136 Services.prefs.clearUserPref(pref);
1138 await this.currentAccountState.signOut();
1139 // this "aborts" this.currentAccountState but doesn't make a new one.
1140 await this.abortExistingFlow();
1141 this.currentAccountState = this.newAccountState();
1142 return this.currentAccountState.promiseInitialized;
1145 async _signOutServer(sessionToken, tokensToRevoke) {
1146 log.debug("Unsubscribing from FxA push.");
1148 await this.fxaPushService.unsubscribe();
1150 log.error("Could not unsubscribe from push.", err);
1153 log.debug("Destroying session and device.");
1155 await this.fxAccountsClient.signOut(sessionToken, { service: "sync" });
1157 log.error("Error during remote sign out of Firefox Accounts", err);
1160 log.warn("Missing session token; skipping remote sign out");
1162 log.debug("Destroying all OAuth tokens.");
1164 await this._destroyAllOAuthTokens(tokensToRevoke);
1166 log.error("Error during destruction of oauth tokens during signout", err);
1170 getUserAccountData(fieldNames = null) {
1171 return this.currentAccountState.getUserAccountData(fieldNames);
1174 isUserEmailVerified: function isUserEmailVerified(data) {
1175 return !!(data && data.verified);
1179 * Setup for and if necessary do email verification polling.
1182 let currentState = this.currentAccountState;
1183 return currentState.getUserAccountData().then(data => {
1185 if (!this.isUserEmailVerified(data)) {
1186 this.startPollEmailStatus(
1197 startVerifiedCheck(data) {
1198 log.debug("startVerifiedCheck", data && data.verified);
1200 log.debug("startVerifiedCheck with user data", data);
1203 // Get us to the verified state. This returns a promise that will fire when
1204 // verification is complete.
1206 // The callers of startVerifiedCheck never consume a returned promise (ie,
1207 // this is simply kicking off a background fetch) so we must add a rejection
1208 // handler to avoid runtime warnings about the rejection not being handled.
1209 this.whenVerified(data).catch(err =>
1210 log.info("startVerifiedCheck promise was rejected: " + err)
1214 whenVerified(data) {
1215 let currentState = this.currentAccountState;
1216 if (data.verified) {
1217 log.debug("already verified");
1218 return currentState.resolve(data);
1220 if (!currentState.whenVerifiedDeferred) {
1221 log.debug("whenVerified promise starts polling for verified email");
1222 this.startPollEmailStatus(currentState, data.sessionToken, "start");
1224 return currentState.whenVerifiedDeferred.promise.then(result =>
1225 currentState.resolve(result)
1229 async notifyObservers(topic, data) {
1230 for (let f of this.observerPreloads) {
1235 log.debug("Notifying observers of " + topic);
1236 Services.obs.notifyObservers(null, topic, data);
1239 startPollEmailStatus(currentState, sessionToken, why) {
1240 log.debug("entering startPollEmailStatus: " + why);
1241 // If we were already polling, stop and start again. This could happen
1242 // if the user requested the verification email to be resent while we
1243 // were already polling for receipt of an earlier email.
1244 if (this.currentTimer) {
1246 "startPollEmailStatus starting while existing timer is running"
1248 clearTimeout(this.currentTimer);
1249 this.currentTimer = null;
1252 this.pollStartDate = Date.now();
1253 if (!currentState.whenVerifiedDeferred) {
1254 currentState.whenVerifiedDeferred = Promise.withResolvers();
1255 // This deferred might not end up with any handlers (eg, if sync
1256 // is yet to start up.) This might cause "A promise chain failed to
1257 // handle a rejection" messages, so add an error handler directly
1258 // on the promise to log the error.
1259 currentState.whenVerifiedDeferred.promise.then(
1261 log.info("the user became verified");
1262 // We are now ready for business. This should only be invoked once
1263 // per setSignedInUser(), regardless of whether we've rebooted since
1264 // setSignedInUser() was called.
1265 this.notifyObservers(ONVERIFIED_NOTIFICATION);
1268 log.info("the wait for user verification was stopped: " + err);
1272 return this.pollEmailStatus(currentState, sessionToken, why);
1275 // We return a promise for testing only. Other callers can ignore this,
1276 // since verification polling continues in the background.
1277 async pollEmailStatus(currentState, sessionToken, why) {
1278 log.debug("entering pollEmailStatus: " + why);
1281 const response = await this.checkEmailStatus(sessionToken, {
1284 log.debug("checkEmailStatus -> " + JSON.stringify(response));
1285 if (response && response.verified) {
1286 await this.onPollEmailSuccess(currentState);
1290 if (error && error.code && error.code == 401) {
1291 let error = new Error("Verification status check failed");
1292 this._rejectWhenVerified(currentState, error);
1295 if (error && error.retryAfter) {
1296 // If the server told us to back off, back off the requested amount.
1297 nextPollMs = (error.retryAfter + 3) * 1000;
1299 `the server rejected our email status check and told us to try again in ${nextPollMs}ms`
1302 log.error(`checkEmailStatus failed to poll`, error);
1305 if (why == "push") {
1308 let pollDuration = Date.now() - this.pollStartDate;
1309 // Polling session expired.
1310 if (pollDuration >= this.POLL_SESSION) {
1311 if (currentState.whenVerifiedDeferred) {
1312 let error = new Error("User email verification timed out.");
1313 this._rejectWhenVerified(currentState, error);
1315 log.debug("polling session exceeded, giving up");
1318 // Poll email status again after a short delay.
1319 if (nextPollMs === undefined) {
1320 let currentMinute = Math.ceil(pollDuration / 60000);
1323 currentMinute < this.VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD
1324 ? this.VERIFICATION_POLL_TIMEOUT_INITIAL
1325 : this.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT;
1327 this._scheduleNextPollEmailStatus(
1335 // Easy-to-mock testable method
1336 _scheduleNextPollEmailStatus(currentState, sessionToken, nextPollMs, why) {
1337 log.debug("polling with timeout = " + nextPollMs);
1338 this.currentTimer = setTimeout(() => {
1339 this.pollEmailStatus(currentState, sessionToken, why);
1343 async onPollEmailSuccess(currentState) {
1345 await currentState.updateUserAccountData({ verified: true });
1346 const accountData = await currentState.getUserAccountData();
1347 this._setLastUserPref(accountData.email);
1348 // Now that the user is verified, we can proceed to fetch keys
1349 if (currentState.whenVerifiedDeferred) {
1350 currentState.whenVerifiedDeferred.resolve(accountData);
1351 delete currentState.whenVerifiedDeferred;
1358 _rejectWhenVerified(currentState, error) {
1359 currentState.whenVerifiedDeferred.reject(error);
1360 delete currentState.whenVerifiedDeferred;
1364 * Does the actual fetch of an oauth token for getOAuthToken()
1365 * using the account session token.
1367 * It's split out into a separate method so that we can easily
1368 * stash in-flight calls in a cache.
1370 * @param {String} scopeString
1371 * @param {Number} ttl
1372 * @returns {Promise<string>}
1375 async _doTokenFetchWithSessionToken(sessionToken, scopeString, ttl) {
1376 const result = await this.fxAccountsClient.accessTokenWithSessionToken(
1382 return result.access_token;
1385 getOAuthToken(options = {}) {
1386 log.debug("getOAuthToken enter");
1387 let scope = options.scope;
1388 if (typeof scope === "string") {
1392 if (!scope || !scope.length) {
1393 return Promise.reject(
1395 ERROR_INVALID_PARAMETER,
1396 "Missing or invalid 'scope' option"
1401 return this.withSessionToken(async (sessionToken, currentState) => {
1402 // Early exit for a cached token.
1403 let cached = currentState.getCachedToken(scope);
1405 log.debug("getOAuthToken returning a cached token");
1406 return cached.token;
1409 // Build the string we use in our "inflight" map and that we send to the
1410 // server. Because it's used as a key in the map we sort the scopes.
1411 let scopeString = scope.sort().join(" ");
1413 // We keep a map of in-flight requests to avoid multiple promise-based
1414 // consumers concurrently requesting the same token.
1415 let maybeInFlight = currentState.inFlightTokenRequests.get(scopeString);
1416 if (maybeInFlight) {
1417 log.debug("getOAuthToken has an in-flight request for this scope");
1418 return maybeInFlight;
1421 // We need to start a new fetch and stick the promise in our in-flight map
1422 // and remove it when it resolves.
1423 let promise = this._doTokenFetchWithSessionToken(
1429 // As a sanity check, ensure something else hasn't raced getting a token
1430 // of the same scope. If something has we just make noise rather than
1431 // taking any concrete action because it should never actually happen.
1432 if (currentState.getCachedToken(scope)) {
1433 log.error(`detected a race for oauth token with scope ${scope}`);
1435 // If we got one, cache it.
1437 let entry = { token };
1438 currentState.setCachedToken(scope, entry);
1443 // Remove ourself from the in-flight map. There's no need to check the
1444 // result of .delete() to handle a signout race, because setCachedToken
1445 // above will fail in that case and cause the entire call to fail.
1446 currentState.inFlightTokenRequests.delete(scopeString);
1449 currentState.inFlightTokenRequests.set(scopeString, promise);
1455 * Remove an OAuth token from the token cache
1456 * and makes a network request to FxA server to destroy the token.
1460 * token: (string) A previously fetched token.
1462 * @return Promise.<undefined> This function will always resolve, even if
1463 * an unknown token is passed.
1465 removeCachedOAuthToken(options) {
1466 if (!options.token || typeof options.token !== "string") {
1468 ERROR_INVALID_PARAMETER,
1469 "Missing or invalid 'token' option"
1472 return this.withCurrentAccountState(currentState => {
1473 let existing = currentState.removeCachedToken(options.token);
1475 // background destroy.
1476 this.destroyOAuthToken(existing).catch(err => {
1477 log.warn("FxA failed to revoke a cached token", err);
1483 /** Sets the user to be verified in the account state,
1484 * This prevents any polling for the user's verification state from the FxA server
1486 async setUserVerified() {
1487 await this.withCurrentAccountState(async currentState => {
1488 const userData = await currentState.getUserAccountData();
1489 if (!userData.verified) {
1490 await currentState.updateUserAccountData({ verified: true });
1493 await this.notifyObservers(ONVERIFIED_NOTIFICATION);
1496 async _getVerifiedAccountOrReject() {
1497 let data = await this.currentAccountState.getUserAccountData();
1499 // No signed-in user
1500 throw this._error(ERROR_NO_ACCOUNT);
1502 if (!this.isUserEmailVerified(data)) {
1503 // Signed-in user has not verified email
1504 throw this._error(ERROR_UNVERIFIED_ACCOUNT);
1509 // _handle* methods used by push, used when the account/device status is
1510 // changed on a different device.
1511 async _handleAccountDestroyed(uid) {
1512 let state = this.currentAccountState;
1513 const accountData = await state.getUserAccountData();
1514 const localUid = accountData ? accountData.uid : null;
1517 `Account destroyed push notification received, but we're already logged-out`
1521 if (uid == localUid) {
1522 const data = JSON.stringify({ isLocalDevice: true });
1523 await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
1524 return this.signOut(true);
1527 `The destroyed account uid doesn't match with the local uid. ` +
1528 `Local: ${localUid}, account uid destroyed: ${uid}`
1533 async _handleDeviceDisconnection(deviceId) {
1534 let state = this.currentAccountState;
1535 const accountData = await state.getUserAccountData();
1536 if (!accountData || !accountData.device) {
1537 // Nothing we can do here.
1540 const localDeviceId = accountData.device.id;
1541 const isLocalDevice = deviceId == localDeviceId;
1542 if (isLocalDevice) {
1545 const data = JSON.stringify({ isLocalDevice });
1546 await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
1549 _setLastUserPref(newEmail) {
1550 Services.prefs.setStringPref(
1552 CryptoUtils.sha256Base64(newEmail)
1556 async _handleEmailUpdated(newEmail) {
1557 this._setLastUserPref(newEmail);
1558 await this.currentAccountState.updateUserAccountData({ email: newEmail });
1562 * Coerce an error into one of the general error cases:
1567 * These errors will pass through:
1570 * UNVERIFIED_ACCOUNT
1572 _errorToErrorClass(aError) {
1574 let error = SERVER_ERRNO_TO_ERROR[aError.errno];
1576 ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN,
1581 (aError.message === "INVALID_PARAMETER" ||
1582 aError.message === "NO_ACCOUNT" ||
1583 aError.message === "UNVERIFIED_ACCOUNT" ||
1584 aError.message === "AUTH_ERROR")
1588 return this._error(ERROR_UNKNOWN, aError);
1591 _error(aError, aDetails) {
1592 log.error("FxA rejecting with error ${aError}, details: ${aDetails}", {
1596 let reason = new Error(aError);
1598 reason.details = aDetails;
1603 // Attempt to update the auth server with whatever device details are stored
1604 // in the account data. Returns a promise that always resolves, never rejects.
1605 // If the promise resolves to a value, that value is the device id.
1606 updateDeviceRegistration() {
1607 return this.device.updateDeviceRegistration();
1611 * Delete all the persisted credentials we store for FxA. After calling
1612 * this, the user will be forced to re-authenticate to continue.
1614 * @return Promise resolves when the user data has been persisted
1616 dropCredentials(state) {
1617 // Delete all fields except those required for the user to
1619 let updateData = {};
1620 let clearField = field => {
1621 if (!FXA_PWDMGR_REAUTH_ALLOWLIST.has(field)) {
1622 updateData[field] = null;
1625 FXA_PWDMGR_PLAINTEXT_FIELDS.forEach(clearField);
1626 FXA_PWDMGR_SECURE_FIELDS.forEach(clearField);
1628 return state.updateUserAccountData(updateData);
1631 async checkAccountStatus(state) {
1632 log.info("checking account status...");
1633 let data = await state.getUserAccountData(["uid", "sessionToken"]);
1635 log.info("account status: no user");
1638 // If we have a session token, then check if that remains valid - if this
1639 // works we know the account must also be OK.
1640 if (data.sessionToken) {
1641 if (await this.fxAccountsClient.sessionStatus(data.sessionToken)) {
1642 log.info("account status: ok");
1646 let exists = await this.fxAccountsClient.accountStatus(data.uid);
1648 // Delete all local account data. Since the account no longer
1649 // exists, we can skip the remote calls.
1650 log.info("account status: deleted");
1651 await this._handleAccountDestroyed(data.uid);
1653 // Note that we may already have been in a "needs reauth" state (ie, if
1654 // this function was called when we already had no session token), but
1655 // that's OK - re-notifying etc should cause no harm.
1656 log.info("account status: needs reauthentication");
1657 await this.dropCredentials(this.currentAccountState);
1658 // Notify the account state has changed so the UI updates.
1659 await this.notifyObservers(ON_ACCOUNT_STATE_CHANGE_NOTIFICATION);
1664 async _handleTokenError(err) {
1665 if (!err || err.code != 401 || err.errno != ERRNO_INVALID_AUTH_TOKEN) {
1668 log.warn("handling invalid token error", err);
1669 // Note that we don't use `withCurrentAccountState` here as that will cause
1670 // an error to be thrown if we sign out due to the account not existing.
1671 let state = this.currentAccountState;
1672 let ok = await this.checkAccountStatus(state);
1674 log.warn("invalid token error, but account state appears ok?");
1676 // always re-throw the error.
1681 let fxAccountsSingleton = null;
1683 export function getFxAccountsSingleton() {
1684 if (fxAccountsSingleton) {
1685 return fxAccountsSingleton;
1688 fxAccountsSingleton = new FxAccounts();
1690 // XXX Bug 947061 - We need a strategy for resuming email verification after
1692 fxAccountsSingleton._internal.loadAndPoll();
1694 return fxAccountsSingleton;
1697 // `AccountState` is exported for tests.