tokenserverclient.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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 file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. this.EXPORTED_SYMBOLS = [
  6. "TokenServerClient",
  7. "TokenServerClientError",
  8. "TokenServerClientNetworkError",
  9. "TokenServerClientServerError",
  10. ];
  11. var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
  12. Cu.import("resource://gre/modules/Services.jsm");
  13. Cu.import("resource://gre/modules/Log.jsm");
  14. Cu.import("resource://services-common/rest.js");
  15. Cu.import("resource://services-common/observers.js");
  16. const PREF_LOG_LEVEL = "services.common.log.logger.tokenserverclient";
  17. /**
  18. * Represents a TokenServerClient error that occurred on the client.
  19. *
  20. * This is the base type for all errors raised by client operations.
  21. *
  22. * @param message
  23. * (string) Error message.
  24. */
  25. this.TokenServerClientError = function TokenServerClientError(message) {
  26. this.name = "TokenServerClientError";
  27. this.message = message || "Client error.";
  28. // Without explicitly setting .stack, all stacks from these errors will point
  29. // to the "new Error()" call a few lines down, which isn't helpful.
  30. this.stack = Error().stack;
  31. }
  32. TokenServerClientError.prototype = new Error();
  33. TokenServerClientError.prototype.constructor = TokenServerClientError;
  34. TokenServerClientError.prototype._toStringFields = function() {
  35. return {message: this.message};
  36. }
  37. TokenServerClientError.prototype.toString = function() {
  38. return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";
  39. }
  40. TokenServerClientError.prototype.toJSON = function() {
  41. let result = this._toStringFields();
  42. result["name"] = this.name;
  43. return result;
  44. }
  45. /**
  46. * Represents a TokenServerClient error that occurred in the network layer.
  47. *
  48. * @param error
  49. * The underlying error thrown by the network layer.
  50. */
  51. this.TokenServerClientNetworkError =
  52. function TokenServerClientNetworkError(error) {
  53. this.name = "TokenServerClientNetworkError";
  54. this.error = error;
  55. this.stack = Error().stack;
  56. }
  57. TokenServerClientNetworkError.prototype = new TokenServerClientError();
  58. TokenServerClientNetworkError.prototype.constructor =
  59. TokenServerClientNetworkError;
  60. TokenServerClientNetworkError.prototype._toStringFields = function() {
  61. return {error: this.error};
  62. }
  63. /**
  64. * Represents a TokenServerClient error that occurred on the server.
  65. *
  66. * This type will be encountered for all non-200 response codes from the
  67. * server. The type of error is strongly enumerated and is stored in the
  68. * `cause` property. This property can have the following string values:
  69. *
  70. * conditions-required -- The server is requesting that the client
  71. * agree to service conditions before it can obtain a token. The
  72. * conditions that must be presented to the user and agreed to are in
  73. * the `urls` mapping on the instance. Keys of this mapping are
  74. * identifiers. Values are string URLs.
  75. *
  76. * invalid-credentials -- A token could not be obtained because
  77. * the credentials presented by the client were invalid.
  78. *
  79. * unknown-service -- The requested service was not found.
  80. *
  81. * malformed-request -- The server rejected the request because it
  82. * was invalid. If you see this, code in this file is likely wrong.
  83. *
  84. * malformed-response -- The response from the server was not what was
  85. * expected.
  86. *
  87. * general -- A general server error has occurred. Clients should
  88. * interpret this as an opaque failure.
  89. *
  90. * @param message
  91. * (string) Error message.
  92. */
  93. this.TokenServerClientServerError =
  94. function TokenServerClientServerError(message, cause="general") {
  95. this.now = new Date().toISOString(); // may be useful to diagnose time-skew issues.
  96. this.name = "TokenServerClientServerError";
  97. this.message = message || "Server error.";
  98. this.cause = cause;
  99. this.stack = Error().stack;
  100. }
  101. TokenServerClientServerError.prototype = new TokenServerClientError();
  102. TokenServerClientServerError.prototype.constructor =
  103. TokenServerClientServerError;
  104. TokenServerClientServerError.prototype._toStringFields = function() {
  105. let fields = {
  106. now: this.now,
  107. message: this.message,
  108. cause: this.cause,
  109. };
  110. if (this.response) {
  111. fields.response_body = this.response.body;
  112. fields.response_headers = this.response.headers;
  113. fields.response_status = this.response.status;
  114. }
  115. return fields;
  116. };
  117. /**
  118. * Represents a client to the Token Server.
  119. *
  120. * http://docs.services.mozilla.com/token/index.html
  121. *
  122. * The Token Server supports obtaining tokens for arbitrary apps by
  123. * constructing URI paths of the form <app>/<app_version>. However, the service
  124. * discovery mechanism emphasizes the use of full URIs and tries to not force
  125. * the client to manipulate URIs. This client currently enforces this practice
  126. * by not implementing an API which would perform URI manipulation.
  127. *
  128. * If you are tempted to implement this API in the future, consider this your
  129. * warning that you may be doing it wrong and that you should store full URIs
  130. * instead.
  131. *
  132. * Areas to Improve:
  133. *
  134. * - The server sends a JSON response on error. The client does not currently
  135. * parse this. It might be convenient if it did.
  136. * - Currently most non-200 status codes are rolled into one error type. It
  137. * might be helpful if callers had a richer API that communicated who was
  138. * at fault (e.g. differentiating a 503 from a 401).
  139. */
  140. this.TokenServerClient = function TokenServerClient() {
  141. this._log = Log.repository.getLogger("Common.TokenServerClient");
  142. let level = Services.prefs.getCharPref(PREF_LOG_LEVEL, "Debug");
  143. this._log.level = Log.Level[level];
  144. }
  145. TokenServerClient.prototype = {
  146. /**
  147. * Logger instance.
  148. */
  149. _log: null,
  150. /**
  151. * Obtain a token from a BrowserID assertion against a specific URL.
  152. *
  153. * This asynchronously obtains the token. The callback receives 2 arguments:
  154. *
  155. * (TokenServerClientError | null) If no token could be obtained, this
  156. * will be a TokenServerClientError instance describing why. The
  157. * type seen defines the type of error encountered. If an HTTP response
  158. * was seen, a RESTResponse instance will be stored in the `response`
  159. * property of this object. If there was no error and a token is
  160. * available, this will be null.
  161. *
  162. * (map | null) On success, this will be a map containing the results from
  163. * the server. If there was an error, this will be null. The map has the
  164. * following properties:
  165. *
  166. * id (string) HTTP MAC public key identifier.
  167. * key (string) HTTP MAC shared symmetric key.
  168. * endpoint (string) URL where service can be connected to.
  169. * uid (string) user ID for requested service.
  170. * duration (string) the validity duration of the issued token.
  171. *
  172. * Terms of Service Acceptance
  173. * ---------------------------
  174. *
  175. * Some services require users to accept terms of service before they can
  176. * obtain a token. If a service requires ToS acceptance, the error passed
  177. * to the callback will be a `TokenServerClientServerError` with the
  178. * `cause` property set to "conditions-required". The `urls` property of that
  179. * instance will be a map of string keys to string URL values. The user-agent
  180. * should prompt the user to accept the content at these URLs.
  181. *
  182. * Clients signify acceptance of the terms of service by sending a token
  183. * request with additional metadata. This is controlled by the
  184. * `conditionsAccepted` argument to this function. Clients only need to set
  185. * this flag once per service and the server remembers acceptance. If
  186. * the conditions for the service change, the server may request
  187. * clients agree to terms again. Therefore, clients should always be
  188. * prepared to handle a conditions required response.
  189. *
  190. * Clients should not blindly send acceptance to conditions. Instead, clients
  191. * should set `conditionsAccepted` if and only if the server asks for
  192. * acceptance, the conditions are displayed to the user, and the user agrees
  193. * to them.
  194. *
  195. * Example Usage
  196. * -------------
  197. *
  198. * let client = new TokenServerClient();
  199. * let assertion = getBrowserIDAssertionFromSomewhere();
  200. * let url = "https://token.services.mozilla.com/1.0/sync/2.0";
  201. *
  202. * client.getTokenFromBrowserIDAssertion(url, assertion,
  203. * function onResponse(error, result) {
  204. * if (error) {
  205. * if (error.cause == "conditions-required") {
  206. * promptConditionsAcceptance(error.urls, function onAccept() {
  207. * client.getTokenFromBrowserIDAssertion(url, assertion,
  208. * onResponse, true);
  209. * }
  210. * return;
  211. * }
  212. *
  213. * // Do other error handling.
  214. * return;
  215. * }
  216. *
  217. * let {
  218. * id: id, key: key, uid: uid, endpoint: endpoint, duration: duration
  219. * } = result;
  220. * // Do stuff with data and carry on.
  221. * });
  222. *
  223. * @param url
  224. * (string) URL to fetch token from.
  225. * @param assertion
  226. * (string) BrowserID assertion to exchange token for.
  227. * @param cb
  228. * (function) Callback to be invoked with result of operation.
  229. * @param conditionsAccepted
  230. * (bool) Whether to send acceptance to service conditions.
  231. */
  232. getTokenFromBrowserIDAssertion:
  233. function getTokenFromBrowserIDAssertion(url, assertion, cb, addHeaders={}) {
  234. if (!url) {
  235. throw new TokenServerClientError("url argument is not valid.");
  236. }
  237. if (!assertion) {
  238. throw new TokenServerClientError("assertion argument is not valid.");
  239. }
  240. if (!cb) {
  241. throw new TokenServerClientError("cb argument is not valid.");
  242. }
  243. this._log.debug("Beginning BID assertion exchange: " + url);
  244. let req = this.newRESTRequest(url);
  245. req.setHeader("Accept", "application/json");
  246. req.setHeader("Authorization", "BrowserID " + assertion);
  247. for (let header in addHeaders) {
  248. req.setHeader(header, addHeaders[header]);
  249. }
  250. let client = this;
  251. req.get(function onResponse(error) {
  252. if (error) {
  253. cb(new TokenServerClientNetworkError(error), null);
  254. return;
  255. }
  256. let self = this;
  257. function callCallback(error, result) {
  258. if (!cb) {
  259. self._log.warn("Callback already called! Did it throw?");
  260. return;
  261. }
  262. try {
  263. cb(error, result);
  264. } catch (ex) {
  265. self._log.warn("Exception when calling user-supplied callback", ex);
  266. }
  267. cb = null;
  268. }
  269. try {
  270. client._processTokenResponse(this.response, callCallback);
  271. } catch (ex) {
  272. this._log.warn("Error processing token server response", ex);
  273. let error = new TokenServerClientError(ex);
  274. error.response = this.response;
  275. callCallback(error, null);
  276. }
  277. });
  278. },
  279. /**
  280. * Handler to process token request responses.
  281. *
  282. * @param response
  283. * RESTResponse from token HTTP request.
  284. * @param cb
  285. * The original callback passed to the public API.
  286. */
  287. _processTokenResponse: function processTokenResponse(response, cb) {
  288. this._log.debug("Got token response: " + response.status);
  289. // Responses should *always* be JSON, even in the case of 4xx and 5xx
  290. // errors. If we don't see JSON, the server is likely very unhappy.
  291. let ct = response.headers["content-type"] || "";
  292. if (ct != "application/json" && !ct.startsWith("application/json;")) {
  293. this._log.warn("Did not receive JSON response. Misconfigured server?");
  294. this._log.debug("Content-Type: " + ct);
  295. this._log.debug("Body: " + response.body);
  296. let error = new TokenServerClientServerError("Non-JSON response.",
  297. "malformed-response");
  298. error.response = response;
  299. cb(error, null);
  300. return;
  301. }
  302. let result;
  303. try {
  304. result = JSON.parse(response.body);
  305. } catch (ex) {
  306. this._log.warn("Invalid JSON returned by server: " + response.body);
  307. let error = new TokenServerClientServerError("Malformed JSON.",
  308. "malformed-response");
  309. error.response = response;
  310. cb(error, null);
  311. return;
  312. }
  313. // Any response status can have X-Backoff or X-Weave-Backoff headers.
  314. this._maybeNotifyBackoff(response, "x-weave-backoff");
  315. this._maybeNotifyBackoff(response, "x-backoff");
  316. // The service shouldn't have any 3xx, so we don't need to handle those.
  317. if (response.status != 200) {
  318. // We /should/ have a Cornice error report in the JSON. We log that to
  319. // help with debugging.
  320. if ("errors" in result) {
  321. // This could throw, but this entire function is wrapped in a try. If
  322. // the server is sending something not an array of objects, it has
  323. // failed to keep its contract with us and there is little we can do.
  324. for (let error of result.errors) {
  325. this._log.info("Server-reported error: " + JSON.stringify(error));
  326. }
  327. }
  328. let error = new TokenServerClientServerError();
  329. error.response = response;
  330. if (response.status == 400) {
  331. error.message = "Malformed request.";
  332. error.cause = "malformed-request";
  333. } else if (response.status == 401) {
  334. // Cause can be invalid-credentials, invalid-timestamp, or
  335. // invalid-generation.
  336. error.message = "Authentication failed.";
  337. error.cause = result.status;
  338. }
  339. // 403 should represent a "condition acceptance needed" response.
  340. //
  341. // The extra validation of "urls" is important. We don't want to signal
  342. // conditions required unless we are absolutely sure that is what the
  343. // server is asking for.
  344. else if (response.status == 403) {
  345. if (!("urls" in result)) {
  346. this._log.warn("403 response without proper fields!");
  347. this._log.warn("Response body: " + response.body);
  348. error.message = "Missing JSON fields.";
  349. error.cause = "malformed-response";
  350. } else if (typeof(result.urls) != "object") {
  351. error.message = "urls field is not a map.";
  352. error.cause = "malformed-response";
  353. } else {
  354. error.message = "Conditions must be accepted.";
  355. error.cause = "conditions-required";
  356. error.urls = result.urls;
  357. }
  358. } else if (response.status == 404) {
  359. error.message = "Unknown service.";
  360. error.cause = "unknown-service";
  361. }
  362. // A Retry-After header should theoretically only appear on a 503, but
  363. // we'll look for it on any error response.
  364. this._maybeNotifyBackoff(response, "retry-after");
  365. cb(error, null);
  366. return;
  367. }
  368. for (let k of ["id", "key", "api_endpoint", "uid", "duration"]) {
  369. if (!(k in result)) {
  370. let error = new TokenServerClientServerError("Expected key not " +
  371. " present in result: " +
  372. k);
  373. error.cause = "malformed-response";
  374. error.response = response;
  375. cb(error, null);
  376. return;
  377. }
  378. }
  379. this._log.debug("Successful token response");
  380. cb(null, {
  381. id: result.id,
  382. key: result.key,
  383. endpoint: result.api_endpoint,
  384. uid: result.uid,
  385. duration: result.duration,
  386. hashed_fxa_uid: result.hashed_fxa_uid,
  387. });
  388. },
  389. /*
  390. * The prefix used for all notifications sent by this module. This
  391. * allows the handler of notifications to be sure they are handling
  392. * notifications for the service they expect.
  393. *
  394. * If not set, no notifications will be sent.
  395. */
  396. observerPrefix: null,
  397. // Given an optional header value, notify that a backoff has been requested.
  398. _maybeNotifyBackoff: function (response, headerName) {
  399. if (!this.observerPrefix) {
  400. return;
  401. }
  402. let headerVal = response.headers[headerName];
  403. if (!headerVal) {
  404. return;
  405. }
  406. let backoffInterval;
  407. try {
  408. backoffInterval = parseInt(headerVal, 10);
  409. } catch (ex) {
  410. this._log.error("TokenServer response had invalid backoff value in '" +
  411. headerName + "' header: " + headerVal);
  412. return;
  413. }
  414. Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval);
  415. },
  416. // override points for testing.
  417. newRESTRequest: function(url) {
  418. return new RESTRequest(url);
  419. }
  420. };