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/. */
5 import { Log } from "resource://gre/modules/Log.sys.mjs";
7 import { RESTRequest } from "resource://services-common/rest.sys.mjs";
8 import { Observers } from "resource://services-common/observers.sys.mjs";
10 const PREF_LOG_LEVEL = "services.common.log.logger.tokenserverclient";
13 * Represents a TokenServerClient error that occurred on the client.
15 * This is the base type for all errors raised by client operations.
18 * (string) Error message.
20 export function TokenServerClientError(message) {
21 this.name = "TokenServerClientError";
22 this.message = message || "Client error.";
23 // Without explicitly setting .stack, all stacks from these errors will point
24 // to the "new Error()" call a few lines down, which isn't helpful.
25 this.stack = Error().stack;
28 TokenServerClientError.prototype = new Error();
29 TokenServerClientError.prototype.constructor = TokenServerClientError;
30 TokenServerClientError.prototype._toStringFields = function () {
31 return { message: this.message };
33 TokenServerClientError.prototype.toString = function () {
34 return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";
36 TokenServerClientError.prototype.toJSON = function () {
37 let result = this._toStringFields();
38 result.name = this.name;
43 * Represents a TokenServerClient error that occurred in the network layer.
46 * The underlying error thrown by the network layer.
48 export function TokenServerClientNetworkError(error) {
49 this.name = "TokenServerClientNetworkError";
51 this.stack = Error().stack;
54 TokenServerClientNetworkError.prototype = new TokenServerClientError();
55 TokenServerClientNetworkError.prototype.constructor =
56 TokenServerClientNetworkError;
57 TokenServerClientNetworkError.prototype._toStringFields = function () {
58 return { error: this.error };
62 * Represents a TokenServerClient error that occurred on the server.
64 * This type will be encountered for all non-200 response codes from the
65 * server. The type of error is strongly enumerated and is stored in the
66 * `cause` property. This property can have the following string values:
68 * invalid-credentials -- A token could not be obtained because
69 * the credentials presented by the client were invalid.
71 * unknown-service -- The requested service was not found.
73 * malformed-request -- The server rejected the request because it
74 * was invalid. If you see this, code in this file is likely wrong.
76 * malformed-response -- The response from the server was not what was
79 * general -- A general server error has occurred. Clients should
80 * interpret this as an opaque failure.
83 * (string) Error message.
85 export function TokenServerClientServerError(message, cause = "general") {
86 this.now = new Date().toISOString(); // may be useful to diagnose time-skew issues.
87 this.name = "TokenServerClientServerError";
88 this.message = message || "Server error.";
90 this.stack = Error().stack;
93 TokenServerClientServerError.prototype = new TokenServerClientError();
94 TokenServerClientServerError.prototype.constructor =
95 TokenServerClientServerError;
97 TokenServerClientServerError.prototype._toStringFields = function () {
100 message: this.message,
104 fields.response_body = this.response.body;
105 fields.response_headers = this.response.headers;
106 fields.response_status = this.response.status;
112 * Represents a client to the Token Server.
114 * http://docs.services.mozilla.com/token/index.html
116 * The Token Server was designed to support obtaining tokens for arbitrary apps by
117 * constructing URI paths of the form <app>/<app_version>. In practice this was
118 * never used and it only supports an <app> value of `sync`, and the API presented
119 * here reflects that.
123 * - The server sends a JSON response on error. The client does not currently
124 * parse this. It might be convenient if it did.
125 * - Currently most non-200 status codes are rolled into one error type. It
126 * might be helpful if callers had a richer API that communicated who was
127 * at fault (e.g. differentiating a 503 from a 401).
129 export function TokenServerClient() {
130 this._log = Log.repository.getLogger("Services.Common.TokenServerClient");
131 this._log.manageLevelFromPref(PREF_LOG_LEVEL);
134 TokenServerClient.prototype = {
141 * Obtain a token from a provided OAuth token against a specific URL.
143 * This asynchronously obtains the token.
144 * It returns a Promise that resolves or rejects:
147 * (TokenServerClientError) If no token could be obtained, this
148 * will be a TokenServerClientError instance describing why. The
149 * type seen defines the type of error encountered. If an HTTP response
150 * was seen, a RESTResponse instance will be stored in the `response`
151 * property of this object. If there was no error and a token is
152 * available, this will be null.
155 * (map) On success, this will be a map containing the results from
156 * the server. If there was an error, this will be null. The map has the
157 * following properties:
159 * id (string) HTTP MAC public key identifier.
160 * key (string) HTTP MAC shared symmetric key.
161 * endpoint (string) URL where service can be connected to.
162 * uid (string) user ID for requested service.
163 * duration (string) the validity duration of the issued token.
168 * let client = new TokenServerClient();
169 * let access_token = getOAuthAccessTokenFromSomewhere();
170 * let url = "https://token.services.mozilla.com/1.0/sync/2.0";
173 * const result = await client.getTokenUsingOAuth(url, access_token);
174 * let {id, key, uid, endpoint, duration} = result;
175 * // Do stuff with data and carry on.
179 * Obtain a token from a provided OAuth token against a specific URL.
182 * (string) URL to fetch token from.
184 * (string) FxA OAuth Token to exchange token for.
186 * (object) Extra headers for the request.
188 async getTokenUsingOAuth(url, oauthToken, addHeaders = {}) {
189 this._log.debug("Beginning OAuth token exchange: " + url);
192 throw new TokenServerClientError("oauthToken argument is not valid.");
195 return this._tokenServerExchangeRequest(
197 `Bearer ${oauthToken}`,
203 * Performs the exchange request to the token server to
204 * produce a token based on the authorizationHeader input.
207 * (string) URL to fetch token from.
208 * @param authorizationHeader
209 * (string) The auth header string that populates the 'Authorization' header.
211 * (object) Extra headers for the request.
213 async _tokenServerExchangeRequest(url, authorizationHeader, addHeaders = {}) {
215 throw new TokenServerClientError("url argument is not valid.");
218 if (!authorizationHeader) {
219 throw new TokenServerClientError(
220 "authorizationHeader argument is not valid."
224 let req = this.newRESTRequest(url);
225 req.setHeader("Accept", "application/json");
226 req.setHeader("Authorization", authorizationHeader);
228 for (let header in addHeaders) {
229 req.setHeader(header, addHeaders[header]);
233 response = await req.get();
235 throw new TokenServerClientNetworkError(err);
239 return this._processTokenResponse(response);
241 if (ex instanceof TokenServerClientServerError) {
244 this._log.warn("Error processing token server response", ex);
245 let error = new TokenServerClientError(ex);
246 error.response = response;
252 * Handler to process token request responses.
255 * RESTResponse from token HTTP request.
257 _processTokenResponse(response) {
258 this._log.debug("Got token response: " + response.status);
260 // Responses should *always* be JSON, even in the case of 4xx and 5xx
261 // errors. If we don't see JSON, the server is likely very unhappy.
262 let ct = response.headers["content-type"] || "";
263 if (ct != "application/json" && !ct.startsWith("application/json;")) {
264 this._log.warn("Did not receive JSON response. Misconfigured server?");
265 this._log.debug("Content-Type: " + ct);
266 this._log.debug("Body: " + response.body);
268 let error = new TokenServerClientServerError(
269 "Non-JSON response.",
272 error.response = response;
278 result = JSON.parse(response.body);
280 this._log.warn("Invalid JSON returned by server: " + response.body);
281 let error = new TokenServerClientServerError(
285 error.response = response;
289 // Any response status can have X-Backoff or X-Weave-Backoff headers.
290 this._maybeNotifyBackoff(response, "x-weave-backoff");
291 this._maybeNotifyBackoff(response, "x-backoff");
293 // The service shouldn't have any 3xx, so we don't need to handle those.
294 if (response.status != 200) {
295 // We /should/ have a Cornice error report in the JSON. We log that to
296 // help with debugging.
297 if ("errors" in result) {
298 // This could throw, but this entire function is wrapped in a try. If
299 // the server is sending something not an array of objects, it has
300 // failed to keep its contract with us and there is little we can do.
301 for (let error of result.errors) {
302 this._log.info("Server-reported error: " + JSON.stringify(error));
306 let error = new TokenServerClientServerError();
307 error.response = response;
309 if (response.status == 400) {
310 error.message = "Malformed request.";
311 error.cause = "malformed-request";
312 } else if (response.status == 401) {
313 // Cause can be invalid-credentials, invalid-timestamp, or
314 // invalid-generation.
315 error.message = "Authentication failed.";
316 error.cause = result.status;
317 } else if (response.status == 404) {
318 error.message = "Unknown service.";
319 error.cause = "unknown-service";
322 // A Retry-After header should theoretically only appear on a 503, but
323 // we'll look for it on any error response.
324 this._maybeNotifyBackoff(response, "retry-after");
329 for (let k of ["id", "key", "api_endpoint", "uid", "duration"]) {
330 if (!(k in result)) {
331 let error = new TokenServerClientServerError(
332 "Expected key not present in result: " + k
334 error.cause = "malformed-response";
335 error.response = response;
340 this._log.debug("Successful token response");
344 endpoint: result.api_endpoint,
346 duration: result.duration,
347 hashed_fxa_uid: result.hashed_fxa_uid,
348 node_type: result.node_type,
353 * The prefix used for all notifications sent by this module. This
354 * allows the handler of notifications to be sure they are handling
355 * notifications for the service they expect.
357 * If not set, no notifications will be sent.
359 observerPrefix: null,
361 // Given an optional header value, notify that a backoff has been requested.
362 _maybeNotifyBackoff(response, headerName) {
363 if (!this.observerPrefix) {
366 let headerVal = response.headers[headerName];
372 backoffInterval = parseInt(headerVal, 10);
375 "TokenServer response had invalid backoff value in '" +
383 this.observerPrefix + ":backoff:interval",
388 // override points for testing.
389 newRESTRequest(url) {
390 return new RESTRequest(url);