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 this.EXPORTED_SYMBOLS = ["HawkClient"];
29 const {interfaces: Ci, utils: Cu} = Components;
31 Cu.import("resource://services-common/utils.js");
32 Cu.import("resource://services-crypto/utils.js");
33 Cu.import("resource://services-common/hawkrequest.js");
34 Cu.import("resource://services-common/observers.js");
35 Cu.import("resource://gre/modules/Promise.jsm");
36 Cu.import("resource://gre/modules/Log.jsm");
37 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
38 Cu.import("resource://gre/modules/Services.jsm");
40 // loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config",
41 // "Debug", "Trace" or "All". If none is specified, "Error" will be used by
43 const PREF_LOG_LEVEL = "services.hawk.loglevel";
45 // A pref that can be set so "sensitive" information (eg, personally
46 // identifiable info, credentials, etc) will be logged.
47 const PREF_LOG_SENSITIVE_DETAILS = "services.hawk.log.sensitive";
49 XPCOMUtils.defineLazyGetter(this, "log", function() {
50 let log = Log.repository.getLogger("Hawk");
51 log.addAppender(new Log.DumpAppender());
52 log.level = Log.Level.Error;
55 Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
56 && Services.prefs.getCharPref(PREF_LOG_LEVEL);
57 log.level = Log.Level[level] || Log.Level.Error;
65 // A boolean to indicate if personally identifiable information (or anything
66 // else sensitive, such as credentials) should be logged.
67 XPCOMUtils.defineLazyGetter(this, 'logPII', function() {
69 return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS);
76 * A general purpose client for making HAWK authenticated requests to a single
77 * host. Keeps track of the clock offset between the client and the host for
78 * computation of the timestamp in the HAWK Authorization header.
80 * Clients should create one HawkClient object per each server they wish to
86 this.HawkClient = function(host) {
89 // Clock offset in milliseconds between our client's clock and the date
90 // reported in responses from our host.
91 this._localtimeOffsetMsec = 0;
94 this.HawkClient.prototype = {
97 * Construct an error message for a response. Private.
100 * A RESTResponse object from a RESTRequest
103 * A string describing the error
105 _constructError: function(restResponse, errorString) {
108 message: restResponse.statusText,
109 code: restResponse.status,
110 errno: restResponse.status
112 let retryAfter = restResponse.headers && restResponse.headers["retry-after"];
113 retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter;
115 errorObj.retryAfter = retryAfter;
116 // and notify observers of the retry interval
117 if (this.observerPrefix) {
118 Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter);
126 * Update clock offset by determining difference from date gives in the (RFC
127 * 1123) Date header of a server response. Because HAWK tolerates a window
128 * of one minute of clock skew (so two minutes total since the skew can be
129 * positive or negative), the simple method of calculating offset here is
130 * probably good enough. We keep the value in milliseconds to make life
131 * easier, even though the value will not have millisecond accuracy.
134 * An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT")
136 * For HAWK clock skew and replay protection, see
137 * https://github.com/hueniverse/hawk#replay-protection
139 _updateClockOffset: function(dateString) {
141 let serverDateMsec = Date.parse(dateString);
142 this._localtimeOffsetMsec = serverDateMsec - this.now();
143 log.debug("Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec);
145 log.warn("Bad date header in server response: " + dateString);
150 * Get the current clock offset in milliseconds.
152 * The offset is the number of milliseconds that must be added to the client
153 * clock to make it equal to the server clock. For example, if the client is
154 * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
156 get localtimeOffsetMsec() {
157 return this._localtimeOffsetMsec;
161 * return current time in milliseconds
167 /* A general method for sending raw RESTRequest calls authorized using HAWK
172 * The HTTP request method
176 * An object that can be encodable as JSON as the payload of the
179 * Returns a promise that resolves to the text response of the API call,
180 * or is rejected with an error. If the server response can be parsed
181 * as JSON and contains an 'error' property, the promise will be
182 * rejected with this JSON-parsed response.
184 request: function(path, method, credentials=null, payloadObj={}, retryOK=true) {
185 method = method.toLowerCase();
187 let deferred = Promise.defer();
188 let uri = this.host + path;
191 function _onComplete(error) {
192 let restResponse = this.response;
193 let status = restResponse.status;
195 log.debug("(Response) " + path + ": code: " + status +
196 " - Status text: " + restResponse.statusText);
198 log.debug("Response text: " + restResponse.body);
201 // All responses may have backoff headers, which are a server-side safety
202 // valve to allow slowing down clients without hurting performance.
203 self._maybeNotifyBackoff(restResponse, "x-weave-backoff");
204 self._maybeNotifyBackoff(restResponse, "x-backoff");
207 // When things really blow up, reconstruct an error object that follows
208 // the general format of the server on error responses.
209 return deferred.reject(self._constructError(restResponse, error));
212 self._updateClockOffset(restResponse.headers["date"]);
214 if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) {
215 // Retry once if we were rejected due to a bad timestamp.
216 // Clock offset is adjusted already in the top of this function.
217 log.debug("Received 401 for " + path + ": retrying");
218 return deferred.resolve(
219 self.request(path, method, credentials, payloadObj, false));
222 // If the server returned a json error message, use it in the rejection
225 // In the case of a 401, in which we are probably being rejected for a
226 // bad timestamp, retry exactly once, during which time clock offset will
229 let jsonResponse = {};
231 jsonResponse = JSON.parse(restResponse.body);
234 let okResponse = (200 <= status && status < 300);
235 if (!okResponse || jsonResponse.error) {
236 if (jsonResponse.error) {
237 return deferred.reject(jsonResponse);
239 return deferred.reject(self._constructError(restResponse, "Request failed"));
241 // It's up to the caller to know how to decode the response.
242 // We just return the raw text.
243 deferred.resolve(this.response.body);
246 function onComplete(error) {
248 // |this| is the RESTRequest object and we need to ensure _onComplete
249 // gets the same one.
250 _onComplete.call(this, error);
252 log.error("Unhandled exception processing response:" +
253 CommonUtils.exceptionStr(ex));
260 localtimeOffsetMsec: this.localtimeOffsetMsec,
263 let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
264 if (method == "post" || method == "put") {
265 request[method](payloadObj, onComplete);
267 request[method](onComplete);
270 return deferred.promise;
274 * The prefix used for all notifications sent by this module. This
275 * allows the handler of notifications to be sure they are handling
276 * notifications for the service they expect.
278 * If not set, no notifications will be sent.
280 observerPrefix: null,
282 // Given an optional header value, notify that a backoff has been requested.
283 _maybeNotifyBackoff: function (response, headerName) {
284 if (!this.observerPrefix || !response.headers) {
287 let headerVal = response.headers[headerName];
293 backoffInterval = parseInt(headerVal, 10);
295 log.error("hawkclient response had invalid backoff value in '" +
296 headerName + "' header: " + headerVal);
299 Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval);
302 // override points for testing.
303 newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) {
304 return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);