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