FxAccountsClient.sys.mjs (26209B)
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 { CommonUtils } from "resource://services-common/utils.sys.mjs"; 6 7 import { HawkClient } from "resource://services-common/hawkclient.sys.mjs"; 8 import { deriveHawkCredentials } from "resource://services-common/hawkrequest.sys.mjs"; 9 import { CryptoUtils } from "moz-src:///services/crypto/modules/utils.sys.mjs"; 10 11 import { 12 ERRNO_ACCOUNT_DOES_NOT_EXIST, 13 ERRNO_INCORRECT_EMAIL_CASE, 14 ERRNO_INCORRECT_PASSWORD, 15 ERRNO_INVALID_AUTH_NONCE, 16 ERRNO_INVALID_AUTH_TIMESTAMP, 17 ERRNO_INVALID_AUTH_TOKEN, 18 log, 19 } from "resource://gre/modules/FxAccountsCommon.sys.mjs"; 20 21 import { Credentials } from "resource://gre/modules/Credentials.sys.mjs"; 22 23 const HOST_PREF = "identity.fxaccounts.auth.uri"; 24 25 const SIGNIN = "/account/login"; 26 const SIGNUP = "/account/create"; 27 // Devices older than this many days will not appear in the devices list 28 const DEVICES_FILTER_DAYS = 21; 29 30 export var FxAccountsClient = function ( 31 host = Services.prefs.getStringPref(HOST_PREF) 32 ) { 33 this.host = host; 34 35 // The FxA auth server expects requests to certain endpoints to be authorized 36 // using Hawk. 37 this.hawk = new HawkClient(host); 38 this.hawk.observerPrefix = "FxA:hawk"; 39 40 // Manage server backoff state. C.f. 41 // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol 42 this.backoffError = null; 43 }; 44 45 FxAccountsClient.prototype = { 46 /** 47 * Return client clock offset, in milliseconds, as determined by hawk client. 48 * Provided because callers should not have to know about hawk 49 * implementation. 50 * 51 * The offset is the number of milliseconds that must be added to the client 52 * clock to make it equal to the server clock. For example, if the client is 53 * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. 54 */ 55 get localtimeOffsetMsec() { 56 return this.hawk.localtimeOffsetMsec; 57 }, 58 59 /* 60 * Return current time in milliseconds 61 * 62 * Not used by this module, but made available to the FxAccounts.sys.mjs 63 * that uses this client. 64 */ 65 now() { 66 return this.hawk.now(); 67 }, 68 69 /** 70 * Common code from signIn and signUp. 71 * 72 * @param path 73 * Request URL path. Can be /account/create or /account/login 74 * @param email 75 * The email address for the account (utf8) 76 * @param password 77 * The user's password 78 * @param [getKeys=false] 79 * If set to true the keyFetchToken will be retrieved 80 * @param [retryOK=true] 81 * If capitalization of the email is wrong and retryOK is set to true, 82 * we will retry with the suggested capitalization from the server 83 * @return Promise 84 * Returns a promise that resolves to an object: 85 * { 86 * authAt: authentication time for the session (seconds since epoch) 87 * email: the primary email for this account 88 * keyFetchToken: a key fetch token (hex) 89 * sessionToken: a session token (hex) 90 * uid: the user's unique ID (hex) 91 * unwrapBKey: used to unwrap kB, derived locally from the 92 * password (not revealed to the FxA server) 93 * verified (optional): flag indicating verification status of the 94 * email 95 * } 96 */ 97 _createSession(path, email, password, getKeys = false, retryOK = true) { 98 return Credentials.setup(email, password).then(creds => { 99 let data = { 100 authPW: CommonUtils.bytesAsHex(creds.authPW), 101 email, 102 }; 103 let keys = getKeys ? "?keys=true" : ""; 104 105 return this._request(path + keys, "POST", null, data).then( 106 // Include the canonical capitalization of the email in the response so 107 // the caller can set its signed-in user state accordingly. 108 result => { 109 result.email = data.email; 110 result.unwrapBKey = CommonUtils.bytesAsHex(creds.unwrapBKey); 111 112 return result; 113 }, 114 error => { 115 log.debug("Session creation failed", error); 116 // If the user entered an email with different capitalization from 117 // what's stored in the database (e.g., Greta.Garbo@gmail.COM as 118 // opposed to greta.garbo@gmail.com), the server will respond with a 119 // errno 120 (code 400) and the expected capitalization of the email. 120 // We retry with this email exactly once. If successful, we use the 121 // server's version of the email as the signed-in-user's email. This 122 // is necessary because the email also serves as salt; so we must be 123 // in agreement with the server on capitalization. 124 // 125 // API reference: 126 // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md 127 if (ERRNO_INCORRECT_EMAIL_CASE === error.errno && retryOK) { 128 if (!error.email) { 129 log.error("Server returned errno 120 but did not provide email"); 130 throw error; 131 } 132 return this._createSession( 133 path, 134 error.email, 135 password, 136 getKeys, 137 false 138 ); 139 } 140 throw error; 141 } 142 ); 143 }); 144 }, 145 146 /** 147 * Create a new Firefox Account and authenticate 148 * 149 * @param email 150 * The email address for the account (utf8) 151 * @param password 152 * The user's password 153 * @param [getKeys=false] 154 * If set to true the keyFetchToken will be retrieved 155 * @return Promise 156 * Returns a promise that resolves to an object: 157 * { 158 * uid: the user's unique ID (hex) 159 * sessionToken: a session token (hex) 160 * keyFetchToken: a key fetch token (hex), 161 * unwrapBKey: used to unwrap kB, derived locally from the 162 * password (not revealed to the FxA server) 163 * } 164 */ 165 signUp(email, password, getKeys = false) { 166 return this._createSession( 167 SIGNUP, 168 email, 169 password, 170 getKeys, 171 false /* no retry */ 172 ); 173 }, 174 175 /** 176 * Authenticate and create a new session with the Firefox Account API server 177 * 178 * @param email 179 * The email address for the account (utf8) 180 * @param password 181 * The user's password 182 * @param [getKeys=false] 183 * If set to true the keyFetchToken will be retrieved 184 * @return Promise 185 * Returns a promise that resolves to an object: 186 * { 187 * authAt: authentication time for the session (seconds since epoch) 188 * email: the primary email for this account 189 * keyFetchToken: a key fetch token (hex) 190 * sessionToken: a session token (hex) 191 * uid: the user's unique ID (hex) 192 * unwrapBKey: used to unwrap kB, derived locally from the 193 * password (not revealed to the FxA server) 194 * verified: flag indicating verification status of the email 195 * } 196 */ 197 signIn: function signIn(email, password, getKeys = false) { 198 return this._createSession( 199 SIGNIN, 200 email, 201 password, 202 getKeys, 203 true /* retry */ 204 ); 205 }, 206 207 /** 208 * Check the status of a session given a session token 209 * 210 * @param sessionTokenHex 211 * The session token encoded in hex 212 * @return Promise 213 * Resolves with a boolean indicating if the session is still valid 214 */ 215 async sessionStatus(sessionTokenHex) { 216 const credentials = await deriveHawkCredentials( 217 sessionTokenHex, 218 "sessionToken" 219 ); 220 return this._request("/session/status", "GET", credentials).then( 221 () => Promise.resolve(true), 222 error => { 223 if (isInvalidTokenError(error)) { 224 return Promise.resolve(false); 225 } 226 throw error; 227 } 228 ); 229 }, 230 231 /** 232 * List all the clients connected to the authenticated user's account, 233 * including devices, OAuth clients, and web sessions. 234 * 235 * @param sessionTokenHex 236 * The session token encoded in hex 237 * @return Promise 238 */ 239 async attachedClients(sessionTokenHex) { 240 const credentials = await deriveHawkCredentials( 241 sessionTokenHex, 242 "sessionToken" 243 ); 244 return this._requestWithHeaders( 245 "/account/attached_clients", 246 "GET", 247 credentials 248 ); 249 }, 250 251 /** 252 * Retrieves an OAuth authorization code. 253 * 254 * @param String sessionTokenHex 255 * The session token encoded in hex 256 * @param {object} options 257 * @param options.client_id 258 * @param options.state 259 * @param options.scope 260 * @param options.access_type 261 * @param options.code_challenge_method 262 * @param options.code_challenge 263 * @param [options.keys_jwe] 264 * @returns {Promise<object>} Object containing `code` and `state`. 265 */ 266 async oauthAuthorize(sessionTokenHex, options) { 267 const credentials = await deriveHawkCredentials( 268 sessionTokenHex, 269 "sessionToken" 270 ); 271 const body = { 272 client_id: options.client_id, 273 response_type: "code", 274 state: options.state, 275 scope: options.scope, 276 access_type: options.access_type, 277 code_challenge: options.code_challenge, 278 code_challenge_method: options.code_challenge_method, 279 }; 280 if (options.keys_jwe) { 281 body.keys_jwe = options.keys_jwe; 282 } 283 return this._request("/oauth/authorization", "POST", credentials, body); 284 }, 285 /** 286 * Exchanges an OAuth authorization code with a refresh token, access tokens and an optional JWE representing scoped keys 287 * Takes in the sessionToken to tie the device record associated with the session, with the device record associated with the refreshToken 288 * 289 * @param string sessionTokenHex: The session token encoded in hex 290 * @param String code: OAuth authorization code 291 * @param String verifier: OAuth PKCE verifier 292 * @param String clientId: OAuth client ID 293 * 294 * @returns {object} object containing `refresh_token`, `access_token` and `keys_jwe` 295 */ 296 async oauthToken(sessionTokenHex, code, verifier, clientId) { 297 const credentials = await deriveHawkCredentials( 298 sessionTokenHex, 299 "sessionToken" 300 ); 301 const body = { 302 grant_type: "authorization_code", 303 code, 304 client_id: clientId, 305 code_verifier: verifier, 306 }; 307 return this._request("/oauth/token", "POST", credentials, body); 308 }, 309 /** 310 * Destroy an OAuth access token or refresh token. 311 * 312 * @param String clientId 313 * @param String token The token to be revoked. 314 */ 315 async oauthDestroy(clientId, token) { 316 const body = { 317 client_id: clientId, 318 token, 319 }; 320 return this._request("/oauth/destroy", "POST", null, body); 321 }, 322 323 /** 324 * Query for the information required to derive 325 * scoped encryption keys requested by the specified OAuth client. 326 * 327 * @param sessionTokenHex 328 * The session token encoded in hex 329 * @param clientId 330 * @param scope 331 * Space separated list of scopes 332 * @return Promise 333 */ 334 async getScopedKeyData(sessionTokenHex, clientId, scope) { 335 if (!clientId) { 336 throw new Error("Missing 'clientId' parameter"); 337 } 338 if (!scope) { 339 throw new Error("Missing 'scope' parameter"); 340 } 341 const params = { 342 client_id: clientId, 343 scope, 344 }; 345 const credentials = await deriveHawkCredentials( 346 sessionTokenHex, 347 "sessionToken" 348 ); 349 return this._request( 350 "/account/scoped-key-data", 351 "POST", 352 credentials, 353 params 354 ); 355 }, 356 357 /** 358 * Destroy the current session with the Firefox Account API server and its 359 * associated device. 360 * 361 * @param sessionTokenHex 362 * The session token encoded in hex 363 * @return Promise 364 */ 365 async signOut(sessionTokenHex, options = {}) { 366 const credentials = await deriveHawkCredentials( 367 sessionTokenHex, 368 "sessionToken" 369 ); 370 let path = "/session/destroy"; 371 if (options.service) { 372 path += "?service=" + encodeURIComponent(options.service); 373 } 374 return this._request(path, "POST", credentials); 375 }, 376 377 /** 378 * Check the verification status of the user's FxA email address 379 * 380 * @param sessionTokenHex 381 * The current session token encoded in hex 382 * @return Promise 383 */ 384 async recoveryEmailStatus(sessionTokenHex, options = {}) { 385 const credentials = await deriveHawkCredentials( 386 sessionTokenHex, 387 "sessionToken" 388 ); 389 let path = "/recovery_email/status"; 390 if (options.reason) { 391 path += "?reason=" + encodeURIComponent(options.reason); 392 } 393 394 return this._request(path, "GET", credentials); 395 }, 396 397 /** 398 * Resend the verification email for the user 399 * 400 * @param sessionTokenHex 401 * The current token encoded in hex 402 * @return Promise 403 */ 404 async resendVerificationEmail(sessionTokenHex) { 405 const credentials = await deriveHawkCredentials( 406 sessionTokenHex, 407 "sessionToken" 408 ); 409 return this._request("/recovery_email/resend_code", "POST", credentials); 410 }, 411 412 /** 413 * Retrieve encryption keys 414 * 415 * @param keyFetchTokenHex 416 * A one-time use key fetch token encoded in hex 417 * @return Promise 418 * Returns a promise that resolves to an object: 419 * { 420 * kA: an encryption key for recevorable data (bytes) 421 * wrapKB: an encryption key that requires knowledge of the 422 * user's password (bytes) 423 * } 424 */ 425 async accountKeys(keyFetchTokenHex) { 426 let creds = await deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken"); 427 let keyRequestKey = creds.extra.slice(0, 32); 428 let morecreds = await CryptoUtils.hkdfLegacy( 429 keyRequestKey, 430 undefined, 431 Credentials.keyWord("account/keys"), 432 3 * 32 433 ); 434 let respHMACKey = morecreds.slice(0, 32); 435 let respXORKey = morecreds.slice(32, 96); 436 437 const resp = await this._request("/account/keys", "GET", creds); 438 if (!resp.bundle) { 439 throw new Error("failed to retrieve keys"); 440 } 441 442 let bundle = CommonUtils.hexToBytes(resp.bundle); 443 let mac = bundle.slice(-32); 444 let key = CommonUtils.byteStringToArrayBuffer(respHMACKey); 445 // CryptoUtils.hmac takes ArrayBuffers as inputs for the key and data and 446 // returns an ArrayBuffer. 447 let bundleMAC = await CryptoUtils.hmac( 448 "SHA-256", 449 key, 450 CommonUtils.byteStringToArrayBuffer(bundle.slice(0, -32)) 451 ); 452 if (mac !== CommonUtils.arrayBufferToByteString(bundleMAC)) { 453 throw new Error("error unbundling encryption keys"); 454 } 455 456 let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64)); 457 458 return { 459 kA: keyAWrapB.slice(0, 32), 460 wrapKB: keyAWrapB.slice(32), 461 }; 462 }, 463 464 /** 465 * Obtain an OAuth access token by authenticating using a session token. 466 * 467 * @param {string} sessionTokenHex 468 * The session token encoded in hex 469 * @param {string} clientId 470 * @param {string} scope 471 * List of space-separated scopes. 472 * @param {number} ttl 473 * Token time to live. 474 * @return {Promise<object>} Object containing an `access_token`. 475 */ 476 async accessTokenWithSessionToken(sessionTokenHex, clientId, scope, ttl) { 477 const credentials = await deriveHawkCredentials( 478 sessionTokenHex, 479 "sessionToken" 480 ); 481 const body = { 482 client_id: clientId, 483 grant_type: "fxa-credentials", 484 scope, 485 ttl, 486 }; 487 return this._request("/oauth/token", "POST", credentials, body); 488 }, 489 490 /** 491 * Determine if an account exists 492 * 493 * @param email 494 * The email address to check 495 * @return Promise 496 * The promise resolves to true if the account exists, or false 497 * if it doesn't. The promise is rejected on other errors. 498 */ 499 accountExists(email) { 500 return this.signIn(email, "").then( 501 () => { 502 throw new Error("How did I sign in with an empty password?"); 503 }, 504 expectedError => { 505 switch (expectedError.errno) { 506 case ERRNO_ACCOUNT_DOES_NOT_EXIST: 507 return false; 508 case ERRNO_INCORRECT_PASSWORD: 509 return true; 510 default: 511 // not so expected, any more ... 512 throw expectedError; 513 } 514 } 515 ); 516 }, 517 518 /** 519 * Given the uid of an existing account (not an arbitrary email), ask 520 * the server if it still exists via /account/status. 521 * 522 * Used for differentiating between password change and account deletion. 523 */ 524 accountStatus(uid) { 525 return this._request("/account/status?uid=" + uid, "GET").then( 526 result => { 527 return result.exists; 528 }, 529 error => { 530 log.error("accountStatus failed", error); 531 return Promise.reject(error); 532 } 533 ); 534 }, 535 536 /** 537 * Register a new device 538 * 539 * @function registerDevice 540 * @param sessionTokenHex 541 * Session token obtained from signIn 542 * @param name 543 * Device name 544 * @param type 545 * Device type (mobile|desktop) 546 * @param [options] 547 * Extra device options 548 * @param [options.availableCommands] 549 * Available commands for this device 550 * @param [options.pushCallback] 551 * `pushCallback` push endpoint callback 552 * @param [options.pushPublicKey] 553 * `pushPublicKey` push public key (URLSafe Base64 string) 554 * @param [options.pushAuthKey] 555 * `pushAuthKey` push auth secret (URLSafe Base64 string) 556 * @return Promise 557 * Resolves to an object: 558 * { 559 * id: Device identifier 560 * createdAt: Creation time (milliseconds since epoch) 561 * name: Name of device 562 * type: Type of device (mobile|desktop) 563 * } 564 */ 565 async registerDevice(sessionTokenHex, name, type, options = {}) { 566 let path = "/account/device"; 567 568 let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken"); 569 let body = { name, type }; 570 571 if (options.pushCallback) { 572 body.pushCallback = options.pushCallback; 573 } 574 if (options.pushPublicKey && options.pushAuthKey) { 575 body.pushPublicKey = options.pushPublicKey; 576 body.pushAuthKey = options.pushAuthKey; 577 } 578 body.availableCommands = options.availableCommands; 579 580 return this._request(path, "POST", creds, body); 581 }, 582 583 /** 584 * Sends a message to other devices. Must conform with the push payload schema: 585 * https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json 586 * 587 * @function notifyDevice 588 * @param sessionTokenHex 589 * Session token obtained from signIn 590 * @param deviceIds 591 * Devices to send the message to. If null, will be sent to all devices. 592 * @param excludedIds 593 * Devices to exclude when sending to all devices (deviceIds must be null). 594 * @param payload 595 * Data to send with the message 596 * @return Promise 597 * Resolves to an empty object: 598 * {} 599 */ 600 async notifyDevices( 601 sessionTokenHex, 602 deviceIds, 603 excludedIds, 604 payload, 605 TTL = 0 606 ) { 607 const credentials = await deriveHawkCredentials( 608 sessionTokenHex, 609 "sessionToken" 610 ); 611 if (deviceIds && excludedIds) { 612 throw new Error( 613 "You cannot specify excluded devices if deviceIds is set." 614 ); 615 } 616 const body = { 617 to: deviceIds || "all", 618 payload, 619 TTL, 620 }; 621 if (excludedIds) { 622 body.excluded = excludedIds; 623 } 624 return this._request("/account/devices/notify", "POST", credentials, body); 625 }, 626 627 /** 628 * Retrieves pending commands for our device. 629 * 630 * @function getCommands 631 * @param sessionTokenHex - Session token obtained from signIn 632 * @param [index] - If specified, only messages received after the one who 633 * had that index will be retrieved. 634 * @param [limit] - Maximum number of messages to retrieve. 635 */ 636 async getCommands(sessionTokenHex, { index, limit }) { 637 const credentials = await deriveHawkCredentials( 638 sessionTokenHex, 639 "sessionToken" 640 ); 641 const params = new URLSearchParams(); 642 if (index != undefined) { 643 params.set("index", index); 644 } 645 if (limit != undefined) { 646 params.set("limit", limit); 647 } 648 const path = `/account/device/commands?${params.toString()}`; 649 return this._request(path, "GET", credentials); 650 }, 651 652 /** 653 * Invokes a command on another device. 654 * 655 * @function invokeCommand 656 * @param sessionTokenHex - Session token obtained from signIn 657 * @param command - Name of the command to invoke 658 * @param target - Recipient device ID. 659 * @param payload 660 * @return Promise 661 * Resolves to the request's response, (which should be an empty object) 662 */ 663 async invokeCommand(sessionTokenHex, command, target, payload) { 664 const credentials = await deriveHawkCredentials( 665 sessionTokenHex, 666 "sessionToken" 667 ); 668 const body = { 669 command, 670 target, 671 payload, 672 }; 673 return this._request( 674 "/account/devices/invoke_command", 675 "POST", 676 credentials, 677 body 678 ); 679 }, 680 681 /** 682 * Update the session or name for an existing device 683 * 684 * @function updateDevice 685 * @param sessionTokenHex 686 * Session token obtained from signIn 687 * @param id 688 * Device identifier 689 * @param name 690 * Device name 691 * @param [options] 692 * Extra device options 693 * @param [options.availableCommands] 694 * Available commands for this device 695 * @param [options.pushCallback] 696 * `pushCallback` push endpoint callback 697 * @param [options.pushPublicKey] 698 * `pushPublicKey` push public key (URLSafe Base64 string) 699 * @param [options.pushAuthKey] 700 * `pushAuthKey` push auth secret (URLSafe Base64 string) 701 * @return Promise 702 * Resolves to an object: 703 * { 704 * id: Device identifier 705 * name: Device name 706 * } 707 */ 708 async updateDevice(sessionTokenHex, id, name, options = {}) { 709 let path = "/account/device"; 710 711 let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken"); 712 let body = { id, name }; 713 if (options.pushCallback) { 714 body.pushCallback = options.pushCallback; 715 } 716 if (options.pushPublicKey && options.pushAuthKey) { 717 body.pushPublicKey = options.pushPublicKey; 718 body.pushAuthKey = options.pushAuthKey; 719 } 720 body.availableCommands = options.availableCommands; 721 722 return this._request(path, "POST", creds, body); 723 }, 724 725 /** 726 * Get a list of currently registered devices that have been accessed 727 * in the last `DEVICES_FILTER_DAYS` days 728 * 729 * @function getDeviceList 730 * @param sessionTokenHex 731 * Session token obtained from signIn 732 * @return Promise 733 * Resolves to an array of objects: 734 * [ 735 * { 736 * id: Device id 737 * isCurrentDevice: Boolean indicating whether the item 738 * represents the current device 739 * name: Device name 740 * type: Device type (mobile|desktop) 741 * }, 742 * ... 743 * ] 744 */ 745 async getDeviceList(sessionTokenHex) { 746 let timestamp = Date.now() - 1000 * 60 * 60 * 24 * DEVICES_FILTER_DAYS; 747 let path = `/account/devices?filterIdleDevicesTimestamp=${timestamp}`; 748 let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken"); 749 return this._request(path, "GET", creds, {}); 750 }, 751 752 _clearBackoff() { 753 this.backoffError = null; 754 }, 755 756 /** 757 * A general method for sending raw API calls to the FxA auth server. 758 * All request bodies and responses are JSON. 759 * 760 * @param path 761 * API endpoint path 762 * @param method 763 * The HTTP request method 764 * @param credentials 765 * Hawk credentials 766 * @param jsonPayload 767 * A JSON payload 768 * @return Promise 769 * Returns a promise that resolves to the JSON response of the API call, 770 * or is rejected with an error. Error responses have the following properties: 771 * { 772 * "code": 400, // matches the HTTP status code 773 * "errno": 107, // stable application-level error number 774 * "error": "Bad Request", // string description of the error type 775 * "message": "the value of salt is not allowed to be undefined", 776 * "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error 777 * } 778 */ 779 async _requestWithHeaders(path, method, credentials, jsonPayload) { 780 // We were asked to back off. 781 if (this.backoffError) { 782 log.debug("Received new request during backoff, re-rejecting."); 783 throw this.backoffError; 784 } 785 let response; 786 try { 787 response = await this.hawk.request( 788 path, 789 method, 790 credentials, 791 jsonPayload 792 ); 793 } catch (error) { 794 log.error(`error ${method}ing ${path}`, error); 795 if (error.retryAfter) { 796 log.debug("Received backoff response; caching error as flag."); 797 this.backoffError = error; 798 // Schedule clearing of cached-error-as-flag. 799 CommonUtils.namedTimer( 800 this._clearBackoff, 801 error.retryAfter * 1000, 802 this, 803 "fxaBackoffTimer" 804 ); 805 } 806 throw error; 807 } 808 try { 809 return { body: JSON.parse(response.body), headers: response.headers }; 810 } catch (error) { 811 log.error("json parse error on response: " + response.body); 812 // eslint-disable-next-line no-throw-literal 813 throw { error }; 814 } 815 }, 816 817 async _request(path, method, credentials, jsonPayload) { 818 const response = await this._requestWithHeaders( 819 path, 820 method, 821 credentials, 822 jsonPayload 823 ); 824 return response.body; 825 }, 826 }; 827 828 function isInvalidTokenError(error) { 829 if (error.code != 401) { 830 return false; 831 } 832 switch (error.errno) { 833 case ERRNO_INVALID_AUTH_TOKEN: 834 case ERRNO_INVALID_AUTH_TIMESTAMP: 835 case ERRNO_INVALID_AUTH_NONCE: 836 return true; 837 } 838 return false; 839 }