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 = TokenServerClientNetworkError;
56 TokenServerClientNetworkError.prototype._toStringFields = function() {
57 return { error: this.error };
61 * Represents a TokenServerClient error that occurred on the server.
63 * This type will be encountered for all non-200 response codes from the
64 * server. The type of error is strongly enumerated and is stored in the
65 * `cause` property. This property can have the following string values:
67 * invalid-credentials -- A token could not be obtained because
68 * the credentials presented by the client were invalid.
70 * unknown-service -- The requested service was not found.
72 * malformed-request -- The server rejected the request because it
73 * was invalid. If you see this, code in this file is likely wrong.
75 * malformed-response -- The response from the server was not what was
78 * general -- A general server error has occurred. Clients should
79 * interpret this as an opaque failure.
82 * (string) Error message.
84 export function TokenServerClientServerError(message, cause = "general") {
85 this.now = new Date().toISOString(); // may be useful to diagnose time-skew issues.
86 this.name = "TokenServerClientServerError";
87 this.message = message || "Server error.";
89 this.stack = Error().stack;
92 TokenServerClientServerError.prototype = new TokenServerClientError();
93 TokenServerClientServerError.prototype.constructor = TokenServerClientServerError;
95 TokenServerClientServerError.prototype._toStringFields = function() {
98 message: this.message,
102 fields.response_body = this.response.body;
103 fields.response_headers = this.response.headers;
104 fields.response_status = this.response.status;
110 * Represents a client to the Token Server.
112 * http://docs.services.mozilla.com/token/index.html
114 * The Token Server was designed to support obtaining tokens for arbitrary apps by
115 * constructing URI paths of the form <app>/<app_version>. In practice this was
116 * never used and it only supports an <app> value of `sync`, and the API presented
117 * here reflects that.
121 * - The server sends a JSON response on error. The client does not currently
122 * parse this. It might be convenient if it did.
123 * - Currently most non-200 status codes are rolled into one error type. It
124 * might be helpful if callers had a richer API that communicated who was
125 * at fault (e.g. differentiating a 503 from a 401).
127 export function TokenServerClient() {
128 this._log = Log.repository.getLogger("Services.Common.TokenServerClient");
129 this._log.manageLevelFromPref(PREF_LOG_LEVEL);
132 TokenServerClient.prototype = {
139 * Obtain a token from a provided OAuth token against a specific URL.
141 * This asynchronously obtains the token.
142 * It returns a Promise that resolves or rejects:
145 * (TokenServerClientError) If no token could be obtained, this
146 * will be a TokenServerClientError instance describing why. The
147 * type seen defines the type of error encountered. If an HTTP response
148 * was seen, a RESTResponse instance will be stored in the `response`
149 * property of this object. If there was no error and a token is
150 * available, this will be null.
153 * (map) On success, this will be a map containing the results from
154 * the server. If there was an error, this will be null. The map has the
155 * following properties:
157 * id (string) HTTP MAC public key identifier.
158 * key (string) HTTP MAC shared symmetric key.
159 * endpoint (string) URL where service can be connected to.
160 * uid (string) user ID for requested service.
161 * duration (string) the validity duration of the issued token.
166 * let client = new TokenServerClient();
167 * let access_token = getOAuthAccessTokenFromSomewhere();
168 * let url = "https://token.services.mozilla.com/1.0/sync/2.0";
171 * const result = await client.getTokenUsingOAuth(url, access_token);
172 * let {id, key, uid, endpoint, duration} = result;
173 * // Do stuff with data and carry on.
177 * Obtain a token from a provided OAuth token against a specific URL.
180 * (string) URL to fetch token from.
182 * (string) FxA OAuth Token to exchange token for.
184 * (object) Extra headers for the request.
186 async getTokenUsingOAuth(url, oauthToken, addHeaders = {}) {
187 this._log.debug("Beginning OAuth token exchange: " + url);
190 throw new TokenServerClientError("oauthToken argument is not valid.");
193 return this._tokenServerExchangeRequest(
195 `Bearer ${oauthToken}`,
201 * Performs the exchange request to the token server to
202 * produce a token based on the authorizationHeader input.
205 * (string) URL to fetch token from.
206 * @param authorizationHeader
207 * (string) The auth header string that populates the 'Authorization' header.
209 * (object) Extra headers for the request.
211 async _tokenServerExchangeRequest(url, authorizationHeader, addHeaders = {}) {
213 throw new TokenServerClientError("url argument is not valid.");
216 if (!authorizationHeader) {
217 throw new TokenServerClientError(
218 "authorizationHeader argument is not valid."
222 let req = this.newRESTRequest(url);
223 req.setHeader("Accept", "application/json");
224 req.setHeader("Authorization", authorizationHeader);
226 for (let header in addHeaders) {
227 req.setHeader(header, addHeaders[header]);
231 response = await req.get();
233 throw new TokenServerClientNetworkError(err);
237 return this._processTokenResponse(response);
239 if (ex instanceof TokenServerClientServerError) {
242 this._log.warn("Error processing token server response", ex);
243 let error = new TokenServerClientError(ex);
244 error.response = response;
250 * Handler to process token request responses.
253 * RESTResponse from token HTTP request.
255 _processTokenResponse(response) {
256 this._log.debug("Got token response: " + response.status);
258 // Responses should *always* be JSON, even in the case of 4xx and 5xx
259 // errors. If we don't see JSON, the server is likely very unhappy.
260 let ct = response.headers["content-type"] || "";
261 if (ct != "application/json" && !ct.startsWith("application/json;")) {
262 this._log.warn("Did not receive JSON response. Misconfigured server?");
263 this._log.debug("Content-Type: " + ct);
264 this._log.debug("Body: " + response.body);
266 let error = new TokenServerClientServerError(
267 "Non-JSON response.",
270 error.response = response;
276 result = JSON.parse(response.body);
278 this._log.warn("Invalid JSON returned by server: " + response.body);
279 let error = new TokenServerClientServerError(
283 error.response = response;
287 // Any response status can have X-Backoff or X-Weave-Backoff headers.
288 this._maybeNotifyBackoff(response, "x-weave-backoff");
289 this._maybeNotifyBackoff(response, "x-backoff");
291 // The service shouldn't have any 3xx, so we don't need to handle those.
292 if (response.status != 200) {
293 // We /should/ have a Cornice error report in the JSON. We log that to
294 // help with debugging.
295 if ("errors" in result) {
296 // This could throw, but this entire function is wrapped in a try. If
297 // the server is sending something not an array of objects, it has
298 // failed to keep its contract with us and there is little we can do.
299 for (let error of result.errors) {
300 this._log.info("Server-reported error: " + JSON.stringify(error));
304 let error = new TokenServerClientServerError();
305 error.response = response;
307 if (response.status == 400) {
308 error.message = "Malformed request.";
309 error.cause = "malformed-request";
310 } else if (response.status == 401) {
311 // Cause can be invalid-credentials, invalid-timestamp, or
312 // invalid-generation.
313 error.message = "Authentication failed.";
314 error.cause = result.status;
315 } else if (response.status == 404) {
316 error.message = "Unknown service.";
317 error.cause = "unknown-service";
320 // A Retry-After header should theoretically only appear on a 503, but
321 // we'll look for it on any error response.
322 this._maybeNotifyBackoff(response, "retry-after");
327 for (let k of ["id", "key", "api_endpoint", "uid", "duration"]) {
328 if (!(k in result)) {
329 let error = new TokenServerClientServerError(
330 "Expected key not present in result: " + k
332 error.cause = "malformed-response";
333 error.response = response;
338 this._log.debug("Successful token response");
342 endpoint: result.api_endpoint,
344 duration: result.duration,
345 hashed_fxa_uid: result.hashed_fxa_uid,
346 node_type: result.node_type,
351 * The prefix used for all notifications sent by this module. This
352 * allows the handler of notifications to be sure they are handling
353 * notifications for the service they expect.
355 * If not set, no notifications will be sent.
357 observerPrefix: null,
359 // Given an optional header value, notify that a backoff has been requested.
360 _maybeNotifyBackoff(response, headerName) {
361 if (!this.observerPrefix) {
364 let headerVal = response.headers[headerName];
370 backoffInterval = parseInt(headerVal, 10);
373 "TokenServer response had invalid backoff value in '" +
381 this.observerPrefix + ":backoff:interval",
386 // override points for testing.
387 newRESTRequest(url) {
388 return new RESTRequest(url);