API Docs for: 1.0.8
Show:

File: client/FxAccountClient.js

  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. define([
  5. 'es6-promise',
  6. 'sjcl',
  7. './lib/credentials',
  8. './lib/errors',
  9. './lib/hawkCredentials',
  10. './lib/metricsContext',
  11. './lib/request'
  12. ], function (ES6Promise, sjcl, credentials, ERRORS, hawkCredentials, metricsContext, Request) {
  13. 'use strict';
  14.  
  15. // polyfill ES6 promises on browsers that do not support them.
  16. ES6Promise.polyfill();
  17.  
  18. var VERSION = 'v1';
  19. var uriVersionRegExp = new RegExp('/' + VERSION + '$');
  20. var HKDF_SIZE = 2 * 32;
  21.  
  22. function isUndefined(val) {
  23. return typeof val === 'undefined';
  24. }
  25.  
  26. function isNull(val) {
  27. return val === null;
  28. }
  29.  
  30. function isEmptyObject(val) {
  31. return Object.prototype.toString.call(val) === '[object Object]' && ! Object.keys(val).length;
  32. }
  33.  
  34. function isEmptyString(val) {
  35. return val === '';
  36. }
  37.  
  38. function required(val, name) {
  39. if (isUndefined(val) ||
  40. isNull(val) ||
  41. isEmptyObject(val) ||
  42. isEmptyString(val)) {
  43. throw new Error('Missing ' + name);
  44. }
  45. }
  46.  
  47. /**
  48. * @class FxAccountClient
  49. * @constructor
  50. * @param {String} uri Auth Server URI
  51. * @param {Object} config Configuration
  52. */
  53. function FxAccountClient(uri, config) {
  54. if (! uri && ! config) {
  55. throw new Error('Firefox Accounts auth server endpoint or configuration object required.');
  56. }
  57.  
  58. if (typeof uri !== 'string') {
  59. config = uri || {};
  60. uri = config.uri;
  61. }
  62.  
  63. if (typeof config === 'undefined') {
  64. config = {};
  65. }
  66.  
  67. if (! uri) {
  68. throw new Error('FxA auth server uri not set.');
  69. }
  70.  
  71. if (!uriVersionRegExp.test(uri)) {
  72. uri = uri + '/' + VERSION;
  73. }
  74.  
  75. this.request = new Request(uri, config.xhr, { localtimeOffsetMsec: config.localtimeOffsetMsec });
  76. }
  77.  
  78. FxAccountClient.VERSION = VERSION;
  79.  
  80. /**
  81. * @method signUp
  82. * @param {String} email Email input
  83. * @param {String} password Password input
  84. * @param {Object} [options={}] Options
  85. * @param {Boolean} [options.keys]
  86. * If `true`, calls the API with `?keys=true` to get the keyFetchToken
  87. * @param {String} [options.service]
  88. * Opaque alphanumeric token to be included in verification links
  89. * @param {String} [options.redirectTo]
  90. * a URL that the client should be redirected to after handling the request
  91. * @param {String} [options.preVerified]
  92. * set email to be verified if possible
  93. * @param {String} [options.resume]
  94. * Opaque url-encoded string that will be included in the verification link
  95. * as a querystring parameter, useful for continuing an OAuth flow for
  96. * example.
  97. * @param {String} [options.lang]
  98. * set the language for the 'Accept-Language' header
  99. * @param {Object} [options.metricsContext={}] Metrics context metadata
  100. * @param {String} options.metricsContext.deviceId identifier for the current device
  101. * @param {String} options.metricsContext.flowId identifier for the current event flow
  102. * @param {Number} options.metricsContext.flowBeginTime flow.begin event time
  103. * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
  104. * @param {Number} options.metricsContext.utmContent content identifier
  105. * @param {Number} options.metricsContext.utmMedium acquisition medium
  106. * @param {Number} options.metricsContext.utmSource traffic source
  107. * @param {Number} options.metricsContext.utmTerm search terms
  108. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  109. */
  110. FxAccountClient.prototype.signUp = function (email, password, options) {
  111. var self = this;
  112.  
  113. return Promise.resolve()
  114. .then(function () {
  115. required(email, 'email');
  116. required(password, 'password');
  117.  
  118. return credentials.setup(email, password);
  119. })
  120. .then(
  121. function (result) {
  122. /*eslint complexity: [2, 13] */
  123. var endpoint = '/account/create';
  124. var data = {
  125. email: result.emailUTF8,
  126. authPW: sjcl.codec.hex.fromBits(result.authPW)
  127. };
  128. var requestOpts = {};
  129.  
  130. if (options) {
  131. if (options.service) {
  132. data.service = options.service;
  133. }
  134.  
  135. if (options.redirectTo) {
  136. data.redirectTo = options.redirectTo;
  137. }
  138.  
  139. // preVerified is used for unit/functional testing
  140. if (options.preVerified) {
  141. data.preVerified = options.preVerified;
  142. }
  143.  
  144. if (options.resume) {
  145. data.resume = options.resume;
  146. }
  147.  
  148. if (options.keys) {
  149. endpoint += '?keys=true';
  150. }
  151.  
  152. if (options.lang) {
  153. requestOpts.headers = {
  154. 'Accept-Language': options.lang
  155. };
  156. }
  157.  
  158. if (options.metricsContext) {
  159. data.metricsContext = metricsContext.marshall(options.metricsContext);
  160. }
  161. }
  162.  
  163. return self.request.send(endpoint, 'POST', null, data, requestOpts)
  164. .then(
  165. function(accountData) {
  166. if (options && options.keys) {
  167. accountData.unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey);
  168. }
  169. return accountData;
  170. }
  171. );
  172. }
  173. );
  174. };
  175.  
  176. /**
  177. * @method signIn
  178. * @param {String} email Email input
  179. * @param {String} password Password input
  180. * @param {Object} [options={}] Options
  181. * @param {Boolean} [options.keys]
  182. * If `true`, calls the API with `?keys=true` to get the keyFetchToken
  183. * @param {Boolean} [options.skipCaseError]
  184. * If `true`, the request will skip the incorrect case error
  185. * @param {String} [options.service]
  186. * Service being signed into
  187. * @param {String} [options.reason]
  188. * Reason for sign in. Can be one of: `signin`, `password_check`,
  189. * `password_change`, `password_reset`
  190. * @param {String} [options.redirectTo]
  191. * a URL that the client should be redirected to after handling the request
  192. * @param {String} [options.resume]
  193. * Opaque url-encoded string that will be included in the verification link
  194. * as a querystring parameter, useful for continuing an OAuth flow for
  195. * example.
  196. * @param {String} [options.originalLoginEmail]
  197. * If retrying after an "incorrect email case" error, this specifies
  198. * the email address as originally entered by the user.
  199. * @param {String} [options.verificationMethod]
  200. * Request a specific verification method be used for verifying the session,
  201. * e.g. 'email-2fa' or 'totp-2fa'.
  202. * @param {Object} [options.metricsContext={}] Metrics context metadata
  203. * @param {String} options.metricsContext.deviceId identifier for the current device
  204. * @param {String} options.metricsContext.flowId identifier for the current event flow
  205. * @param {Number} options.metricsContext.flowBeginTime flow.begin event time
  206. * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
  207. * @param {Number} options.metricsContext.utmContent content identifier
  208. * @param {Number} options.metricsContext.utmMedium acquisition medium
  209. * @param {Number} options.metricsContext.utmSource traffic source
  210. * @param {Number} options.metricsContext.utmTerm search terms
  211. * @param {String} [options.unblockCode]
  212. * Login unblock code.
  213. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  214. */
  215. FxAccountClient.prototype.signIn = function (email, password, options) {
  216. var self = this;
  217. options = options || {};
  218.  
  219. return Promise.resolve()
  220. .then(function () {
  221. required(email, 'email');
  222. required(password, 'password');
  223.  
  224. return credentials.setup(email, password);
  225. })
  226. .then(
  227. function (result) {
  228. var endpoint = '/account/login';
  229.  
  230. if (options.keys) {
  231. endpoint += '?keys=true';
  232. }
  233.  
  234. var data = {
  235. email: result.emailUTF8,
  236. authPW: sjcl.codec.hex.fromBits(result.authPW)
  237. };
  238.  
  239. if (options.metricsContext) {
  240. data.metricsContext = metricsContext.marshall(options.metricsContext);
  241. }
  242.  
  243. if (options.reason) {
  244. data.reason = options.reason;
  245. }
  246.  
  247. if (options.redirectTo) {
  248. data.redirectTo = options.redirectTo;
  249. }
  250.  
  251. if (options.resume) {
  252. data.resume = options.resume;
  253. }
  254.  
  255. if (options.service) {
  256. data.service = options.service;
  257. }
  258.  
  259. if (options.unblockCode) {
  260. data.unblockCode = options.unblockCode;
  261. }
  262.  
  263. if (options.originalLoginEmail) {
  264. data.originalLoginEmail = options.originalLoginEmail;
  265. }
  266.  
  267. if (options.verificationMethod) {
  268. data.verificationMethod = options.verificationMethod;
  269. }
  270.  
  271. return self.request.send(endpoint, 'POST', null, data)
  272. .then(
  273. function(accountData) {
  274. if (options.keys) {
  275. accountData.unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey);
  276. }
  277. return accountData;
  278. },
  279. function(error) {
  280. if (error && error.email && error.errno === ERRORS.INCORRECT_EMAIL_CASE && !options.skipCaseError) {
  281. options.skipCaseError = true;
  282. options.originalLoginEmail = email;
  283.  
  284. return self.signIn(error.email, password, options);
  285. } else {
  286. throw error;
  287. }
  288. }
  289. );
  290. }
  291. );
  292. };
  293.  
  294. /**
  295. * @method verifyCode
  296. * @param {String} uid Account ID
  297. * @param {String} code Verification code
  298. * @param {Object} [options={}] Options
  299. * @param {String} [options.service]
  300. * Service being signed into
  301. * @param {String} [options.reminder]
  302. * Reminder that was used to verify the account
  303. * @param {String} [options.type]
  304. * Type of code being verified, only supports `secondary` otherwise will verify account/sign-in
  305. * @param {Boolean} [options.marketingOptIn]
  306. * If `true`, notifies marketing of opt-in intent.
  307. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  308. */
  309. FxAccountClient.prototype.verifyCode = function(uid, code, options) {
  310. var self = this;
  311.  
  312. return Promise.resolve()
  313. .then(function () {
  314. required(uid, 'uid');
  315. required(code, 'verify code');
  316.  
  317. var data = {
  318. uid: uid,
  319. code: code
  320. };
  321.  
  322. if (options) {
  323. if (options.service) {
  324. data.service = options.service;
  325. }
  326.  
  327. if (options.reminder) {
  328. data.reminder = options.reminder;
  329. }
  330.  
  331. if (options.type) {
  332. data.type = options.type;
  333. }
  334.  
  335. if (options.marketingOptIn) {
  336. data.marketingOptIn = true;
  337. }
  338. }
  339.  
  340. return self.request.send('/recovery_email/verify_code', 'POST', null, data);
  341. });
  342. };
  343.  
  344. FxAccountClient.prototype.verifyTokenCode = function(sessionToken, uid, code) {
  345. var self = this;
  346.  
  347. required(uid, 'uid');
  348. required(code, 'verify token code');
  349. required(sessionToken, 'sessionToken');
  350.  
  351. return Promise.resolve()
  352. .then(function () {
  353. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  354. })
  355. .then(function (creds) {
  356. var data = {
  357. uid: uid,
  358. code: code
  359. };
  360.  
  361. return self.request.send('/session/verify/token', 'POST', creds, data);
  362. });
  363. };
  364.  
  365. /**
  366. * @method recoveryEmailStatus
  367. * @param {String} sessionToken sessionToken obtained from signIn
  368. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  369. */
  370. FxAccountClient.prototype.recoveryEmailStatus = function(sessionToken) {
  371. var self = this;
  372.  
  373. return Promise.resolve()
  374. .then(function () {
  375. required(sessionToken, 'sessionToken');
  376.  
  377. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  378. })
  379. .then(function(creds) {
  380. return self.request.send('/recovery_email/status', 'GET', creds);
  381. });
  382. };
  383.  
  384. /**
  385. * Re-sends a verification code to the account's recovery email address.
  386. *
  387. * @method recoveryEmailResendCode
  388. * @param {String} sessionToken sessionToken obtained from signIn
  389. * @param {Object} [options={}] Options
  390. * @param {String} [options.email]
  391. * Code will be resent to this email, only used for secondary email codes
  392. * @param {String} [options.service]
  393. * Opaque alphanumeric token to be included in verification links
  394. * @param {String} [options.redirectTo]
  395. * a URL that the client should be redirected to after handling the request
  396. * @param {String} [options.resume]
  397. * Opaque url-encoded string that will be included in the verification link
  398. * as a querystring parameter, useful for continuing an OAuth flow for
  399. * example.
  400. * @param {String} [options.type]
  401. * Specifies the type of code to send, currently only supported type is
  402. * `upgradeSession`.
  403. * @param {String} [options.lang]
  404. * set the language for the 'Accept-Language' header
  405. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  406. */
  407. FxAccountClient.prototype.recoveryEmailResendCode = function(sessionToken, options) {
  408. var self = this;
  409. var data = {};
  410. var requestOpts = {};
  411.  
  412. return Promise.resolve()
  413. .then(function () {
  414. required(sessionToken, 'sessionToken');
  415.  
  416. if (options) {
  417. if (options.email) {
  418. data.email = options.email;
  419. }
  420.  
  421. if (options.service) {
  422. data.service = options.service;
  423. }
  424.  
  425. if (options.redirectTo) {
  426. data.redirectTo = options.redirectTo;
  427. }
  428.  
  429. if (options.resume) {
  430. data.resume = options.resume;
  431. }
  432.  
  433. if (options.type) {
  434. data.type = options.type;
  435. }
  436.  
  437. if (options.lang) {
  438. requestOpts.headers = {
  439. 'Accept-Language': options.lang
  440. };
  441. }
  442. }
  443.  
  444. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  445. })
  446. .then(function(creds) {
  447. return self.request.send('/recovery_email/resend_code', 'POST', creds, data, requestOpts);
  448. });
  449. };
  450.  
  451. /**
  452. * Used to ask the server to send a recovery code.
  453. * The API returns passwordForgotToken to the client.
  454. *
  455. * @method passwordForgotSendCode
  456. * @param {String} email
  457. * @param {Object} [options={}] Options
  458. * @param {String} [options.service]
  459. * Opaque alphanumeric token to be included in verification links
  460. * @param {String} [options.redirectTo]
  461. * a URL that the client should be redirected to after handling the request
  462. * @param {String} [options.resume]
  463. * Opaque url-encoded string that will be included in the verification link
  464. * as a querystring parameter, useful for continuing an OAuth flow for
  465. * example.
  466. * @param {String} [options.lang]
  467. * set the language for the 'Accept-Language' header
  468. * @param {Object} [options.metricsContext={}] Metrics context metadata
  469. * @param {String} options.metricsContext.deviceId identifier for the current device
  470. * @param {String} options.metricsContext.flowId identifier for the current event flow
  471. * @param {Number} options.metricsContext.flowBeginTime flow.begin event time
  472. * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
  473. * @param {Number} options.metricsContext.utmContent content identifier
  474. * @param {Number} options.metricsContext.utmMedium acquisition medium
  475. * @param {Number} options.metricsContext.utmSource traffic source
  476. * @param {Number} options.metricsContext.utmTerm search terms
  477. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  478. */
  479. FxAccountClient.prototype.passwordForgotSendCode = function(email, options) {
  480. var self = this;
  481. var data = {
  482. email: email
  483. };
  484. var requestOpts = {};
  485.  
  486. return Promise.resolve()
  487. .then(function () {
  488. required(email, 'email');
  489.  
  490. if (options) {
  491. if (options.service) {
  492. data.service = options.service;
  493. }
  494.  
  495. if (options.redirectTo) {
  496. data.redirectTo = options.redirectTo;
  497. }
  498.  
  499. if (options.resume) {
  500. data.resume = options.resume;
  501. }
  502.  
  503. if (options.lang) {
  504. requestOpts.headers = {
  505. 'Accept-Language': options.lang
  506. };
  507. }
  508.  
  509. if (options.metricsContext) {
  510. data.metricsContext = metricsContext.marshall(options.metricsContext);
  511. }
  512. }
  513.  
  514. return self.request.send('/password/forgot/send_code', 'POST', null, data, requestOpts);
  515. });
  516. };
  517.  
  518. /**
  519. * Re-sends a verification code to the account's recovery email address.
  520. * HAWK-authenticated with the passwordForgotToken.
  521. *
  522. * @method passwordForgotResendCode
  523. * @param {String} email
  524. * @param {String} passwordForgotToken
  525. * @param {Object} [options={}] Options
  526. * @param {String} [options.service]
  527. * Opaque alphanumeric token to be included in verification links
  528. * @param {String} [options.redirectTo]
  529. * a URL that the client should be redirected to after handling the request
  530. * @param {String} [options.resume]
  531. * Opaque url-encoded string that will be included in the verification link
  532. * as a querystring parameter, useful for continuing an OAuth flow for
  533. * example.
  534. * @param {String} [options.lang]
  535. * set the language for the 'Accept-Language' header
  536. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  537. */
  538. FxAccountClient.prototype.passwordForgotResendCode = function(email, passwordForgotToken, options) {
  539. var self = this;
  540. var data = {
  541. email: email
  542. };
  543. var requestOpts = {};
  544.  
  545. return Promise.resolve()
  546. .then(function () {
  547. required(email, 'email');
  548. required(passwordForgotToken, 'passwordForgotToken');
  549.  
  550. if (options) {
  551. if (options.service) {
  552. data.service = options.service;
  553. }
  554.  
  555. if (options.redirectTo) {
  556. data.redirectTo = options.redirectTo;
  557. }
  558.  
  559. if (options.resume) {
  560. data.resume = options.resume;
  561. }
  562.  
  563. if (options.lang) {
  564. requestOpts.headers = {
  565. 'Accept-Language': options.lang
  566. };
  567. }
  568. }
  569.  
  570. return hawkCredentials(passwordForgotToken, 'passwordForgotToken', HKDF_SIZE);
  571. })
  572. .then(function(creds) {
  573. return self.request.send('/password/forgot/resend_code', 'POST', creds, data, requestOpts);
  574. });
  575. };
  576.  
  577. /**
  578. * Submits the verification token to the server.
  579. * The API returns accountResetToken to the client.
  580. * HAWK-authenticated with the passwordForgotToken.
  581. *
  582. * @method passwordForgotVerifyCode
  583. * @param {String} code
  584. * @param {String} passwordForgotToken
  585. * @param {Object} [options={}] Options
  586. * @param {Boolean} [options.accountResetWithRecoveryKey] verifying code to be use in account recovery
  587. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  588. */
  589. FxAccountClient.prototype.passwordForgotVerifyCode = function(code, passwordForgotToken, options) {
  590. var self = this;
  591.  
  592. return Promise.resolve()
  593. .then(function () {
  594. required(code, 'reset code');
  595. required(passwordForgotToken, 'passwordForgotToken');
  596.  
  597. return hawkCredentials(passwordForgotToken, 'passwordForgotToken', HKDF_SIZE);
  598. })
  599. .then(function(creds) {
  600. var data = {
  601. code: code
  602. };
  603.  
  604. if (options && options.accountResetWithRecoveryKey ) {
  605. data.accountResetWithRecoveryKey = options.accountResetWithRecoveryKey;
  606. }
  607.  
  608. return self.request.send('/password/forgot/verify_code', 'POST', creds, data);
  609. });
  610. };
  611.  
  612. /**
  613. * Returns the status for the passwordForgotToken.
  614. * If the request returns a success response, the token has not yet been consumed.
  615.  
  616. * @method passwordForgotStatus
  617. * @param {String} passwordForgotToken
  618. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  619. */
  620. FxAccountClient.prototype.passwordForgotStatus = function(passwordForgotToken) {
  621. var self = this;
  622.  
  623. return Promise.resolve()
  624. .then(function () {
  625. required(passwordForgotToken, 'passwordForgotToken');
  626.  
  627. return hawkCredentials(passwordForgotToken, 'passwordForgotToken', HKDF_SIZE);
  628. })
  629. .then(function(creds) {
  630. return self.request.send('/password/forgot/status', 'GET', creds);
  631. });
  632. };
  633.  
  634. /**
  635. * The API returns reset result to the client.
  636. * HAWK-authenticated with accountResetToken
  637. *
  638. * @method accountReset
  639. * @param {String} email
  640. * @param {String} newPassword
  641. * @param {String} accountResetToken
  642. * @param {Object} [options={}] Options
  643. * @param {Boolean} [options.keys]
  644. * If `true`, a new `keyFetchToken` is provisioned. `options.sessionToken`
  645. * is required if `options.keys` is true.
  646. * @param {Boolean} [options.sessionToken]
  647. * If `true`, a new `sessionToken` is provisioned.
  648. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  649. */
  650. FxAccountClient.prototype.accountReset = function(email, newPassword, accountResetToken, options) {
  651. var self = this;
  652. var data = {};
  653. var unwrapBKey;
  654.  
  655. options = options || {};
  656.  
  657. if (options.sessionToken) {
  658. data.sessionToken = options.sessionToken;
  659. }
  660.  
  661. return Promise.resolve()
  662. .then(function () {
  663. required(email, 'email');
  664. required(newPassword, 'new password');
  665. required(accountResetToken, 'accountResetToken');
  666.  
  667. if (options.keys) {
  668. required(options.sessionToken, 'sessionToken');
  669. }
  670.  
  671. return credentials.setup(email, newPassword);
  672. })
  673. .then(
  674. function (result) {
  675. if (options.keys) {
  676. unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey);
  677. }
  678.  
  679. data.authPW = sjcl.codec.hex.fromBits(result.authPW);
  680.  
  681. return hawkCredentials(accountResetToken, 'accountResetToken', HKDF_SIZE);
  682. }
  683. ).then(
  684. function (creds) {
  685. var queryParams = '';
  686. if (options.keys) {
  687. queryParams = '?keys=true';
  688. }
  689.  
  690. var endpoint = '/account/reset' + queryParams;
  691. return self.request.send(endpoint, 'POST', creds, data)
  692. .then(
  693. function(accountData) {
  694. if (options.keys && accountData.keyFetchToken) {
  695. accountData.unwrapBKey = unwrapBKey;
  696. }
  697.  
  698. return accountData;
  699. }
  700. );
  701. }
  702. );
  703. };
  704.  
  705. /**
  706. * Get the base16 bundle of encrypted kA|wrapKb.
  707. *
  708. * @method accountKeys
  709. * @param {String} keyFetchToken
  710. * @param {String} oldUnwrapBKey
  711. * @return {Promise} A promise that will be fulfilled with JSON of {kA, kB} of the key bundle
  712. */
  713. FxAccountClient.prototype.accountKeys = function(keyFetchToken, oldUnwrapBKey) {
  714. var self = this;
  715.  
  716. return Promise.resolve()
  717. .then(function () {
  718. required(keyFetchToken, 'keyFetchToken');
  719. required(oldUnwrapBKey, 'oldUnwrapBKey');
  720.  
  721. return hawkCredentials(keyFetchToken, 'keyFetchToken', 3 * 32);
  722. })
  723. .then(function(creds) {
  724. var bundleKey = sjcl.codec.hex.fromBits(creds.bundleKey);
  725.  
  726. return self.request.send('/account/keys', 'GET', creds)
  727. .then(
  728. function(payload) {
  729.  
  730. return credentials.unbundleKeyFetchResponse(bundleKey, payload.bundle);
  731. });
  732. })
  733. .then(function(keys) {
  734. return {
  735. kB: sjcl.codec.hex.fromBits(
  736. credentials.xor(
  737. sjcl.codec.hex.toBits(keys.wrapKB),
  738. sjcl.codec.hex.toBits(oldUnwrapBKey)
  739. )
  740. ),
  741. kA: keys.kA
  742. };
  743. });
  744. };
  745.  
  746. /**
  747. * This deletes the account completely. All stored data is erased.
  748. *
  749. * @method accountDestroy
  750. * @param {String} email Email input
  751. * @param {String} password Password input
  752. * @param {Object} [options={}] Options
  753. * @param {Boolean} [options.skipCaseError]
  754. * If `true`, the request will skip the incorrect case error
  755. * @param {String} sessionToken User session token
  756. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  757. */
  758. FxAccountClient.prototype.accountDestroy = function (email, password, options, sessionToken) {
  759. var self = this;
  760. options = options || {};
  761.  
  762. return Promise.resolve()
  763. .then(function () {
  764. required(email, 'email');
  765. required(password, 'password');
  766.  
  767. var defers = [credentials.setup(email, password)];
  768. if (sessionToken) {
  769. defers.push(hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE));
  770. }
  771.  
  772. return Promise.all(defers);
  773. })
  774. .then(
  775. function (results) {
  776. var auth = results[0];
  777. var creds = results[1];
  778. var data = {
  779. email: auth.emailUTF8,
  780. authPW: sjcl.codec.hex.fromBits(auth.authPW)
  781. };
  782.  
  783. return self.request.send('/account/destroy', 'POST', creds, data)
  784. .then(
  785. function (response) {
  786. return response;
  787. },
  788. function (error) {
  789. // if incorrect email case error
  790. if (error && error.email && error.errno === ERRORS.INCORRECT_EMAIL_CASE && !options.skipCaseError) {
  791. options.skipCaseError = true;
  792.  
  793. return self.accountDestroy(error.email, password, options, sessionToken);
  794. } else {
  795. throw error;
  796. }
  797. }
  798. );
  799. }
  800. );
  801. };
  802.  
  803. /**
  804. * Gets the status of an account by uid.
  805. *
  806. * @method accountStatus
  807. * @param {String} uid User account id
  808. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  809. */
  810. FxAccountClient.prototype.accountStatus = function(uid) {
  811. var self = this;
  812.  
  813. return Promise.resolve()
  814. .then(function () {
  815. required(uid, 'uid');
  816.  
  817. return self.request.send('/account/status?uid=' + uid, 'GET');
  818. });
  819. };
  820.  
  821. /**
  822. * Gets the status of an account by email.
  823. *
  824. * @method accountStatusByEmail
  825. * @param {String} email User account email
  826. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  827. */
  828. FxAccountClient.prototype.accountStatusByEmail = function(email) {
  829. var self = this;
  830.  
  831. return Promise.resolve()
  832. .then(function () {
  833. required(email, 'email');
  834.  
  835. return self.request.send('/account/status', 'POST', null, {email: email});
  836. });
  837. };
  838.  
  839. /**
  840. * Destroys this session, by invalidating the sessionToken.
  841. *
  842. * @method sessionDestroy
  843. * @param {String} sessionToken User session token
  844. * @param {Object} [options={}] Options
  845. * @param {String} [options.customSessionToken] Override which session token to destroy for this same user
  846. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  847. */
  848. FxAccountClient.prototype.sessionDestroy = function(sessionToken, options) {
  849. var self = this;
  850. var data = {};
  851. options = options || {};
  852.  
  853. if (options.customSessionToken) {
  854. data.customSessionToken = options.customSessionToken;
  855. }
  856.  
  857. return Promise.resolve()
  858. .then(function () {
  859. required(sessionToken, 'sessionToken');
  860.  
  861. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  862. })
  863. .then(function(creds) {
  864. return self.request.send('/session/destroy', 'POST', creds, data);
  865. });
  866. };
  867.  
  868. /**
  869. * Responds successfully if the session status is valid, requires the sessionToken.
  870. *
  871. * @method sessionStatus
  872. * @param {String} sessionToken User session token
  873. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  874. */
  875. FxAccountClient.prototype.sessionStatus = function(sessionToken) {
  876. var self = this;
  877.  
  878. return Promise.resolve()
  879. .then(function () {
  880. required(sessionToken, 'sessionToken');
  881.  
  882. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  883. })
  884. .then(function(creds) {
  885. return self.request.send('/session/status', 'GET', creds);
  886. });
  887. };
  888.  
  889. /**
  890. * @method sessionReauth
  891. * @param {String} sessionToken sessionToken obtained from signIn
  892. * @param {String} email Email input
  893. * @param {String} password Password input
  894. * @param {Object} [options={}] Options
  895. * @param {Boolean} [options.keys]
  896. * If `true`, calls the API with `?keys=true` to get the keyFetchToken
  897. * @param {Boolean} [options.skipCaseError]
  898. * If `true`, the request will skip the incorrect case error
  899. * @param {String} [options.service]
  900. * Service being accessed that needs reauthentication
  901. * @param {String} [options.reason]
  902. * Reason for reauthentication. Can be one of: `signin`, `password_check`,
  903. * `password_change`, `password_reset`
  904. * @param {String} [options.redirectTo]
  905. * a URL that the client should be redirected to after handling the request
  906. * @param {String} [options.resume]
  907. * Opaque url-encoded string that will be included in the verification link
  908. * as a querystring parameter, useful for continuing an OAuth flow for
  909. * example.
  910. * @param {String} [options.originalLoginEmail]
  911. * If retrying after an "incorrect email case" error, this specifies
  912. * the email address as originally entered by the user.
  913. * @param {String} [options.verificationMethod]
  914. * Request a specific verification method be used for verifying the session,
  915. * e.g. 'email-2fa' or 'totp-2fa'.
  916. * @param {Object} [options.metricsContext={}] Metrics context metadata
  917. * @param {String} options.metricsContext.deviceId identifier for the current device
  918. * @param {String} options.metricsContext.flowId identifier for the current event flow
  919. * @param {Number} options.metricsContext.flowBeginTime flow.begin event time
  920. * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
  921. * @param {Number} options.metricsContext.utmContent content identifier
  922. * @param {Number} options.metricsContext.utmMedium acquisition medium
  923. * @param {Number} options.metricsContext.utmSource traffic source
  924. * @param {Number} options.metricsContext.utmTerm search terms
  925. * @param {String} [options.unblockCode]
  926. * Login unblock code.
  927. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  928. */
  929. FxAccountClient.prototype.sessionReauth = function (sessionToken, email, password, options) {
  930. var self = this;
  931. options = options || {};
  932.  
  933. return Promise.resolve()
  934. .then(function () {
  935. required(sessionToken, 'sessionToken');
  936. required(email, 'email');
  937. required(password, 'password');
  938.  
  939. return credentials.setup(email, password);
  940. })
  941. .then(
  942. function (result) {
  943. var endpoint = '/session/reauth';
  944.  
  945. if (options.keys) {
  946. endpoint += '?keys=true';
  947. }
  948.  
  949. var data = {
  950. email: result.emailUTF8,
  951. authPW: sjcl.codec.hex.fromBits(result.authPW)
  952. };
  953.  
  954. if (options.metricsContext) {
  955. data.metricsContext = metricsContext.marshall(options.metricsContext);
  956. }
  957.  
  958. if (options.reason) {
  959. data.reason = options.reason;
  960. }
  961.  
  962. if (options.redirectTo) {
  963. data.redirectTo = options.redirectTo;
  964. }
  965.  
  966. if (options.resume) {
  967. data.resume = options.resume;
  968. }
  969.  
  970. if (options.service) {
  971. data.service = options.service;
  972. }
  973.  
  974. if (options.unblockCode) {
  975. data.unblockCode = options.unblockCode;
  976. }
  977.  
  978. if (options.originalLoginEmail) {
  979. data.originalLoginEmail = options.originalLoginEmail;
  980. }
  981.  
  982. if (options.verificationMethod) {
  983. data.verificationMethod = options.verificationMethod;
  984. }
  985.  
  986. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE)
  987. .then(function (creds) {
  988. return self.request.send(endpoint, 'POST', creds, data);
  989. })
  990. .then(
  991. function(accountData) {
  992. if (options.keys) {
  993. accountData.unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey);
  994. }
  995. return accountData;
  996. },
  997. function(error) {
  998. if (error && error.email && error.errno === ERRORS.INCORRECT_EMAIL_CASE && !options.skipCaseError) {
  999. options.skipCaseError = true;
  1000. options.originalLoginEmail = email;
  1001.  
  1002. return self.sessionReauth(sessionToken, error.email, password, options);
  1003. } else {
  1004. throw error;
  1005. }
  1006. }
  1007. );
  1008. }
  1009. );
  1010. };
  1011.  
  1012. /**
  1013. * Sign a BrowserID public key
  1014. *
  1015. * @method certificateSign
  1016. * @param {String} sessionToken User session token
  1017. * @param {Object} publicKey The key to sign
  1018. * @param {int} duration Time interval from now when the certificate will expire in milliseconds
  1019. * @param {Object} [options={}] Options
  1020. * @param {String} [service=''] The requesting service, sent via the query string
  1021. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1022. */
  1023. FxAccountClient.prototype.certificateSign = function(sessionToken, publicKey, duration, options) {
  1024. var self = this;
  1025. var data = {
  1026. publicKey: publicKey,
  1027. duration: duration
  1028. };
  1029.  
  1030. return Promise.resolve()
  1031. .then(function () {
  1032. required(sessionToken, 'sessionToken');
  1033. required(publicKey, 'publicKey');
  1034. required(duration, 'duration');
  1035.  
  1036. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1037. })
  1038. .then(function(creds) {
  1039. options = options || {};
  1040.  
  1041. var queryString = '';
  1042. if (options.service) {
  1043. queryString = '?service=' + encodeURIComponent(options.service);
  1044. }
  1045.  
  1046. return self.request.send('/certificate/sign' + queryString, 'POST', creds, data);
  1047. });
  1048. };
  1049.  
  1050. /**
  1051. * Change the password from one known value to another.
  1052. *
  1053. * @method passwordChange
  1054. * @param {String} email
  1055. * @param {String} oldPassword
  1056. * @param {String} newPassword
  1057. * @param {Object} [options={}] Options
  1058. * @param {Boolean} [options.keys]
  1059. * If `true`, calls the API with `?keys=true` to get a new keyFetchToken
  1060. * @param {String} [options.sessionToken]
  1061. * If a `sessionToken` is passed, a new sessionToken will be returned
  1062. * with the same `verified` status as the existing sessionToken.
  1063. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1064. */
  1065. FxAccountClient.prototype.passwordChange = function(email, oldPassword, newPassword, options) {
  1066. var self = this;
  1067. options = options || {};
  1068.  
  1069. return Promise.resolve()
  1070. .then(function () {
  1071. required(email, 'email');
  1072. required(oldPassword, 'old password');
  1073. required(newPassword, 'new password');
  1074.  
  1075. return self._passwordChangeStart(email, oldPassword);
  1076. })
  1077. .then(function (credentials) {
  1078.  
  1079. var oldCreds = credentials;
  1080. var emailToHashWith = credentials.emailToHashWith || email;
  1081.  
  1082. return self._passwordChangeKeys(oldCreds)
  1083. .then(function (keys) {
  1084.  
  1085. return self._passwordChangeFinish(emailToHashWith, newPassword, oldCreds, keys, options);
  1086. });
  1087. });
  1088.  
  1089. };
  1090.  
  1091. /**
  1092. * First step to change the password.
  1093. *
  1094. * @method passwordChangeStart
  1095. * @private
  1096. * @param {String} email
  1097. * @param {String} oldPassword
  1098. * @param {Object} [options={}] Options
  1099. * @param {Boolean} [options.skipCaseError]
  1100. * If `true`, the request will skip the incorrect case error
  1101. * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText` and `oldUnwrapBKey`
  1102. */
  1103. FxAccountClient.prototype._passwordChangeStart = function(email, oldPassword, options) {
  1104. var self = this;
  1105. options = options || {};
  1106.  
  1107. return Promise.resolve()
  1108. .then(function () {
  1109. required(email, 'email');
  1110. required(oldPassword, 'old password');
  1111.  
  1112. return credentials.setup(email, oldPassword);
  1113. })
  1114. .then(function (oldCreds) {
  1115. var data = {
  1116. email: oldCreds.emailUTF8,
  1117. oldAuthPW: sjcl.codec.hex.fromBits(oldCreds.authPW)
  1118. };
  1119.  
  1120. return self.request.send('/password/change/start', 'POST', null, data)
  1121. .then(
  1122. function(passwordData) {
  1123. passwordData.oldUnwrapBKey = sjcl.codec.hex.fromBits(oldCreds.unwrapBKey);
  1124.  
  1125. // Similar to password reset, this keeps the contract that we always
  1126. // hash passwords with the original account email.
  1127. passwordData.emailToHashWith = email;
  1128. return passwordData;
  1129. },
  1130. function(error) {
  1131. // if incorrect email case error
  1132. if (error && error.email && error.errno === ERRORS.INCORRECT_EMAIL_CASE && !options.skipCaseError) {
  1133. options.skipCaseError = true;
  1134.  
  1135. return self._passwordChangeStart(error.email, oldPassword, options);
  1136. } else {
  1137. throw error;
  1138. }
  1139. }
  1140. );
  1141. });
  1142. };
  1143.  
  1144. function checkCreds(creds) {
  1145. required(creds, 'credentials');
  1146. required(creds.oldUnwrapBKey, 'credentials.oldUnwrapBKey');
  1147. required(creds.keyFetchToken, 'credentials.keyFetchToken');
  1148. required(creds.passwordChangeToken, 'credentials.passwordChangeToken');
  1149. }
  1150.  
  1151. /**
  1152. * Second step to change the password.
  1153. *
  1154. * @method _passwordChangeKeys
  1155. * @private
  1156. * @param {Object} oldCreds This object should consists of `oldUnwrapBKey`, `keyFetchToken` and `passwordChangeToken`.
  1157. * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText`
  1158. */
  1159. FxAccountClient.prototype._passwordChangeKeys = function(oldCreds) {
  1160. var self = this;
  1161.  
  1162. return Promise.resolve()
  1163. .then(function () {
  1164. checkCreds(oldCreds);
  1165. })
  1166. .then(function () {
  1167. return self.accountKeys(oldCreds.keyFetchToken, oldCreds.oldUnwrapBKey);
  1168. });
  1169. };
  1170.  
  1171. /**
  1172. * Third step to change the password.
  1173. *
  1174. * @method _passwordChangeFinish
  1175. * @private
  1176. * @param {String} email
  1177. * @param {String} newPassword
  1178. * @param {Object} oldCreds This object should consists of `oldUnwrapBKey`, `keyFetchToken` and `passwordChangeToken`.
  1179. * @param {Object} keys This object should contain the unbundled keys
  1180. * @param {Object} [options={}] Options
  1181. * @param {Boolean} [options.keys]
  1182. * If `true`, calls the API with `?keys=true` to get the keyFetchToken
  1183. * @param {String} [options.sessionToken]
  1184. * If a `sessionToken` is passed, a new sessionToken will be returned
  1185. * with the same `verified` status as the existing sessionToken.
  1186. * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText`
  1187. */
  1188. FxAccountClient.prototype._passwordChangeFinish = function(email, newPassword, oldCreds, keys, options) {
  1189. options = options || {};
  1190. var self = this;
  1191.  
  1192. return Promise.resolve()
  1193. .then(function () {
  1194. required(email, 'email');
  1195. required(newPassword, 'new password');
  1196. checkCreds(oldCreds);
  1197. required(keys, 'keys');
  1198. required(keys.kB, 'keys.kB');
  1199.  
  1200. var defers = [];
  1201. defers.push(credentials.setup(email, newPassword));
  1202. defers.push(hawkCredentials(oldCreds.passwordChangeToken, 'passwordChangeToken', HKDF_SIZE));
  1203.  
  1204. if (options.sessionToken) {
  1205. // Unbundle session data to get session id
  1206. defers.push(hawkCredentials(options.sessionToken, 'sessionToken', HKDF_SIZE));
  1207. }
  1208.  
  1209. return Promise.all(defers);
  1210. })
  1211. .then(function (results) {
  1212. var newCreds = results[0];
  1213. var hawkCreds = results[1];
  1214. var sessionData = results[2];
  1215. var newWrapKb = sjcl.codec.hex.fromBits(
  1216. credentials.xor(
  1217. sjcl.codec.hex.toBits(keys.kB),
  1218. newCreds.unwrapBKey
  1219. )
  1220. );
  1221.  
  1222. var queryParams = '';
  1223. if (options.keys) {
  1224. queryParams = '?keys=true';
  1225. }
  1226.  
  1227. var sessionTokenId;
  1228. if (sessionData && sessionData.id) {
  1229. sessionTokenId = sessionData.id;
  1230. }
  1231.  
  1232. return self.request.send('/password/change/finish' + queryParams, 'POST', hawkCreds, {
  1233. wrapKb: newWrapKb,
  1234. authPW: sjcl.codec.hex.fromBits(newCreds.authPW),
  1235. sessionToken: sessionTokenId
  1236. })
  1237. .then(function (accountData) {
  1238. if (options.keys && accountData.keyFetchToken) {
  1239. accountData.unwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey);
  1240. }
  1241. return accountData;
  1242. });
  1243. });
  1244. };
  1245.  
  1246. /**
  1247. * Get 32 bytes of random data. This should be combined with locally-sourced entropy when creating salts, etc.
  1248. *
  1249. * @method getRandomBytes
  1250. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1251. */
  1252. FxAccountClient.prototype.getRandomBytes = function() {
  1253.  
  1254. return this.request.send('/get_random_bytes', 'POST');
  1255. };
  1256.  
  1257. /**
  1258. * Add a new device
  1259. *
  1260. * @method deviceRegister
  1261. * @param {String} sessionToken User session token
  1262. * @param {String} deviceName Name of device
  1263. * @param {String} deviceType Type of device (mobile|desktop)
  1264. * @param {Object} [options={}] Options
  1265. * @param {string} [options.deviceCallback] Device's push endpoint.
  1266. * @param {string} [options.devicePublicKey] Public key used to encrypt push messages.
  1267. * @param {string} [options.deviceAuthKey] Authentication secret used to encrypt push messages.
  1268. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1269. */
  1270. FxAccountClient.prototype.deviceRegister = function (sessionToken, deviceName, deviceType, options) {
  1271. var request = this.request;
  1272. options = options || {};
  1273.  
  1274. return Promise.resolve()
  1275. .then(function () {
  1276. required(sessionToken, 'sessionToken');
  1277. required(deviceName, 'deviceName');
  1278. required(deviceType, 'deviceType');
  1279.  
  1280. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1281. })
  1282. .then(function(creds) {
  1283. var data = {
  1284. name: deviceName,
  1285. type: deviceType
  1286. };
  1287.  
  1288. if (options.deviceCallback) {
  1289. data.pushCallback = options.deviceCallback;
  1290. }
  1291.  
  1292. if (options.devicePublicKey && options.deviceAuthKey) {
  1293. data.pushPublicKey = options.devicePublicKey;
  1294. data.pushAuthKey = options.deviceAuthKey;
  1295. }
  1296.  
  1297. return request.send('/account/device', 'POST', creds, data);
  1298. });
  1299. };
  1300.  
  1301. /**
  1302. * Update the name of an existing device
  1303. *
  1304. * @method deviceUpdate
  1305. * @param {String} sessionToken User session token
  1306. * @param {String} deviceId User-unique identifier of device
  1307. * @param {String} deviceName Name of device
  1308. * @param {Object} [options={}] Options
  1309. * @param {string} [options.deviceCallback] Device's push endpoint.
  1310. * @param {string} [options.devicePublicKey] Public key used to encrypt push messages.
  1311. * @param {string} [options.deviceAuthKey] Authentication secret used to encrypt push messages.
  1312. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1313. */
  1314. FxAccountClient.prototype.deviceUpdate = function (sessionToken, deviceId, deviceName, options) {
  1315. var request = this.request;
  1316. options = options || {};
  1317.  
  1318. return Promise.resolve()
  1319. .then(function () {
  1320. required(sessionToken, 'sessionToken');
  1321. required(deviceId, 'deviceId');
  1322. required(deviceName, 'deviceName');
  1323.  
  1324. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1325. })
  1326. .then(function(creds) {
  1327. var data = {
  1328. id: deviceId,
  1329. name: deviceName
  1330. };
  1331.  
  1332. if (options.deviceCallback) {
  1333. data.pushCallback = options.deviceCallback;
  1334. }
  1335.  
  1336. if (options.devicePublicKey && options.deviceAuthKey) {
  1337. data.pushPublicKey = options.devicePublicKey;
  1338. data.pushAuthKey = options.deviceAuthKey;
  1339. }
  1340.  
  1341. return request.send('/account/device', 'POST', creds, data);
  1342. });
  1343. };
  1344.  
  1345. /**
  1346. * Unregister an existing device
  1347. *
  1348. * @method deviceDestroy
  1349. * @param {String} sessionToken Session token obtained from signIn
  1350. * @param {String} deviceId User-unique identifier of device
  1351. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1352. */
  1353. FxAccountClient.prototype.deviceDestroy = function (sessionToken, deviceId) {
  1354. var request = this.request;
  1355.  
  1356. return Promise.resolve()
  1357. .then(function () {
  1358. required(sessionToken, 'sessionToken');
  1359. required(deviceId, 'deviceId');
  1360.  
  1361. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1362. })
  1363. .then(function(creds) {
  1364. var data = {
  1365. id: deviceId
  1366. };
  1367.  
  1368. return request.send('/account/device/destroy', 'POST', creds, data);
  1369. });
  1370. };
  1371.  
  1372. /**
  1373. * Get a list of all devices for a user
  1374. *
  1375. * @method deviceList
  1376. * @param {String} sessionToken sessionToken obtained from signIn
  1377. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1378. */
  1379. FxAccountClient.prototype.deviceList = function (sessionToken) {
  1380. var request = this.request;
  1381.  
  1382. return Promise.resolve()
  1383. .then(function () {
  1384. required(sessionToken, 'sessionToken');
  1385.  
  1386. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1387. })
  1388. .then(function(creds) {
  1389. return request.send('/account/devices', 'GET', creds);
  1390. });
  1391. };
  1392.  
  1393. /**
  1394. * Get a list of user's sessions
  1395. *
  1396. * @method sessions
  1397. * @param {String} sessionToken sessionToken obtained from signIn
  1398. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1399. */
  1400. FxAccountClient.prototype.sessions = function (sessionToken) {
  1401. var request = this.request;
  1402.  
  1403. return Promise.resolve()
  1404. .then(function () {
  1405. required(sessionToken, 'sessionToken');
  1406.  
  1407. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1408. })
  1409. .then(function(creds) {
  1410. return request.send('/account/sessions', 'GET', creds);
  1411. });
  1412. };
  1413.  
  1414. /**
  1415. * Send an unblock code
  1416. *
  1417. * @method sendUnblockCode
  1418. * @param {String} email email where to send the login authorization code
  1419. * @param {Object} [options={}] Options
  1420. * @param {Object} [options.metricsContext={}] Metrics context metadata
  1421. * @param {String} options.metricsContext.deviceId identifier for the current device
  1422. * @param {String} options.metricsContext.flowId identifier for the current event flow
  1423. * @param {Number} options.metricsContext.flowBeginTime flow.begin event time
  1424. * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
  1425. * @param {Number} options.metricsContext.utmContent content identifier
  1426. * @param {Number} options.metricsContext.utmMedium acquisition medium
  1427. * @param {Number} options.metricsContext.utmSource traffic source
  1428. * @param {Number} options.metricsContext.utmTerm search terms
  1429. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1430. */
  1431. FxAccountClient.prototype.sendUnblockCode = function (email, options) {
  1432. var self = this;
  1433.  
  1434. return Promise.resolve()
  1435. .then(function () {
  1436. required(email, 'email');
  1437.  
  1438. var data = {
  1439. email: email
  1440. };
  1441.  
  1442. if (options && options.metricsContext) {
  1443. data.metricsContext = metricsContext.marshall(options.metricsContext);
  1444. }
  1445.  
  1446. return self.request.send('/account/login/send_unblock_code', 'POST', null, data);
  1447. });
  1448. };
  1449.  
  1450. /**
  1451. * Reject a login unblock code. Code will be deleted from the server
  1452. * and will not be able to be used again.
  1453. *
  1454. * @method rejectLoginAuthorizationCode
  1455. * @param {String} uid Account ID
  1456. * @param {String} unblockCode unblock code
  1457. * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
  1458. */
  1459. FxAccountClient.prototype.rejectUnblockCode = function (uid, unblockCode) {
  1460. var self = this;
  1461.  
  1462. return Promise.resolve()
  1463. .then(function () {
  1464. required(uid, 'uid');
  1465. required(unblockCode, 'unblockCode');
  1466.  
  1467. var data = {
  1468. uid: uid,
  1469. unblockCode: unblockCode
  1470. };
  1471.  
  1472. return self.request.send('/account/login/reject_unblock_code', 'POST', null, data);
  1473. });
  1474. };
  1475.  
  1476. /**
  1477. * Send an sms.
  1478. *
  1479. * @method sendSms
  1480. * @param {String} sessionToken SessionToken obtained from signIn
  1481. * @param {String} phoneNumber Phone number sms will be sent to
  1482. * @param {String} messageId Corresponding message id that will be sent
  1483. * @param {Object} [options={}] Options
  1484. * @param {String} [options.lang] Language that sms will be sent in
  1485. * @param {Array} [options.features] Array of features to be enabled for the request
  1486. * @param {Object} [options.metricsContext={}] Metrics context metadata
  1487. * @param {String} options.metricsContext.deviceId identifier for the current device
  1488. * @param {String} options.metricsContext.flowId identifier for the current event flow
  1489. * @param {Number} options.metricsContext.flowBeginTime flow.begin event time
  1490. * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
  1491. * @param {Number} options.metricsContext.utmContent content identifier
  1492. * @param {Number} options.metricsContext.utmMedium acquisition medium
  1493. * @param {Number} options.metricsContext.utmSource traffic source
  1494. * @param {Number} options.metricsContext.utmTerm search terms
  1495. */
  1496. FxAccountClient.prototype.sendSms = function (sessionToken, phoneNumber, messageId, options) {
  1497. var request = this.request;
  1498.  
  1499. return Promise.resolve()
  1500. .then(function () {
  1501. required(sessionToken, 'sessionToken');
  1502. required(phoneNumber, 'phoneNumber');
  1503. required(messageId, 'messageId');
  1504.  
  1505. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1506. })
  1507. .then(function(creds) {
  1508. var data = {
  1509. phoneNumber: phoneNumber,
  1510. messageId: messageId
  1511. };
  1512. var requestOpts = {};
  1513.  
  1514. if (options) {
  1515. if (options.lang) {
  1516. requestOpts.headers = {
  1517. 'Accept-Language': options.lang
  1518. };
  1519. }
  1520.  
  1521. if (options.features) {
  1522. data.features = options.features;
  1523. }
  1524.  
  1525. if (options.metricsContext) {
  1526. data.metricsContext = metricsContext.marshall(options.metricsContext);
  1527. }
  1528. }
  1529.  
  1530. return request.send('/sms', 'POST', creds, data, requestOpts);
  1531. });
  1532. };
  1533.  
  1534. /**
  1535. * Get SMS status for the current user.
  1536. *
  1537. * @method smsStatus
  1538. * @param {String} sessionToken SessionToken obtained from signIn
  1539. * @param {Object} [options={}] Options
  1540. * @param {String} [options.country] country Country to force for testing.
  1541. */
  1542. FxAccountClient.prototype.smsStatus = function (sessionToken, options) {
  1543. var request = this.request;
  1544. options = options || {};
  1545.  
  1546. return Promise.resolve()
  1547. .then(function () {
  1548. required(sessionToken, 'sessionToken');
  1549.  
  1550. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1551. })
  1552. .then(function (creds) {
  1553. var url = '/sms/status';
  1554. if (options.country) {
  1555. url += '?country=' + encodeURIComponent(options.country);
  1556. }
  1557. return request.send(url, 'GET', creds);
  1558. });
  1559. };
  1560.  
  1561. /**
  1562. * Consume a signinCode.
  1563. *
  1564. * @method consumeSigninCode
  1565. * @param {String} code The signinCode entered by the user
  1566. * @param {String} flowId Identifier for the current event flow
  1567. * @param {Number} flowBeginTime Timestamp for the flow.begin event
  1568. * @param {String} [deviceId] Identifier for the current device
  1569. */
  1570. FxAccountClient.prototype.consumeSigninCode = function (code, flowId, flowBeginTime, deviceId) {
  1571. var self = this;
  1572.  
  1573. return Promise.resolve()
  1574. .then(function () {
  1575. required(code, 'code');
  1576. required(flowId, 'flowId');
  1577. required(flowBeginTime, 'flowBeginTime');
  1578.  
  1579. return self.request.send('/signinCodes/consume', 'POST', null, {
  1580. code: code,
  1581. metricsContext: {
  1582. deviceId: deviceId,
  1583. flowId: flowId,
  1584. flowBeginTime: flowBeginTime
  1585. }
  1586. });
  1587. });
  1588. };
  1589.  
  1590. /**
  1591. * Get the recovery emails associated with the signed in account.
  1592. *
  1593. * @method recoveryEmails
  1594. * @param {String} sessionToken SessionToken obtained from signIn
  1595. */
  1596. FxAccountClient.prototype.recoveryEmails = function (sessionToken) {
  1597. var request = this.request;
  1598.  
  1599. return Promise.resolve()
  1600. .then(function () {
  1601. required(sessionToken, 'sessionToken');
  1602.  
  1603. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1604. })
  1605. .then(function(creds) {
  1606. return request.send('/recovery_emails', 'GET', creds);
  1607. });
  1608. };
  1609.  
  1610. /**
  1611. * Create a new recovery email for the signed in account.
  1612. *
  1613. * @method recoveryEmailCreate
  1614. * @param {String} sessionToken SessionToken obtained from signIn
  1615. * @param {String} email new email to be added
  1616. */
  1617. FxAccountClient.prototype.recoveryEmailCreate = function (sessionToken, email) {
  1618. var request = this.request;
  1619.  
  1620. return Promise.resolve()
  1621. .then(function () {
  1622. required(sessionToken, 'sessionToken');
  1623. required(sessionToken, 'email');
  1624.  
  1625. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1626. })
  1627. .then(function(creds) {
  1628. var data = {
  1629. email: email
  1630. };
  1631.  
  1632. return request.send('/recovery_email', 'POST', creds, data);
  1633. });
  1634. };
  1635.  
  1636. /**
  1637. * Remove the recovery email for the signed in account.
  1638. *
  1639. * @method recoveryEmailDestroy
  1640. * @param {String} sessionToken SessionToken obtained from signIn
  1641. * @param {String} email email to be removed
  1642. */
  1643. FxAccountClient.prototype.recoveryEmailDestroy = function (sessionToken, email) {
  1644. var request = this.request;
  1645.  
  1646. return Promise.resolve()
  1647. .then(function () {
  1648. required(sessionToken, 'sessionToken');
  1649. required(sessionToken, 'email');
  1650.  
  1651. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1652. })
  1653. .then(function(creds) {
  1654. var data = {
  1655. email: email
  1656. };
  1657.  
  1658. return request.send('/recovery_email/destroy', 'POST', creds, data);
  1659. });
  1660. };
  1661.  
  1662. /**
  1663. * Changes user's primary email address.
  1664. *
  1665. * @method recoveryEmailSetPrimaryEmail
  1666. * @param {String} sessionToken SessionToken obtained from signIn
  1667. * @param {String} email Email that will be the new primary email for user
  1668. */
  1669. FxAccountClient.prototype.recoveryEmailSetPrimaryEmail = function (sessionToken, email) {
  1670. var request = this.request;
  1671. return Promise.resolve()
  1672. .then(function () {
  1673. required(sessionToken, 'sessionToken');
  1674.  
  1675. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1676. })
  1677. .then(function(creds) {
  1678. var data = {
  1679. email: email
  1680. };
  1681. return request.send('/recovery_email/set_primary', 'POST', creds, data);
  1682. });
  1683. };
  1684.  
  1685. /**
  1686. * Creates a new TOTP token for the user associated with this session.
  1687. *
  1688. * @method createTotpToken
  1689. * @param {String} sessionToken SessionToken obtained from signIn
  1690. * @param {Object} [options.metricsContext={}] Metrics context metadata
  1691. * @param {String} options.metricsContext.deviceId identifier for the current device
  1692. * @param {String} options.metricsContext.flowId identifier for the current event flow
  1693. * @param {Number} options.metricsContext.flowBeginTime flow.begin event time
  1694. * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
  1695. * @param {Number} options.metricsContext.utmContent content identifier
  1696. * @param {Number} options.metricsContext.utmMedium acquisition medium
  1697. * @param {Number} options.metricsContext.utmSource traffic source
  1698. * @param {Number} options.metricsContext.utmTerm search terms
  1699. */
  1700. FxAccountClient.prototype.createTotpToken = function (sessionToken, options) {
  1701. var request = this.request;
  1702. return Promise.resolve()
  1703. .then(function () {
  1704. required(sessionToken, 'sessionToken');
  1705.  
  1706. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1707. })
  1708. .then(function (creds) {
  1709. var data = {};
  1710.  
  1711. if (options && options.metricsContext) {
  1712. data.metricsContext = metricsContext.marshall(options.metricsContext);
  1713. }
  1714.  
  1715. return request.send('/totp/create', 'POST', creds, data);
  1716. });
  1717. };
  1718.  
  1719. /**
  1720. * Deletes this user's TOTP token.
  1721. *
  1722. * @method deleteTotpToken
  1723. * @param {String} sessionToken SessionToken obtained from signIn
  1724. */
  1725. FxAccountClient.prototype.deleteTotpToken = function (sessionToken) {
  1726. var request = this.request;
  1727. return Promise.resolve()
  1728. .then(function () {
  1729. required(sessionToken, 'sessionToken');
  1730.  
  1731. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1732. })
  1733. .then(function(creds) {
  1734. return request.send('/totp/destroy', 'POST', creds, {});
  1735. });
  1736. };
  1737.  
  1738. /**
  1739. * Check to see if the current user has a TOTP token associated with
  1740. * their account.
  1741. *
  1742. * @method checkTotpTokenExists
  1743. * @param {String} sessionToken SessionToken obtained from signIn
  1744. */
  1745. FxAccountClient.prototype.checkTotpTokenExists = function (sessionToken) {
  1746. var request = this.request;
  1747. return Promise.resolve()
  1748. .then(function () {
  1749. required(sessionToken, 'sessionToken');
  1750.  
  1751. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1752. })
  1753. .then(function(creds) {
  1754. return request.send('/totp/exists', 'GET', creds);
  1755. });
  1756. };
  1757.  
  1758. /**
  1759. * Verify tokens if using a valid TOTP code.
  1760. *
  1761. * @method verifyTotpCode
  1762. * @param {String} sessionToken SessionToken obtained from signIn
  1763. * @param {String} code TOTP code to verif
  1764. * @param {String} [options.service] Service being used
  1765. */
  1766. FxAccountClient.prototype.verifyTotpCode = function (sessionToken, code, options) {
  1767. var request = this.request;
  1768. return Promise.resolve()
  1769. .then(function () {
  1770. required(sessionToken, 'sessionToken');
  1771. required(code, 'code');
  1772.  
  1773. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1774. })
  1775. .then(function (creds) {
  1776. var data = {
  1777. code: code
  1778. };
  1779.  
  1780. if (options && options.service) {
  1781. data.service = options.service;
  1782. }
  1783.  
  1784. return request.send('/session/verify/totp', 'POST', creds, data);
  1785. });
  1786. };
  1787.  
  1788. /**
  1789. * Replace user's recovery codes.
  1790. *
  1791. * @method replaceRecoveryCodes
  1792. * @param {String} sessionToken SessionToken obtained from signIn
  1793. */
  1794. FxAccountClient.prototype.replaceRecoveryCodes = function (sessionToken) {
  1795. var request = this.request;
  1796. return Promise.resolve()
  1797. .then(function () {
  1798. required(sessionToken, 'sessionToken');
  1799.  
  1800. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1801. })
  1802. .then(function (creds) {
  1803.  
  1804. return request.send('/recoveryCodes', 'GET', creds);
  1805. });
  1806. };
  1807.  
  1808. /**
  1809. * Consume recovery code.
  1810. *
  1811. * @method consumeRecoveryCode
  1812. * @param {String} sessionToken SessionToken obtained from signIn
  1813. * @param {String} code recovery code
  1814. */
  1815. FxAccountClient.prototype.consumeRecoveryCode = function (sessionToken, code) {
  1816. var request = this.request;
  1817. return Promise.resolve()
  1818. .then(function () {
  1819. required(sessionToken, 'sessionToken');
  1820. required(code, 'code');
  1821.  
  1822. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1823. })
  1824. .then(function (creds) {
  1825. var data = {
  1826. code: code
  1827. };
  1828.  
  1829. return request.send('/session/verify/recoveryCode', 'POST', creds, data);
  1830. });
  1831. };
  1832.  
  1833. /**
  1834. * Creates a new recovery key for the account. The recovery key contains encrypted
  1835. * data the corresponds the the accounts current `kB`. This data can be used during
  1836. * the password reset process to avoid regenerating the `kB`.
  1837. *
  1838. * @param sessionToken
  1839. * @param recoveryKeyId The recoveryKeyId that can be used to retrieve saved bundle
  1840. * @param bundle The encrypted recovery bundle to store
  1841. * @returns {Promise} A promise that will be fulfilled with decoded recovery data (`kB`)
  1842. */
  1843. FxAccountClient.prototype.createRecoveryKey = function (sessionToken, recoveryKeyId, bundle) {
  1844. var request = this.request;
  1845. return Promise.resolve()
  1846. .then(function () {
  1847. required(sessionToken, 'sessionToken');
  1848. required(recoveryKeyId, 'recoveryKeyId');
  1849. required(bundle, 'bundle');
  1850.  
  1851. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1852. })
  1853. .then(function (creds) {
  1854. var data = {
  1855. recoveryKeyId: recoveryKeyId,
  1856. recoveryData: bundle
  1857. };
  1858.  
  1859. return request.send('/recoveryKey', 'POST', creds, data);
  1860. });
  1861. };
  1862.  
  1863. /**
  1864. * Retrieves the encrypted recovery data that corresponds to the recovery key which
  1865. * then gets decoded into the stored `kB`.
  1866. *
  1867. * @param accountResetToken
  1868. * @param recoveryKeyId The recovery key id to retrieve encrypted bundle
  1869. * @returns {Promise} A promise that will be fulfilled with decoded recovery data (`kB`)
  1870. */
  1871. FxAccountClient.prototype.getRecoveryKey = function (accountResetToken, recoveryKeyId) {
  1872. var request = this.request;
  1873. return Promise.resolve()
  1874. .then(function () {
  1875. required(accountResetToken, 'accountResetToken');
  1876. required(recoveryKeyId, 'recoveryKeyId');
  1877.  
  1878. return hawkCredentials(accountResetToken, 'accountResetToken', HKDF_SIZE);
  1879. })
  1880. .then(function (creds) {
  1881. return request.send('/recoveryKey/' + recoveryKeyId, 'GET', creds);
  1882. });
  1883. };
  1884.  
  1885. /**
  1886. * Reset a user's account using keys (kB) derived from a recovery key. This
  1887. * process can be used to maintain the account's original kB.
  1888. *
  1889. * @param accountResetToken The account reset token
  1890. * @param email The current email of the account
  1891. * @param newPassword The new password of the account
  1892. * @param recoveryKeyId The recovery key id used for account recovery
  1893. * @param keys Keys used to create the new wrapKb
  1894. * @param {Object} [options={}] Options
  1895. * @param {Boolean} [options.keys]
  1896. * If `true`, a new `keyFetchToken` is provisioned. `options.sessionToken`
  1897. * is required if `options.keys` is true.
  1898. * @param {Boolean} [options.sessionToken]
  1899. * If `true`, a new `sessionToken` is provisioned.
  1900. * @returns {Promise} A promise that will be fulfilled with updated account data
  1901. */
  1902. FxAccountClient.prototype.resetPasswordWithRecoveryKey = function (accountResetToken, email, newPassword, recoveryKeyId, keys, options) {
  1903. options = options || {};
  1904. var request = this.request;
  1905. return Promise.resolve()
  1906. .then(function () {
  1907. required(email, 'email');
  1908. required(newPassword, 'new password');
  1909. required(keys, 'keys');
  1910. required(keys.kB, 'keys.kB');
  1911. required(accountResetToken, 'accountResetToken');
  1912. required(recoveryKeyId, 'recoveryKeyId');
  1913.  
  1914. var defers = [];
  1915. defers.push(credentials.setup(email, newPassword));
  1916. defers.push(hawkCredentials(accountResetToken, 'accountResetToken', HKDF_SIZE));
  1917.  
  1918. return Promise.all(defers);
  1919. })
  1920. .then(function (results) {
  1921. var newCreds = results[0];
  1922. var hawkCreds = results[1];
  1923. var newWrapKb = sjcl.codec.hex.fromBits(
  1924. credentials.xor(
  1925. sjcl.codec.hex.toBits(keys.kB),
  1926. newCreds.unwrapBKey
  1927. )
  1928. );
  1929.  
  1930. var data = {
  1931. wrapKb: newWrapKb,
  1932. authPW: sjcl.codec.hex.fromBits(newCreds.authPW),
  1933. recoveryKeyId: recoveryKeyId
  1934. };
  1935.  
  1936. if (options.sessionToken) {
  1937. data.sessionToken = options.sessionToken;
  1938. }
  1939.  
  1940. if (options.keys) {
  1941. required(options.sessionToken, 'sessionToken');
  1942. }
  1943.  
  1944. var queryParams = '';
  1945. if (options.keys) {
  1946. queryParams = '?keys=true';
  1947. }
  1948.  
  1949. return request.send('/account/reset' + queryParams, 'POST', hawkCreds, data)
  1950. .then(function (accountData) {
  1951. if (options.keys && accountData.keyFetchToken) {
  1952. accountData.unwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey);
  1953. }
  1954. return accountData;
  1955. });
  1956. });
  1957. };
  1958.  
  1959. /**
  1960. * Deletes the recovery key associated with this user.
  1961. *
  1962. * @param sessionToken
  1963. */
  1964. FxAccountClient.prototype.deleteRecoveryKey = function (sessionToken) {
  1965. var request = this.request;
  1966. return Promise.resolve()
  1967. .then(function () {
  1968. required(sessionToken, 'sessionToken');
  1969.  
  1970. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
  1971. })
  1972. .then(function (creds) {
  1973. return request.send('/recoveryKey', 'DELETE', creds, {});
  1974. });
  1975. };
  1976.  
  1977. /**
  1978. * This checks to see if a recovery key exists for a user. This check
  1979. * can be performed with either a sessionToken or an email.
  1980. *
  1981. * Typically, sessionToken is used when checking from within the `/settings`
  1982. * view. If it exists, we can give the user an option to revoke the key.
  1983. *
  1984. * Checking with an email is typically performed during the password reset
  1985. * flow. It is used to decide whether or not we can redirect a user to
  1986. * the `Reset password with recovery key` page or regular password reset page.
  1987. *
  1988. * @param sessionToken
  1989. * @param {String} email User's email
  1990. * @returns {Promise} A promise that will be fulfilled with whether or not account has recovery ket
  1991. */
  1992. FxAccountClient.prototype.recoveryKeyExists = function (sessionToken, email) {
  1993. var request = this.request;
  1994. return Promise.resolve()
  1995. .then(function () {
  1996.  
  1997. if (sessionToken) {
  1998. return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE)
  1999. .then(function (creds) {
  2000. return request.send('/recoveryKey/exists', 'POST', creds, {});
  2001. });
  2002. }
  2003.  
  2004. return request.send('/recoveryKey/exists', 'POST', null, {email: email});
  2005. });
  2006. };
  2007.  
  2008. /**
  2009. * Check for a required argument. Exposed for unit testing.
  2010. *
  2011. * @param {Value} val - value to check
  2012. * @param {String} name - name of value
  2013. * @throws {Error} if argument is falsey, or an empty object
  2014. */
  2015. FxAccountClient.prototype._required = required;
  2016.  
  2017. return FxAccountClient;
  2018. });
  2019.