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/. */
8 * HAWK is an HTTP authentication scheme using a message authentication code
9 * (MAC) algorithm to provide partial HTTP request cryptographic verification.
11 * For details, see: https://github.com/hueniverse/hawk
13 * With HAWK, it is essential that the clocks on clients and server not have an
14 * absolute delta of greater than one minute, as the HAWK protocol uses
15 * timestamps to reduce the possibility of replay attacks. However, it is
16 * likely that some clients' clocks will be more than a little off, especially
17 * in mobile devices, which would break HAWK-based services (like sync and
18 * firefox accounts) for those clients.
20 * This library provides a stateful HAWK client that calculates (roughly) the
21 * clock delta on the client vs the server. The library provides an interface
22 * for deriving HAWK credentials and making HAWK-authenticated REST requests to
23 * a single remote server. Therefore, callers who want to interact with
24 * multiple HAWK services should instantiate one HawkClient per service.
27 var EXPORTED_SYMBOLS = ["HawkClient"];
29 const { HAWKAuthenticatedRESTRequest } = ChromeUtils.import(
30 "resource://services-common/hawkrequest.js"
32 const { Observers } = ChromeUtils.import(
33 "resource://services-common/observers.js"
35 const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
36 const { XPCOMUtils } = ChromeUtils.import(
37 "resource://gre/modules/XPCOMUtils.jsm"
39 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
41 // log.appender.dump should be one of "Fatal", "Error", "Warn", "Info", "Config",
42 // "Debug", "Trace" or "All". If none is specified, "Error" will be used by
44 // Note however that Sync will also add this log to *its* DumpAppender, so
45 // in a Sync context it shouldn't be necessary to adjust this - however, that
46 // also means error logs are likely to be dump'd twice but that's OK.
47 const PREF_LOG_LEVEL = "services.common.hawk.log.appender.dump";
49 // A pref that can be set so "sensitive" information (eg, personally
50 // identifiable info, credentials, etc) will be logged.
51 const PREF_LOG_SENSITIVE_DETAILS = "services.common.hawk.log.sensitive";
53 XPCOMUtils.defineLazyGetter(this, "log", function() {
54 let log = Log.repository.getLogger("Hawk");
55 // We set the log itself to "debug" and set the level from the preference to
56 // the appender. This allows other things to send the logs to different
57 // appenders, while still allowing the pref to control what is seen via dump()
58 log.level = Log.Level.Debug;
59 let appender = new Log.DumpAppender();
60 log.addAppender(appender);
61 appender.level = Log.Level.Error;
64 Services.prefs.getPrefType(PREF_LOG_LEVEL) ==
65 Ci.nsIPrefBranch.PREF_STRING &&
66 Services.prefs.getCharPref(PREF_LOG_LEVEL);
67 appender.level = Log.Level[level] || Log.Level.Error;
75 // A boolean to indicate if personally identifiable information (or anything
76 // else sensitive, such as credentials) should be logged.
77 XPCOMUtils.defineLazyGetter(this, "logPII", function() {
79 return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS);
86 * A general purpose client for making HAWK authenticated requests to a single
87 * host. Keeps track of the clock offset between the client and the host for
88 * computation of the timestamp in the HAWK Authorization header.
90 * Clients should create one HawkClient object per each server they wish to
96 var HawkClient = function(host) {
99 // Clock offset in milliseconds between our client's clock and the date
100 // reported in responses from our host.
101 this._localtimeOffsetMsec = 0;
104 this.HawkClient.prototype = {
106 * Construct an error message for a response. Private.
108 * @param restResponse
109 * A RESTResponse object from a RESTRequest
112 * A string or object describing the error
114 _constructError(restResponse, error) {
117 // This object is likely to be JSON.stringify'd, but neither Error()
118 // objects nor Components.Exception objects do the right thing there,
119 // so we add a new element which is simply the .toString() version of
120 // the error object, so it does appear in JSON'd values.
121 errorString: error.toString(),
122 message: restResponse.statusText,
123 code: restResponse.status,
124 errno: restResponse.status,
126 return this.code + ": " + this.message;
130 restResponse.headers && restResponse.headers["retry-after"];
131 retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter;
133 errorObj.retryAfter = retryAfter;
134 // and notify observers of the retry interval
135 if (this.observerPrefix) {
136 Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter);
144 * Update clock offset by determining difference from date gives in the (RFC
145 * 1123) Date header of a server response. Because HAWK tolerates a window
146 * of one minute of clock skew (so two minutes total since the skew can be
147 * positive or negative), the simple method of calculating offset here is
148 * probably good enough. We keep the value in milliseconds to make life
149 * easier, even though the value will not have millisecond accuracy.
152 * An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT")
154 * For HAWK clock skew and replay protection, see
155 * https://github.com/hueniverse/hawk#replay-protection
157 _updateClockOffset(dateString) {
159 let serverDateMsec = Date.parse(dateString);
160 this._localtimeOffsetMsec = serverDateMsec - this.now();
162 "Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec
165 log.warn("Bad date header in server response: " + dateString);
170 * Get the current clock offset in milliseconds.
172 * The offset is the number of milliseconds that must be added to the client
173 * clock to make it equal to the server clock. For example, if the client is
174 * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
176 get localtimeOffsetMsec() {
177 return this._localtimeOffsetMsec;
181 * return current time in milliseconds
187 /* A general method for sending raw RESTRequest calls authorized using HAWK
192 * The HTTP request method
196 * An object that can be encodable as JSON as the payload of the
198 * @param extraHeaders
199 * An object with header/value pairs to send with the request.
201 * Returns a promise that resolves to the response of the API call,
202 * or is rejected with an error. If the server response can be parsed
203 * as JSON and contains an 'error' property, the promise will be
204 * rejected with this JSON-parsed response.
214 method = method.toLowerCase();
216 let uri = this.host + path;
220 localtimeOffsetMsec: this.localtimeOffsetMsec,
221 headers: extraHeaders,
224 let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
226 let restResponse = await request[method](payloadObj).catch(e => {
227 // Keep a reference to the error, log a message about it, and return the
230 log.warn("hawk request error", error);
231 return request.response;
234 // This shouldn't happen anymore, but it's not exactly difficult to handle.
239 let status = restResponse.status;
247 restResponse.statusText
250 log.debug("Response text", restResponse.body);
253 // All responses may have backoff headers, which are a server-side safety
254 // valve to allow slowing down clients without hurting performance.
255 this._maybeNotifyBackoff(restResponse, "x-weave-backoff");
256 this._maybeNotifyBackoff(restResponse, "x-backoff");
259 // When things really blow up, reconstruct an error object that follows
260 // the general format of the server on error responses.
261 throw this._constructError(restResponse, error);
264 this._updateClockOffset(restResponse.headers.date);
266 if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) {
267 // Retry once if we were rejected due to a bad timestamp.
268 // Clock offset is adjusted already in the top of this function.
269 log.debug("Received 401 for " + path + ": retrying");
280 // If the server returned a json error message, use it in the rejection
283 // In the case of a 401, in which we are probably being rejected for a
284 // bad timestamp, retry exactly once, during which time clock offset will
287 let jsonResponse = {};
289 jsonResponse = JSON.parse(restResponse.body);
292 let okResponse = 200 <= status && status < 300;
293 if (!okResponse || jsonResponse.error) {
294 if (jsonResponse.error) {
297 throw this._constructError(restResponse, "Request failed");
300 // It's up to the caller to know how to decode the response.
301 // We just return the whole response.
306 * The prefix used for all notifications sent by this module. This
307 * allows the handler of notifications to be sure they are handling
308 * notifications for the service they expect.
310 * If not set, no notifications will be sent.
312 observerPrefix: null,
314 // Given an optional header value, notify that a backoff has been requested.
315 _maybeNotifyBackoff(response, headerName) {
316 if (!this.observerPrefix || !response.headers) {
319 let headerVal = response.headers[headerName];
325 backoffInterval = parseInt(headerVal, 10);
328 "hawkclient response had invalid backoff value in '" +
336 this.observerPrefix + ":backoff:interval",
341 // override points for testing.
342 newHAWKAuthenticatedRESTRequest(uri, credentials, extra) {
343 return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);