mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 957863 - Use FxA auth clock skew in hawk, jwcrypto, and sync. r=warner, r=rnewman
This commit is contained in:
parent
80b665fc66
commit
556c2f110c
@ -3,6 +3,7 @@
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
modules := \
|
||||
hawk.js \
|
||||
storageservice.js \
|
||||
stringbundle.js \
|
||||
tokenserverclient.js \
|
||||
|
201
services/common/hawk.js
Normal file
201
services/common/hawk.js
Normal file
@ -0,0 +1,201 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
* HAWK is an HTTP authentication scheme using a message authentication code
|
||||
* (MAC) algorithm to provide partial HTTP request cryptographic verification.
|
||||
*
|
||||
* For details, see: https://github.com/hueniverse/hawk
|
||||
*
|
||||
* With HAWK, it is essential that the clocks on clients and server not have an
|
||||
* absolute delta of greater than one minute, as the HAWK protocol uses
|
||||
* timestamps to reduce the possibility of replay attacks. However, it is
|
||||
* likely that some clients' clocks will be more than a little off, especially
|
||||
* in mobile devices, which would break HAWK-based services (like sync and
|
||||
* firefox accounts) for those clients.
|
||||
*
|
||||
* This library provides a stateful HAWK client that calculates (roughly) the
|
||||
* clock delta on the client vs the server. The library provides an interface
|
||||
* for deriving HAWK credentials and making HAWK-authenticated REST requests to
|
||||
* a single remote server. Therefore, callers who want to interact with
|
||||
* multiple HAWK services should instantiate one HawkClient per service.
|
||||
*/
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["HawkClient"];
|
||||
|
||||
const {interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
/*
|
||||
* A general purpose client for making HAWK authenticated requests to a single
|
||||
* host. Keeps track of the clock offset between the client and the host for
|
||||
* computation of the timestamp in the HAWK Authorization header.
|
||||
*
|
||||
* Clients should create one HawkClient object per each server they wish to
|
||||
* interact with.
|
||||
*
|
||||
* @param host
|
||||
* The url of the host
|
||||
*/
|
||||
function HawkClient(host) {
|
||||
this.host = host;
|
||||
|
||||
// Clock offset in milliseconds between our client's clock and the date
|
||||
// reported in responses from our host.
|
||||
this._localtimeOffsetMsec = 0;
|
||||
}
|
||||
|
||||
HawkClient.prototype = {
|
||||
|
||||
/*
|
||||
* Construct an error message for a response. Private.
|
||||
*
|
||||
* @param restResponse
|
||||
* A RESTResponse object from a RESTRequest
|
||||
*
|
||||
* @param errorString
|
||||
* A string describing the error
|
||||
*/
|
||||
_constructError: function(restResponse, errorString) {
|
||||
return {
|
||||
error: errorString,
|
||||
message: restResponse.statusText,
|
||||
code: restResponse.status,
|
||||
errno: restResponse.status
|
||||
};
|
||||
},
|
||||
|
||||
/*
|
||||
*
|
||||
* Update clock offset by determining difference from date gives in the (RFC
|
||||
* 1123) Date header of a server response. Because HAWK tolerates a window
|
||||
* of one minute of clock skew (so two minutes total since the skew can be
|
||||
* positive or negative), the simple method of calculating offset here is
|
||||
* probably good enough. We keep the value in milliseconds to make life
|
||||
* easier, even though the value will not have millisecond accuracy.
|
||||
*
|
||||
* @param dateString
|
||||
* An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT")
|
||||
*
|
||||
* For HAWK clock skew and replay protection, see
|
||||
* https://github.com/hueniverse/hawk#replay-protection
|
||||
*/
|
||||
_updateClockOffset: function(dateString) {
|
||||
try {
|
||||
let serverDateMsec = Date.parse(dateString);
|
||||
this._localtimeOffsetMsec = serverDateMsec - this.now();
|
||||
log.debug("Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec);
|
||||
} catch(err) {
|
||||
log.warn("Bad date header in server response: " + dateString);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Get the current clock offset in milliseconds.
|
||||
*
|
||||
* The offset is the number of milliseconds that must be added to the client
|
||||
* clock to make it equal to the server clock. For example, if the client is
|
||||
* five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
|
||||
*/
|
||||
get localtimeOffsetMsec() {
|
||||
return this._localtimeOffsetMsec;
|
||||
},
|
||||
|
||||
/*
|
||||
* return current time in milliseconds
|
||||
*/
|
||||
now: function() {
|
||||
return Date.now();
|
||||
},
|
||||
|
||||
/* A general method for sending raw RESTRequest calls authorized using HAWK
|
||||
*
|
||||
* @param path
|
||||
* API endpoint path
|
||||
* @param method
|
||||
* The HTTP request method
|
||||
* @param credentials
|
||||
* Hawk credentials
|
||||
* @param payloadObj
|
||||
* An object that can be encodable as JSON as the payload of the
|
||||
* request
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to the text response of the API call,
|
||||
* or is rejected with an error. If the server response can be parsed
|
||||
* as JSON and contains an 'error' property, the promise will be
|
||||
* rejected with this JSON-parsed response.
|
||||
*/
|
||||
request: function(path, method, credentials=null, payloadObj={}, retryOK=true) {
|
||||
method = method.toLowerCase();
|
||||
|
||||
let deferred = Promise.defer();
|
||||
let uri = this.host + path;
|
||||
let self = this;
|
||||
|
||||
function onComplete(error) {
|
||||
let restResponse = this.response;
|
||||
let status = restResponse.status;
|
||||
|
||||
log.debug("(Response) code: " + status +
|
||||
" - Status text: " + restResponse.statusText,
|
||||
" - Response text: " + restResponse.body);
|
||||
|
||||
if (error) {
|
||||
// When things really blow up, reconstruct an error object that follows
|
||||
// the general format of the server on error responses.
|
||||
return deferred.reject(self._constructError(restResponse, error));
|
||||
}
|
||||
|
||||
self._updateClockOffset(restResponse.headers["date"]);
|
||||
|
||||
if (status === 401 && retryOK) {
|
||||
// Retry once if we were rejected due to a bad timestamp.
|
||||
// Clock offset is adjusted already in the top of this function.
|
||||
log.debug("Received 401 for " + path + ": retrying");
|
||||
return deferred.resolve(
|
||||
self.request(path, method, credentials, payloadObj, false));
|
||||
}
|
||||
|
||||
// If the server returned a json error message, use it in the rejection
|
||||
// of the promise.
|
||||
//
|
||||
// In the case of a 401, in which we are probably being rejected for a
|
||||
// bad timestamp, retry exactly once, during which time clock offset will
|
||||
// be adjusted.
|
||||
|
||||
let jsonResponse = {};
|
||||
try {
|
||||
jsonResponse = JSON.parse(restResponse.body);
|
||||
} catch(notJSON) {}
|
||||
|
||||
let okResponse = (200 <= status && status < 300);
|
||||
if (!okResponse || jsonResponse.error) {
|
||||
if (jsonResponse.error) {
|
||||
return deferred.reject(jsonResponse);
|
||||
}
|
||||
return deferred.reject(self._constructError(restResponse, "Request failed"));
|
||||
}
|
||||
// It's up to the caller to know how to decode the response.
|
||||
// We just return the raw text.
|
||||
deferred.resolve(this.response.body);
|
||||
};
|
||||
|
||||
let extra = {
|
||||
now: this.now(),
|
||||
localtimeOffsetMsec: this.localtimeOffsetMsec,
|
||||
};
|
||||
|
||||
let request = new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
|
||||
request[method](payloadObj, onComplete);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
}
|
@ -9,7 +9,8 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"RESTRequest",
|
||||
"RESTResponse",
|
||||
"TokenAuthenticatedRESTRequest"
|
||||
"TokenAuthenticatedRESTRequest",
|
||||
"HAWKAuthenticatedRESTRequest",
|
||||
];
|
||||
|
||||
#endif
|
||||
@ -146,6 +147,11 @@ RESTRequest.prototype = {
|
||||
COMPLETED: 4,
|
||||
ABORTED: 8,
|
||||
|
||||
/**
|
||||
* HTTP status text of response
|
||||
*/
|
||||
statusText: null,
|
||||
|
||||
/**
|
||||
* Request timeout (in seconds, though decimal values can be used for
|
||||
* up to millisecond granularity.)
|
||||
@ -612,8 +618,7 @@ RESTResponse.prototype = {
|
||||
get status() {
|
||||
let status;
|
||||
try {
|
||||
let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
|
||||
status = channel.responseStatus;
|
||||
status = this.request.channel.responseStatus;
|
||||
} catch (ex) {
|
||||
this._log.debug("Caught exception fetching HTTP status code:" +
|
||||
CommonUtils.exceptionStr(ex));
|
||||
@ -623,14 +628,29 @@ RESTResponse.prototype = {
|
||||
return this.status = status;
|
||||
},
|
||||
|
||||
/**
|
||||
* HTTP status text
|
||||
*/
|
||||
get statusText() {
|
||||
let statusText;
|
||||
try {
|
||||
statusText = this.request.channel.responseStatusText;
|
||||
} catch (ex) {
|
||||
this._log.debug("Caught exception fetching HTTP status text:" +
|
||||
CommonUtils.exceptionStr(ex));
|
||||
return null;
|
||||
}
|
||||
delete this.statusText;
|
||||
return this.statusText = statusText;
|
||||
},
|
||||
|
||||
/**
|
||||
* Boolean flag that indicates whether the HTTP status code is 2xx or not.
|
||||
*/
|
||||
get success() {
|
||||
let success;
|
||||
try {
|
||||
let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
|
||||
success = channel.requestSucceeded;
|
||||
success = this.request.channel.requestSucceeded;
|
||||
} catch (ex) {
|
||||
this._log.debug("Caught exception fetching HTTP success flag:" +
|
||||
CommonUtils.exceptionStr(ex));
|
||||
@ -704,3 +724,60 @@ TokenAuthenticatedRESTRequest.prototype = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Single-use HAWK-authenticated HTTP requests to RESTish resources.
|
||||
*
|
||||
* @param uri
|
||||
* (String) URI for the RESTRequest constructor
|
||||
*
|
||||
* @param credentials
|
||||
* (Object) Optional credentials for computing HAWK authentication
|
||||
* header.
|
||||
*
|
||||
* @param payloadObj
|
||||
* (Object) Optional object to be converted to JSON payload
|
||||
*
|
||||
* @param extra
|
||||
* (Object) Optional extra params for HAWK header computation.
|
||||
* Valid properties are:
|
||||
*
|
||||
* now: <current time in milliseconds>,
|
||||
* localtimeOffsetMsec: <local clock offset vs server>
|
||||
*
|
||||
* extra.localtimeOffsetMsec is the value in milliseconds that must be added to
|
||||
* the local clock to make it agree with the server's clock. For instance, if
|
||||
* the local clock is two minutes ahead of the server, the time offset in
|
||||
* milliseconds will be -120000.
|
||||
*/
|
||||
this.HAWKAuthenticatedRESTRequest =
|
||||
function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) {
|
||||
RESTRequest.call(this, uri);
|
||||
|
||||
this.credentials = credentials;
|
||||
this.now = extra.now || Date.now();
|
||||
this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0;
|
||||
this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec));
|
||||
};
|
||||
HAWKAuthenticatedRESTRequest.prototype = {
|
||||
__proto__: RESTRequest.prototype,
|
||||
|
||||
dispatch: function dispatch(method, data, onComplete, onProgress) {
|
||||
if (this.credentials) {
|
||||
let options = {
|
||||
now: this.now,
|
||||
localtimeOffsetMsec: this.localtimeOffsetMsec,
|
||||
credentials: this.credentials,
|
||||
payload: data && JSON.stringify(data) || "",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
};
|
||||
let header = CryptoUtils.computeHAWK(this.uri, method, options);
|
||||
this.setHeader("Authorization", header.field);
|
||||
this._log.trace("hawk auth header: " + header.field);
|
||||
}
|
||||
|
||||
return RESTRequest.prototype.dispatch.call(
|
||||
this, method, data, onComplete, onProgress
|
||||
);
|
||||
}
|
||||
};
|
||||
|
485
services/common/tests/unit/test_hawk.js
Normal file
485
services/common/tests/unit/test_hawk.js
Normal file
@ -0,0 +1,485 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/hawk.js");
|
||||
|
||||
const SECOND_MS = 1000;
|
||||
const MINUTE_MS = SECOND_MS * 60;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
|
||||
const TEST_CREDS = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
|
||||
initTestLogging("Trace");
|
||||
|
||||
add_task(function test_now() {
|
||||
let client = new HawkClient("https://example.com");
|
||||
|
||||
do_check_true(client.now() - Date.now() < SECOND_MS);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_updateClockOffset() {
|
||||
let client = new HawkClient("https://example.com");
|
||||
|
||||
let now = new Date();
|
||||
let serverDate = now.toUTCString();
|
||||
|
||||
// Client's clock is off
|
||||
client.now = () => { return now.valueOf() + HOUR_MS; }
|
||||
|
||||
client._updateClockOffset(serverDate);
|
||||
|
||||
// Check that they're close; there will likely be a one-second rounding
|
||||
// error, so checking strict equality will likely fail.
|
||||
//
|
||||
// localtimeOffsetMsec is how many milliseconds to add to the local clock so
|
||||
// that it agrees with the server. We are one hour ahead of the server, so
|
||||
// our offset should be -1 hour.
|
||||
do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_authenticated_get_request() {
|
||||
let message = "{\"msg\": \"Great Success!\"}";
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({"/foo": (request, response) => {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
|
||||
let response = yield client.request("/foo", method, TEST_CREDS);
|
||||
let result = JSON.parse(response);
|
||||
|
||||
do_check_eq("Great Success!", result.msg);
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_authenticated_post_request() {
|
||||
let method = "POST";
|
||||
|
||||
let server = httpd_setup({"/foo": (request, response) => {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available());
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
|
||||
let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"});
|
||||
let result = JSON.parse(response);
|
||||
|
||||
do_check_eq("bar", result.foo);
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_credentials_optional() {
|
||||
let method = "GET";
|
||||
let server = httpd_setup({
|
||||
"/foo": (request, response) => {
|
||||
do_check_false(request.hasHeader("Authorization"));
|
||||
|
||||
let message = JSON.stringify({msg: "you're in the friend zone"});
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
let result = yield client.request("/foo", method); // credentials undefined
|
||||
do_check_eq(JSON.parse(result).msg, "you're in the friend zone");
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_server_error() {
|
||||
let message = "Ohai!";
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({"/foo": (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 418, "I am a Teapot");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
|
||||
try {
|
||||
yield client.request("/foo", method, TEST_CREDS);
|
||||
} catch(err) {
|
||||
do_check_eq(418, err.code);
|
||||
do_check_eq("I am a Teapot", err.message);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_server_error_json() {
|
||||
let message = JSON.stringify({error: "Cannot get ye flask."});
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({"/foo": (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 400, "What wouldst thou deau?");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
|
||||
try {
|
||||
yield client.request("/foo", method, TEST_CREDS);
|
||||
} catch(err) {
|
||||
do_check_eq("Cannot get ye flask.", err.error);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_offset_after_request() {
|
||||
let message = "Ohai!";
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({"/foo": (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
let now = Date.now();
|
||||
client.now = () => { return now + HOUR_MS; };
|
||||
|
||||
do_check_eq(client.localtimeOffsetMsec, 0);
|
||||
|
||||
let response = yield client.request("/foo", method, TEST_CREDS);
|
||||
// Should be about an hour off
|
||||
do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS);
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_offset_in_hawk_header() {
|
||||
let message = "Ohai!";
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({
|
||||
"/first": function(request, response) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
},
|
||||
|
||||
"/second": function(request, response) {
|
||||
// We see a better date now in the ts component of the header
|
||||
let delta = getTimestampDelta(request.getHeader("Authorization"));
|
||||
let message = "Delta: " + delta;
|
||||
|
||||
// We're now within HAWK's one-minute window.
|
||||
// I hope this isn't a recipe for intermittent oranges ...
|
||||
if (delta < MINUTE_MS) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
} else {
|
||||
response.setStatusLine(request.httpVersion, 400, "Delta: " + delta);
|
||||
}
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
function getOffset() {
|
||||
return client.localtimeOffsetMsec;
|
||||
}
|
||||
|
||||
client.now = () => {
|
||||
return Date.now() + 12 * HOUR_MS;
|
||||
};
|
||||
|
||||
// We begin with no offset
|
||||
do_check_eq(client.localtimeOffsetMsec, 0);
|
||||
yield client.request("/first", method, TEST_CREDS);
|
||||
|
||||
// After the first server response, our offset is updated to -12 hours.
|
||||
// We should be safely in the window, now.
|
||||
do_check_true(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS);
|
||||
yield client.request("/second", method, TEST_CREDS);
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_2xx_success() {
|
||||
// Just to ensure that we're not biased toward 200 OK for success
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({"/foo": (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 202, "Accepted");
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
|
||||
let response = yield client.request("/foo", method, credentials);
|
||||
|
||||
// Shouldn't be any content in a 202
|
||||
do_check_eq(response, "");
|
||||
|
||||
yield deferredStop(server);
|
||||
|
||||
});
|
||||
|
||||
add_task(function test_retry_request_on_fail() {
|
||||
let attempts = 0;
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({
|
||||
"/maybe": function(request, response) {
|
||||
// This path should be hit exactly twice; once with a bad timestamp, and
|
||||
// again when the client retries the request with a corrected timestamp.
|
||||
attempts += 1;
|
||||
do_check_true(attempts <= 2);
|
||||
|
||||
let delta = getTimestampDelta(request.getHeader("Authorization"));
|
||||
|
||||
// First time through, we should have a bad timestamp
|
||||
if (attempts === 1) {
|
||||
do_check_true(delta > MINUTE_MS);
|
||||
let message = "never!!!";
|
||||
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
|
||||
return response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
|
||||
// Second time through, timestamp should be corrected by client
|
||||
do_check_true(delta < MINUTE_MS);
|
||||
let message = "i love you!!!";
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
function getOffset() {
|
||||
return client.localtimeOffsetMsec;
|
||||
}
|
||||
|
||||
client.now = () => {
|
||||
return Date.now() + 12 * HOUR_MS;
|
||||
};
|
||||
|
||||
// We begin with no offset
|
||||
do_check_eq(client.localtimeOffsetMsec, 0);
|
||||
|
||||
// Request will have bad timestamp; client will retry once
|
||||
let response = yield client.request("/maybe", method, credentials);
|
||||
do_check_eq(response, "i love you!!!");
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_multiple_401_retry_once() {
|
||||
// Like test_retry_request_on_fail, but always return a 401
|
||||
// and ensure that the client only retries once.
|
||||
let attempts = 0;
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({
|
||||
"/maybe": function(request, response) {
|
||||
// This path should be hit exactly twice; once with a bad timestamp, and
|
||||
// again when the client retries the request with a corrected timestamp.
|
||||
attempts += 1;
|
||||
|
||||
do_check_true(attempts <= 2);
|
||||
|
||||
let message = "never!!!";
|
||||
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
function getOffset() {
|
||||
return client.localtimeOffsetMsec;
|
||||
}
|
||||
|
||||
client.now = () => {
|
||||
return Date.now() - 12 * HOUR_MS;
|
||||
};
|
||||
|
||||
// We begin with no offset
|
||||
do_check_eq(client.localtimeOffsetMsec, 0);
|
||||
|
||||
// Request will have bad timestamp; client will retry once
|
||||
try {
|
||||
yield client.request("/maybe", method, credentials);
|
||||
} catch (err) {
|
||||
do_check_eq(err.code, 401);
|
||||
}
|
||||
do_check_eq(attempts, 2);
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_500_no_retry() {
|
||||
// If we get a 500 error, the client should not retry (as it would with a
|
||||
// 401)
|
||||
let attempts = 0;
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({
|
||||
"/no-shutup": function() {
|
||||
attempts += 1;
|
||||
let message = "Cannot get ye flask.";
|
||||
response.setStatusLine(request.httpVersion, 500, "Internal server error");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
function getOffset() {
|
||||
return client.localtimeOffsetMsec;
|
||||
}
|
||||
|
||||
// Throw off the clock so the HawkClient would want to retry the request if
|
||||
// it could
|
||||
client.now = () => {
|
||||
return Date.now() - 12 * HOUR_MS;
|
||||
};
|
||||
|
||||
// Request will 500; no retries
|
||||
try {
|
||||
yield client.request("/no-shutup", method, credentials);
|
||||
} catch(err) {
|
||||
do_check_eq(err.code, 500);
|
||||
}
|
||||
do_check_eq(attempts, 1);
|
||||
|
||||
yield deferredStop(server);
|
||||
|
||||
});
|
||||
|
||||
add_task(function test_401_then_500() {
|
||||
// Like test_multiple_401_retry_once, but return a 500 to the
|
||||
// second request, ensuring that the promise is properly rejected
|
||||
// in client.request.
|
||||
let attempts = 0;
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
let method = "GET";
|
||||
|
||||
let server = httpd_setup({
|
||||
"/maybe": function(request, response) {
|
||||
// This path should be hit exactly twice; once with a bad timestamp, and
|
||||
// again when the client retries the request with a corrected timestamp.
|
||||
attempts += 1;
|
||||
do_check_true(attempts <= 2);
|
||||
|
||||
let delta = getTimestampDelta(request.getHeader("Authorization"));
|
||||
|
||||
// First time through, we should have a bad timestamp
|
||||
// Client will retry
|
||||
if (attempts === 1) {
|
||||
do_check_true(delta > MINUTE_MS);
|
||||
let message = "never!!!";
|
||||
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
|
||||
return response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
|
||||
// Second time through, timestamp should be corrected by client
|
||||
// And fail on the client
|
||||
do_check_true(delta < MINUTE_MS);
|
||||
let message = "Cannot get ye flask.";
|
||||
response.setStatusLine(request.httpVersion, 500, "Internal server error");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new HawkClient(server.baseURI);
|
||||
function getOffset() {
|
||||
return client.localtimeOffsetMsec;
|
||||
}
|
||||
|
||||
client.now = () => {
|
||||
return Date.now() - 12 * HOUR_MS;
|
||||
};
|
||||
|
||||
// We begin with no offset
|
||||
do_check_eq(client.localtimeOffsetMsec, 0);
|
||||
|
||||
// Request will have bad timestamp; client will retry once
|
||||
try {
|
||||
yield client.request("/maybe", method, credentials);
|
||||
} catch(err) {
|
||||
do_check_eq(err.code, 500);
|
||||
}
|
||||
do_check_eq(attempts, 2);
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function throw_if_not_json_body() {
|
||||
do_test_pending();
|
||||
let client = new HawkClient("https://example.com");
|
||||
try {
|
||||
yield client.request("/bogus", "GET", {}, "I am not json");
|
||||
} catch(err) {
|
||||
do_check_true(!!err.message);
|
||||
do_test_finished();
|
||||
}
|
||||
});
|
||||
|
||||
// End of tests.
|
||||
// Utility functions follow
|
||||
|
||||
function getTimestampDelta(authHeader, now=Date.now()) {
|
||||
let tsMS = new Date(
|
||||
parseInt(/ts="(\d+)"/.exec(authHeader)[1], 10) * SECOND_MS);
|
||||
return Math.abs(tsMS - now);
|
||||
}
|
||||
|
||||
function deferredStop(server) {
|
||||
let deferred = Promise.defer();
|
||||
server.stop(deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
run_next_test();
|
||||
}
|
||||
|
@ -831,3 +831,62 @@ add_test(function test_not_sending_cookie() {
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_hawk_authenticated_request() {
|
||||
do_test_pending();
|
||||
|
||||
let onProgressCalled = false;
|
||||
let postData = {your: "data"};
|
||||
|
||||
// An arbitrary date - Feb 2, 1971. It ends in a bunch of zeroes to make our
|
||||
// computation with the hawk timestamp easier, since hawk throws away the
|
||||
// millisecond values.
|
||||
let then = 34329600000;
|
||||
|
||||
let clockSkew = 120000;
|
||||
let timeOffset = -1 * clockSkew;
|
||||
let localTime = then + clockSkew;
|
||||
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
|
||||
algorithm: "sha256"
|
||||
};
|
||||
|
||||
let server = httpd_setup({
|
||||
"/elysium": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
// check that the header timestamp is our arbitrary system date, not
|
||||
// today's date. Note that hawk header timestamps are in seconds, not
|
||||
// milliseconds.
|
||||
let authorization = request.getHeader("Authorization");
|
||||
let tsMS = parseInt(/ts="(\d+)"/.exec(authorization)[1], 10) * 1000;
|
||||
do_check_eq(tsMS, then);
|
||||
|
||||
let message = "yay";
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(message, message.length);
|
||||
}
|
||||
});
|
||||
|
||||
function onProgress() {
|
||||
onProgressCalled = true;
|
||||
}
|
||||
|
||||
function onComplete(error) {
|
||||
do_check_eq(200, this.response.status);
|
||||
do_check_eq(this.response.body, "yay");
|
||||
do_check_true(onProgressCalled);
|
||||
do_test_finished();
|
||||
server.stop(run_next_test);
|
||||
}
|
||||
|
||||
let url = server.baseURI + "/elysium";
|
||||
let extra = {
|
||||
now: localTime,
|
||||
localtimeOffsetMsec: timeOffset
|
||||
};
|
||||
let request = new HAWKAuthenticatedRESTRequest(url, credentials, extra);
|
||||
request.post(postData, onComplete, onProgress);
|
||||
});
|
||||
|
@ -25,6 +25,7 @@ firefox-appdir = browser
|
||||
[test_async_querySpinningly.js]
|
||||
[test_bagheera_server.js]
|
||||
[test_bagheera_client.js]
|
||||
[test_hawk.js]
|
||||
[test_observers.js]
|
||||
[test_restrequest.js]
|
||||
[test_tokenauthenticatedrequest.js]
|
||||
|
@ -67,6 +67,26 @@ InternalMethods = function(mock) {
|
||||
}
|
||||
InternalMethods.prototype = {
|
||||
|
||||
/**
|
||||
* Return the current time in milliseconds as an integer. Allows tests to
|
||||
* manipulate the date to simulate certificate expiration.
|
||||
*/
|
||||
now: function() {
|
||||
return this.fxAccountsClient.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return clock offset in milliseconds, as reported by the fxAccountsClient.
|
||||
* This can be overridden for testing.
|
||||
*
|
||||
* The offset is the number of milliseconds that must be added to the client
|
||||
* clock to make it equal to the server clock. For example, if the client is
|
||||
* five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
|
||||
*/
|
||||
get localtimeOffsetMsec() {
|
||||
return this.fxAccountsClient.localtimeOffsetMsec;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ask the server whether the user's email has been verified
|
||||
*/
|
||||
@ -206,9 +226,13 @@ InternalMethods.prototype = {
|
||||
log.debug("getAssertionFromCert");
|
||||
let payload = {};
|
||||
let d = Promise.defer();
|
||||
let options = {
|
||||
localtimeOffsetMsec: internal.localtimeOffsetMsec,
|
||||
now: internal.now()
|
||||
};
|
||||
// "audience" should look like "http://123done.org".
|
||||
// The generated assertion will expire in two minutes.
|
||||
jwcrypto.generateAssertion(cert, keyPair, audience, function(err, signed) {
|
||||
jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => {
|
||||
if (err) {
|
||||
log.error("getAssertionFromCert: " + err);
|
||||
d.reject(err);
|
||||
@ -228,7 +252,7 @@ InternalMethods.prototype = {
|
||||
return Promise.resolve(this.cert.cert);
|
||||
}
|
||||
// else get our cert signed
|
||||
let willBeValidUntil = this.now() + CERT_LIFETIME;
|
||||
let willBeValidUntil = internal.now() + CERT_LIFETIME;
|
||||
return this.getCertificateSigned(data.sessionToken,
|
||||
keyPair.serializedPublicKey,
|
||||
CERT_LIFETIME)
|
||||
@ -255,7 +279,7 @@ InternalMethods.prototype = {
|
||||
return Promise.resolve(this.keyPair.keyPair);
|
||||
}
|
||||
// Otherwse, create a keypair and set validity limit.
|
||||
let willBeValidUntil = this.now() + KEY_LIFETIME;
|
||||
let willBeValidUntil = internal.now() + KEY_LIFETIME;
|
||||
let d = Promise.defer();
|
||||
jwcrypto.generateKeyPair("DS160", (err, kp) => {
|
||||
if (err) {
|
||||
|
@ -6,9 +6,11 @@ this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/hawk.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
|
||||
@ -19,11 +21,17 @@ try {
|
||||
} catch(keepDefault) {}
|
||||
|
||||
const HOST = _host;
|
||||
const PREFIX_NAME = "identity.mozilla.com/picl/v1/";
|
||||
|
||||
const XMLHttpRequest =
|
||||
Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
|
||||
const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/";
|
||||
|
||||
function KW(context) {
|
||||
// This is used as a salt. It's specified by the protocol. Note that the
|
||||
// value of PROTOCOL_VERSION does not refer in any wy to the version of the
|
||||
// Firefox Accounts API. For this reason, it is not exposed as a pref.
|
||||
//
|
||||
// See:
|
||||
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#creating-the-account
|
||||
return PROTOCOL_VERSION + context;
|
||||
}
|
||||
|
||||
function stringToHex(str) {
|
||||
let encoder = new TextEncoder("utf-8");
|
||||
@ -43,9 +51,37 @@ function bytesToHex(bytes) {
|
||||
|
||||
this.FxAccountsClient = function(host = HOST) {
|
||||
this.host = host;
|
||||
|
||||
// The FxA auth server expects requests to certain endpoints to be authorized
|
||||
// using Hawk.
|
||||
this.hawk = new HawkClient(host);
|
||||
};
|
||||
|
||||
this.FxAccountsClient.prototype = {
|
||||
|
||||
/**
|
||||
* Return client clock offset, in milliseconds, as determined by hawk client.
|
||||
* Provided because callers should not have to know about hawk
|
||||
* implementation.
|
||||
*
|
||||
* The offset is the number of milliseconds that must be added to the client
|
||||
* clock to make it equal to the server clock. For example, if the client is
|
||||
* five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
|
||||
*/
|
||||
get localtimeOffsetMsec() {
|
||||
return this.hawk.localtimeOffsetMsec;
|
||||
},
|
||||
|
||||
/*
|
||||
* Return current time in milliseconds
|
||||
*
|
||||
* Not used by this module, but made available to the FxAccounts.jsm
|
||||
* that uses this client.
|
||||
*/
|
||||
now: function() {
|
||||
return this.hawk.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new Firefox Account and authenticate
|
||||
*
|
||||
@ -149,7 +185,7 @@ this.FxAccountsClient.prototype = {
|
||||
let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
|
||||
let keyRequestKey = creds.extra.slice(0, 32);
|
||||
let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
|
||||
PREFIX_NAME + "account/keys", 3 * 32);
|
||||
KW("account/keys"), 3 * 32);
|
||||
let respHMACKey = morecreds.slice(0, 32);
|
||||
let respXORKey = morecreds.slice(32, 96);
|
||||
|
||||
@ -199,8 +235,10 @@ this.FxAccountsClient.prototype = {
|
||||
return Promise.resolve()
|
||||
.then(_ => this._request("/certificate/sign", "POST", creds, body))
|
||||
.then(resp => resp.cert,
|
||||
err => {dump("HAWK.signCertificate error: " + JSON.stringify(err) + "\n");
|
||||
throw err;});
|
||||
err => {
|
||||
log.error("HAWK.signCertificate error: " + JSON.stringify(err));
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -219,8 +257,10 @@ this.FxAccountsClient.prototype = {
|
||||
// the account exists
|
||||
(result) => true,
|
||||
(err) => {
|
||||
log.error("accountExists: error: " + JSON.stringify(err));
|
||||
// the account doesn't exist
|
||||
if (err.errno === 102) {
|
||||
log.debug("returning false for errno 102");
|
||||
return false;
|
||||
}
|
||||
// propogate other request errors
|
||||
@ -251,7 +291,7 @@ this.FxAccountsClient.prototype = {
|
||||
*/
|
||||
_deriveHawkCredentials: function (tokenHex, context, size) {
|
||||
let token = CommonUtils.hexToBytes(tokenHex);
|
||||
let out = CryptoUtils.hkdf(token, undefined, PREFIX_NAME + context, size || 3 * 32);
|
||||
let out = CryptoUtils.hkdf(token, undefined, KW(context), size || 3 * 32);
|
||||
|
||||
return {
|
||||
algorithm: "sha256",
|
||||
@ -286,63 +326,21 @@ this.FxAccountsClient.prototype = {
|
||||
*/
|
||||
_request: function hawkRequest(path, method, credentials, jsonPayload) {
|
||||
let deferred = Promise.defer();
|
||||
let xhr = new XMLHttpRequest({mozSystem: true});
|
||||
let URI = this.host + path;
|
||||
let payload;
|
||||
|
||||
xhr.mozBackgroundRequest = true;
|
||||
|
||||
if (jsonPayload) {
|
||||
payload = JSON.stringify(jsonPayload);
|
||||
}
|
||||
|
||||
log.debug("(HAWK request) - Path: " + path + " - Method: " + method +
|
||||
" - Payload: " + payload);
|
||||
|
||||
xhr.open(method, URI);
|
||||
xhr.channel.loadFlags = Ci.nsIChannel.LOAD_BYPASS_CACHE |
|
||||
Ci.nsIChannel.INHIBIT_CACHING;
|
||||
|
||||
// When things really blow up, reconstruct an error object that follows the general format
|
||||
// of the server on error responses.
|
||||
function constructError(err) {
|
||||
return { error: err, message: xhr.statusText, code: xhr.status, errno: xhr.status };
|
||||
}
|
||||
|
||||
xhr.onerror = function() {
|
||||
deferred.reject(constructError('Request failed'));
|
||||
};
|
||||
|
||||
xhr.onload = function onload() {
|
||||
this.hawk.request(path, method, credentials, jsonPayload).then(
|
||||
(responseText) => {
|
||||
try {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
log.debug("(Response) Code: " + xhr.status + " - Status text: " +
|
||||
xhr.statusText + " - Response text: " + xhr.responseText);
|
||||
if (xhr.status !== 200 || response.error) {
|
||||
// In this case, the response is an object with error information.
|
||||
return deferred.reject(response);
|
||||
}
|
||||
let response = JSON.parse(responseText);
|
||||
deferred.resolve(response);
|
||||
} catch (e) {
|
||||
log.error("(Response) Code: " + xhr.status + " - Status text: " +
|
||||
xhr.statusText);
|
||||
deferred.reject(constructError(e));
|
||||
} catch (err) {
|
||||
deferred.reject({error: err});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
let uri = Services.io.newURI(URI, null, null);
|
||||
|
||||
if (credentials) {
|
||||
let header = CryptoUtils.computeHAWK(uri, method, {
|
||||
credentials: credentials,
|
||||
payload: payload,
|
||||
contentType: "application/json"
|
||||
});
|
||||
xhr.setRequestHeader("authorization", header.field);
|
||||
(error) => {
|
||||
deferred.reject(error);
|
||||
}
|
||||
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(payload);
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
@ -11,6 +11,12 @@ Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
const ONE_HOUR_MS = 1000 * 60 * 60;
|
||||
const ONE_DAY_MS = ONE_HOUR_MS * 24;
|
||||
const TWO_MINUTES_MS = 1000 * 60 * 2;
|
||||
|
||||
initTestLogging("Trace");
|
||||
|
||||
// XXX until bug 937114 is fixed
|
||||
Cu.importGlobalProperties(['atob']);
|
||||
|
||||
@ -383,15 +389,18 @@ add_task(function test_getAssertion() {
|
||||
yield fxa.setSignedInUser(creds);
|
||||
|
||||
_("== ready to go\n");
|
||||
let now = 138000000*1000;
|
||||
let start = Date.now();
|
||||
// Start with a nice arbitrary but realistic date. Here we use a nice RFC
|
||||
// 1123 date string like we would get from an HTTP header. Over the course of
|
||||
// the test, we will update 'now', but leave 'start' where it is.
|
||||
let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
|
||||
let start = now;
|
||||
fxa._now_is = now;
|
||||
|
||||
let d = fxa.getAssertion("audience.example.com");
|
||||
// At this point, a thread has been spawned to generate the keys.
|
||||
_("-- back from fxa.getAssertion\n");
|
||||
fxa._d_signCertificate.resolve("cert1");
|
||||
let assertion = yield d;
|
||||
let finish = Date.now();
|
||||
do_check_eq(fxa._getCertificateSigned_calls.length, 1);
|
||||
do_check_eq(fxa._getCertificateSigned_calls[0][0], "sessionToken");
|
||||
do_check_neq(assertion, null);
|
||||
@ -407,32 +416,56 @@ add_task(function test_getAssertion() {
|
||||
let payload = JSON.parse(atob(p2[1]));
|
||||
_("PAYLOAD: " + JSON.stringify(payload) + "\n");
|
||||
do_check_eq(payload.aud, "audience.example.com");
|
||||
// FxAccounts KEY_LIFETIME
|
||||
do_check_eq(fxa.internal.keyPair.validUntil, now + (12*3600*1000));
|
||||
// FxAccounts CERT_LIFETIME
|
||||
do_check_eq(fxa.internal.cert.validUntil, now + (6*3600*1000));
|
||||
_("delta: "+(new Date(payload.exp) - now)+"\n");
|
||||
do_check_eq(fxa.internal.keyPair.validUntil, start + KEY_LIFETIME);
|
||||
do_check_eq(fxa.internal.cert.validUntil, start + CERT_LIFETIME);
|
||||
_("delta: " + Date.parse(payload.exp - start) + "\n");
|
||||
let exp = Number(payload.exp);
|
||||
// jwcrypto.jsm uses an unmocked Date.now()+2min to decide on the
|
||||
// expiration time, so we test that it's inside a specific timebox
|
||||
do_check_true(start + 2*60*1000 <= exp);
|
||||
do_check_true(exp <= finish + 2*60*1000);
|
||||
|
||||
do_check_eq(exp, now + TWO_MINUTES_MS);
|
||||
|
||||
// Reset for next call.
|
||||
fxa._d_signCertificate = Promise.defer();
|
||||
|
||||
// Getting a new assertion "soon" (i.e. w/o incrementing "now"), even for
|
||||
// Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for
|
||||
// a new audience, should not provoke key generation or a signing request.
|
||||
assertion = yield fxa.getAssertion("other.example.com");
|
||||
|
||||
// There were no additional calls - same number of getcert calls as before
|
||||
do_check_eq(fxa._getCertificateSigned_calls.length, 1);
|
||||
|
||||
// But "waiting" (i.e. incrementing "now") will need a new key+signature.
|
||||
fxa._now_is = now + 24*3600*1000;
|
||||
start = Date.now();
|
||||
d = fxa.getAssertion("third.example.com");
|
||||
// Wait an hour; assertion expires, but not the certificate
|
||||
now += ONE_HOUR_MS;
|
||||
fxa._now_is = now;
|
||||
|
||||
// This won't block on anything - will make an assertion, but not get a
|
||||
// new certificate.
|
||||
assertion = yield fxa.getAssertion("third.example.com");
|
||||
|
||||
// Test will time out if that failed (i.e., if that had to go get a new cert)
|
||||
pieces = assertion.split("~");
|
||||
do_check_eq(pieces[0], "cert1");
|
||||
p2 = pieces[1].split(".");
|
||||
header = JSON.parse(atob(p2[0]));
|
||||
payload = JSON.parse(atob(p2[1]));
|
||||
do_check_eq(payload.aud, "third.example.com");
|
||||
|
||||
// The keypair and cert should have the same validity as before, but the
|
||||
// expiration time of the assertion should be different. We compare this to
|
||||
// the initial start time, to which they are relative, not the current value
|
||||
// of "now".
|
||||
|
||||
do_check_eq(fxa.internal.keyPair.validUntil, start + KEY_LIFETIME);
|
||||
do_check_eq(fxa.internal.cert.validUntil, start + CERT_LIFETIME);
|
||||
exp = Number(payload.exp);
|
||||
do_check_eq(exp, now + TWO_MINUTES_MS);
|
||||
|
||||
// Now we wait even longer, and expect both assertion and cert to expire. So
|
||||
// we will have to get a new keypair and cert.
|
||||
now += ONE_DAY_MS;
|
||||
fxa._now_is = now;
|
||||
d = fxa.getAssertion("fourth.example.com");
|
||||
fxa._d_signCertificate.resolve("cert2");
|
||||
assertion = yield d;
|
||||
finish = Date.now();
|
||||
do_check_eq(fxa._getCertificateSigned_calls.length, 2);
|
||||
do_check_eq(fxa._getCertificateSigned_calls[1][0], "sessionToken");
|
||||
pieces = assertion.split("~");
|
||||
@ -440,15 +473,12 @@ add_task(function test_getAssertion() {
|
||||
p2 = pieces[1].split(".");
|
||||
header = JSON.parse(atob(p2[0]));
|
||||
payload = JSON.parse(atob(p2[1]));
|
||||
do_check_eq(payload.aud, "third.example.com");
|
||||
// 12*3600*1000 === FxAccounts KEY_LIFETIME
|
||||
do_check_eq(fxa.internal.keyPair.validUntil, now + 24*3600*1000 + (12*3600*1000));
|
||||
// 6*3600*1000 === FxAccounts CERT_LIFETIME
|
||||
do_check_eq(fxa.internal.cert.validUntil, now + 24*3600*1000 + (6*3600*1000));
|
||||
do_check_eq(payload.aud, "fourth.example.com");
|
||||
do_check_eq(fxa.internal.keyPair.validUntil, now + KEY_LIFETIME);
|
||||
do_check_eq(fxa.internal.cert.validUntil, now + CERT_LIFETIME);
|
||||
exp = Number(payload.exp);
|
||||
do_check_true(start + 2*60*1000 <= exp);
|
||||
do_check_true(exp <= finish + 2*60*1000);
|
||||
|
||||
do_check_eq(exp, now + TWO_MINUTES_MS);
|
||||
_("----- DONE ----\n");
|
||||
});
|
||||
|
||||
|
@ -5,6 +5,8 @@ Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
const FAKE_SESSION_TOKEN = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
@ -28,7 +30,6 @@ add_test(function test_hawk_credentials() {
|
||||
});
|
||||
|
||||
add_task(function test_authenticated_get_request() {
|
||||
|
||||
let message = "{\"msg\": \"Great Success!\"}";
|
||||
let credentials = {
|
||||
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
|
||||
@ -79,7 +80,6 @@ add_task(function test_authenticated_post_request() {
|
||||
});
|
||||
|
||||
add_task(function test_500_error() {
|
||||
|
||||
let message = "<h1>Ooops!</h1>";
|
||||
let method = "GET";
|
||||
|
||||
@ -181,7 +181,7 @@ add_task(function test_api_endpoints() {
|
||||
result = yield client.signIn('mé@example.com', 'bigsecret');
|
||||
do_check_eq("NotARealToken", result.sessionToken);
|
||||
|
||||
result = yield client.signOut('NotARealToken');
|
||||
result = yield client.signOut(FAKE_SESSION_TOKEN);
|
||||
do_check_eq(typeof result, "object");
|
||||
|
||||
result = yield client.recoveryEmailStatus('NotARealToken');
|
||||
|
@ -20,6 +20,7 @@ Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://services-sync/constants.js");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-sync/stages/cluster.js");
|
||||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
|
||||
// Lazy imports to prevent unnecessary load on startup.
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle",
|
||||
@ -149,7 +150,7 @@ this.BrowserIDManager.prototype = {
|
||||
});
|
||||
// and we are done - the fetch continues on in the background...
|
||||
}).then(null, err => {
|
||||
dump("err in processing logged in account "+err.message);
|
||||
this._log.error("Processing logged in account: " + err.message);
|
||||
});
|
||||
},
|
||||
|
||||
@ -193,7 +194,11 @@ this.BrowserIDManager.prototype = {
|
||||
* Provide override point for testing token expiration.
|
||||
*/
|
||||
_now: function() {
|
||||
return Date.now();
|
||||
return this._fxaService.internal.now()
|
||||
},
|
||||
|
||||
get _localtimeOffsetMsec() {
|
||||
return this._fxaService.internal.localtimeOffsetMsec;
|
||||
},
|
||||
|
||||
get account() {
|
||||
@ -464,8 +469,16 @@ this.BrowserIDManager.prototype = {
|
||||
key: this._token.key,
|
||||
};
|
||||
method = method || httpObject.method;
|
||||
let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method,
|
||||
{credentials: credentials});
|
||||
|
||||
// Get the local clock offset from the Firefox Accounts server. This should
|
||||
// be close to the offset from the storage server.
|
||||
let options = {
|
||||
now: this._now(),
|
||||
localtimeOffsetMsec: this._localtimeOffsetMsec,
|
||||
credentials: credentials,
|
||||
};
|
||||
|
||||
let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method, options);
|
||||
return {headers: {authorization: headerValue.field}};
|
||||
},
|
||||
|
||||
|
@ -8,11 +8,48 @@ Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://testing-common/services/sync/utils.js");
|
||||
Cu.import("resource://services-common/hawk.js");
|
||||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
|
||||
const SECOND_MS = 1000;
|
||||
const MINUTE_MS = SECOND_MS * 60;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
|
||||
let identityConfig = makeIdentityConfig();
|
||||
let browseridManager = new BrowserIDManager();
|
||||
configureFxAccountIdentity(browseridManager, identityConfig);
|
||||
|
||||
/**
|
||||
* Mock client clock and skew vs server in FxAccounts signed-in user module and
|
||||
* API client. browserid_identity.js queries these values to construct HAWK
|
||||
* headers. We will use this to test clock skew compensation in these headers
|
||||
* below.
|
||||
*/
|
||||
let MockFxAccountsClient = function() {
|
||||
FxAccountsClient.apply(this);
|
||||
};
|
||||
MockFxAccountsClient.prototype = {
|
||||
__proto__: FxAccountsClient.prototype
|
||||
};
|
||||
|
||||
let MockFxAccounts = function() {
|
||||
this._now_is = Date.now();
|
||||
|
||||
let mockInternal = {
|
||||
now: () => {
|
||||
return this._now_is;
|
||||
},
|
||||
|
||||
fxAccountsClient: new MockFxAccountsClient()
|
||||
};
|
||||
|
||||
FxAccounts.apply(this, [mockInternal]);
|
||||
};
|
||||
MockFxAccounts.prototype = {
|
||||
__proto__: FxAccounts.prototype,
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace;
|
||||
@ -60,6 +97,111 @@ add_test(function test_getRESTRequestAuthenticator() {
|
||||
}
|
||||
);
|
||||
|
||||
add_test(function test_resourceAuthenticatorSkew() {
|
||||
_("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header.");
|
||||
|
||||
// Clock is skewed 12 hours into the future
|
||||
// We pick a date in the past so we don't risk concealing bugs in code that
|
||||
// uses new Date() instead of our given date.
|
||||
let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
|
||||
let browseridManager = new BrowserIDManager();
|
||||
let hawkClient = new HawkClient("https://example.net/v1", "/foo");
|
||||
|
||||
// mock fxa hawk client skew
|
||||
hawkClient.now = function() {
|
||||
dump("mocked client now: " + now + '\n');
|
||||
return now;
|
||||
}
|
||||
// Imagine there's already been one fxa request and the hawk client has
|
||||
// already detected skew vs the fxa auth server.
|
||||
let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
|
||||
hawkClient._localtimeOffsetMsec = localtimeOffsetMsec;
|
||||
|
||||
let fxaClient = new MockFxAccountsClient();
|
||||
fxaClient.hawk = hawkClient;
|
||||
|
||||
// Sanity check
|
||||
do_check_eq(hawkClient.now(), now);
|
||||
do_check_eq(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec);
|
||||
|
||||
// Properly picked up by the client
|
||||
do_check_eq(fxaClient.now(), now);
|
||||
do_check_eq(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec);
|
||||
|
||||
let fxa = new MockFxAccounts();
|
||||
fxa._now_is = now;
|
||||
fxa.internal.fxAccountsClient = fxaClient;
|
||||
|
||||
// Picked up by the signed-in user module
|
||||
do_check_eq(fxa.internal.now(), now);
|
||||
do_check_eq(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec);
|
||||
|
||||
// Mocks within mocks...
|
||||
configureFxAccountIdentity(browseridManager, identityConfig);
|
||||
browseridManager._fxaService = fxa;
|
||||
do_check_eq(browseridManager._fxaService.internal.now(), now);
|
||||
do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec,
|
||||
localtimeOffsetMsec);
|
||||
|
||||
let request = new SyncStorageRequest("https://example.net/i/like/pie/");
|
||||
let authenticator = browseridManager.getResourceAuthenticator();
|
||||
let output = authenticator(request, 'GET');
|
||||
dump("output" + JSON.stringify(output));
|
||||
let authHeader = output.headers.authorization;
|
||||
do_check_true(authHeader.startsWith('Hawk'));
|
||||
|
||||
// Skew correction is applied in the header and we're within the two-minute
|
||||
// window.
|
||||
do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS);
|
||||
do_check_true(
|
||||
(getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_RESTResourceAuthenticatorSkew() {
|
||||
_("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header.");
|
||||
|
||||
// Clock is skewed 12 hours into the future from our arbitary date
|
||||
let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
|
||||
let browseridManager = new BrowserIDManager();
|
||||
let hawkClient = new HawkClient("https://example.net/v1", "/foo");
|
||||
|
||||
// mock fxa hawk client skew
|
||||
hawkClient.now = function() {
|
||||
return now;
|
||||
}
|
||||
// Imagine there's already been one fxa request and the hawk client has
|
||||
// already detected skew vs the fxa auth server.
|
||||
hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS;
|
||||
|
||||
let fxaClient = new MockFxAccountsClient();
|
||||
fxaClient.hawk = hawkClient;
|
||||
let fxa = new MockFxAccounts();
|
||||
fxa._now_is = now;
|
||||
fxa.internal.fxAccountsClient = fxaClient;
|
||||
|
||||
configureFxAccountIdentity(browseridManager, identityConfig);
|
||||
browseridManager._fxaService = fxa;
|
||||
|
||||
do_check_eq(browseridManager._fxaService.internal.now(), now);
|
||||
|
||||
let request = new SyncStorageRequest("https://example.net/i/like/pie/");
|
||||
let authenticator = browseridManager.getResourceAuthenticator();
|
||||
let output = authenticator(request, 'GET');
|
||||
dump("output" + JSON.stringify(output));
|
||||
let authHeader = output.headers.authorization;
|
||||
do_check_true(authHeader.startsWith('Hawk'));
|
||||
|
||||
// Skew correction is applied in the header and we're within the two-minute
|
||||
// window.
|
||||
do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS);
|
||||
do_check_true(
|
||||
(getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_tokenExpiration() {
|
||||
_("BrowserIDManager notices token expiration:");
|
||||
let bimExp = new BrowserIDManager();
|
||||
@ -143,3 +285,14 @@ add_test(function test_computeXClientStateHeader() {
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// End of tests
|
||||
// Utility functions follow
|
||||
|
||||
function getTimestamp(hawkAuthHeader) {
|
||||
return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
|
||||
}
|
||||
|
||||
function getTimestampDelta(hawkAuthHeader, now=Date.now()) {
|
||||
return Math.abs(getTimestamp(hawkAuthHeader) - now);
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ XPCOMUtils.defineLazyServiceGetter(this,
|
||||
this.EXPORTED_SYMBOLS = ["jwcrypto"];
|
||||
|
||||
const ALGORITHMS = { RS256: "RS256", DS160: "DS160" };
|
||||
const DURATION_MS = 1000 * 60 * 2; // 2 minutes default assertion lifetime
|
||||
|
||||
function log(...aMessageArgs) {
|
||||
Logger.log.apply(Logger, ["jwcrypto"].concat(aMessageArgs));
|
||||
@ -87,6 +88,23 @@ function jwcryptoClass()
|
||||
}
|
||||
|
||||
jwcryptoClass.prototype = {
|
||||
/*
|
||||
* Determine the expiration of the assertion. Returns expiry date
|
||||
* in milliseconds as integer.
|
||||
*
|
||||
* @param localtimeOffsetMsec (optional)
|
||||
* The number of milliseconds that must be added to the local clock
|
||||
* for it to agree with the server. For example, if the local clock
|
||||
* if two minutes fast, localtimeOffsetMsec would be -120000
|
||||
*
|
||||
* @param now (options)
|
||||
* Current date in milliseconds. Useful for mocking clock
|
||||
* skew in testing.
|
||||
*/
|
||||
getExpiration: function(duration=DURATION_MS, localtimeOffsetMsec=0, now=Date.now()) {
|
||||
return now + localtimeOffsetMsec + duration;
|
||||
},
|
||||
|
||||
isCertValid: function(aCert, aCallback) {
|
||||
// XXX check expiration, bug 769850
|
||||
aCallback(true);
|
||||
@ -97,7 +115,41 @@ jwcryptoClass.prototype = {
|
||||
generateKeyPair(aAlgorithmName, aCallback);
|
||||
},
|
||||
|
||||
generateAssertion: function(aCert, aKeyPair, aAudience, aCallback) {
|
||||
/*
|
||||
* Generate an assertion and return it through the provided callback.
|
||||
*
|
||||
* @param aCert
|
||||
* Identity certificate
|
||||
*
|
||||
* @param aKeyPair
|
||||
* KeyPair object
|
||||
*
|
||||
* @param aAudience
|
||||
* Audience of the assertion
|
||||
*
|
||||
* @param aOptions (optional)
|
||||
* Can include:
|
||||
* {
|
||||
* localtimeOffsetMsec: <clock offset in milliseconds>,
|
||||
* now: <current date in milliseconds>
|
||||
* duration: <validity duration for this assertion in milliseconds>
|
||||
* }
|
||||
*
|
||||
* localtimeOffsetMsec is the number of milliseconds that need to be
|
||||
* added to the local clock time to make it concur with the server.
|
||||
* For example, if the local clock is two minutes fast, the offset in
|
||||
* milliseconds would be -120000.
|
||||
*
|
||||
* @param aCallback
|
||||
* Function to invoke with resulting assertion. Assertion
|
||||
* will be string or null on failure.
|
||||
*/
|
||||
generateAssertion: function(aCert, aKeyPair, aAudience, aOptions, aCallback) {
|
||||
if (typeof aOptions == "function") {
|
||||
aCallback = aOptions;
|
||||
aOptions = { };
|
||||
}
|
||||
|
||||
// for now, we hack the algorithm name
|
||||
// XXX bug 769851
|
||||
var header = {"alg": "DS128"};
|
||||
@ -105,9 +157,8 @@ jwcryptoClass.prototype = {
|
||||
JSON.stringify(header));
|
||||
|
||||
var payload = {
|
||||
// expires in 2 minutes
|
||||
// XXX clock skew needs exploration bug 769852
|
||||
exp: Date.now() + (2 * 60 * 1000),
|
||||
exp: this.getExpiration(
|
||||
aOptions.duration, aOptions.localtimeOffsetMsec, aOptions.now),
|
||||
aud: aAudience
|
||||
};
|
||||
var payloadBytes = IdentityCryptoService.base64UrlEncode(
|
||||
|
@ -12,9 +12,18 @@ XPCOMUtils.defineLazyModuleGetter(this, "IDService",
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
|
||||
"resource://gre/modules/identity/jwcrypto.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this,
|
||||
"CryptoService",
|
||||
"@mozilla.org/identity/crypto-service;1",
|
||||
"nsIIdentityCryptoService");
|
||||
|
||||
const RP_ORIGIN = "http://123done.org";
|
||||
const INTERNAL_ORIGIN = "browserid://";
|
||||
|
||||
const SECOND_MS = 1000;
|
||||
const MINUTE_MS = SECOND_MS * 60;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
|
||||
function test_sanity() {
|
||||
do_test_pending();
|
||||
|
||||
@ -43,11 +52,11 @@ function test_get_assertion() {
|
||||
jwcrypto.generateKeyPair(
|
||||
"DS160",
|
||||
function(err, kp) {
|
||||
jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN, function(err, assertion) {
|
||||
jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN, (err, backedAssertion) => {
|
||||
do_check_null(err);
|
||||
|
||||
// more checks on assertion
|
||||
log("assertion", assertion);
|
||||
do_check_eq(backedAssertion.split("~").length, 2);
|
||||
do_check_eq(backedAssertion.split(".").length, 3);
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
@ -116,7 +125,128 @@ function test_dsa() {
|
||||
jwcrypto.generateKeyPair("DS160", checkDSA);
|
||||
}
|
||||
|
||||
var TESTS = [test_sanity, test_generate, test_get_assertion];
|
||||
function test_get_assertion_with_offset() {
|
||||
do_test_pending();
|
||||
|
||||
|
||||
// Use an arbitrary date in the past to ensure we don't accidentally pass
|
||||
// this test with current dates, missing offsets, etc.
|
||||
let serverMsec = Date.parse("Tue Oct 31 2000 00:00:00 GMT-0800");
|
||||
|
||||
// local clock skew
|
||||
// clock is 12 hours fast; -12 hours offset must be applied
|
||||
let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
|
||||
let localMsec = serverMsec - localtimeOffsetMsec;
|
||||
|
||||
jwcrypto.generateKeyPair(
|
||||
"DS160",
|
||||
function(err, kp) {
|
||||
jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN,
|
||||
{ duration: MINUTE_MS,
|
||||
localtimeOffsetMsec: localtimeOffsetMsec,
|
||||
now: localMsec},
|
||||
function(err, backedAssertion) {
|
||||
do_check_null(err);
|
||||
|
||||
// properly formed
|
||||
let cert;
|
||||
let assertion;
|
||||
[cert, assertion] = backedAssertion.split("~");
|
||||
|
||||
do_check_eq(cert, "fake-cert");
|
||||
do_check_eq(assertion.split(".").length, 3);
|
||||
|
||||
let components = extractComponents(assertion);
|
||||
|
||||
// Expiry is within two minutes, corrected for skew
|
||||
let exp = parseInt(components.payload.exp, 10);
|
||||
do_check_true(exp - serverMsec === MINUTE_MS);
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function test_assertion_lifetime() {
|
||||
do_test_pending();
|
||||
|
||||
jwcrypto.generateKeyPair(
|
||||
"DS160",
|
||||
function(err, kp) {
|
||||
jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN,
|
||||
{duration: MINUTE_MS},
|
||||
function(err, backedAssertion) {
|
||||
do_check_null(err);
|
||||
|
||||
// properly formed
|
||||
let cert;
|
||||
let assertion;
|
||||
[cert, assertion] = backedAssertion.split("~");
|
||||
|
||||
do_check_eq(cert, "fake-cert");
|
||||
do_check_eq(assertion.split(".").length, 3);
|
||||
|
||||
let components = extractComponents(assertion);
|
||||
|
||||
// Expiry is within one minute, as we specified above
|
||||
let exp = parseInt(components.payload.exp, 10);
|
||||
do_check_true(Math.abs(Date.now() - exp) > 50 * SECOND_MS);
|
||||
do_check_true(Math.abs(Date.now() - exp) <= MINUTE_MS);
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// End of tests
|
||||
// Helper function follow
|
||||
|
||||
function extractComponents(signedObject) {
|
||||
if (typeof(signedObject) != 'string') {
|
||||
throw new Error("malformed signature " + typeof(signedObject));
|
||||
}
|
||||
|
||||
let parts = signedObject.split(".");
|
||||
if (parts.length != 3) {
|
||||
throw new Error("signed object must have three parts, this one has " + parts.length);
|
||||
}
|
||||
|
||||
let headerSegment = parts[0];
|
||||
let payloadSegment = parts[1];
|
||||
let cryptoSegment = parts[2];
|
||||
|
||||
let header = JSON.parse(CryptoService.base64UrlDecode(headerSegment));
|
||||
let payload = JSON.parse(CryptoService.base64UrlDecode(payloadSegment));
|
||||
|
||||
// Ensure well-formed header
|
||||
do_check_eq(Object.keys(header).length, 1);
|
||||
do_check_true(!!header.alg);
|
||||
|
||||
// Ensure well-formed payload
|
||||
for (let field of ["exp", "aud"]) {
|
||||
do_check_true(!!payload[field]);
|
||||
}
|
||||
|
||||
return {header: header,
|
||||
payload: payload,
|
||||
headerSegment: headerSegment,
|
||||
payloadSegment: payloadSegment,
|
||||
cryptoSegment: cryptoSegment};
|
||||
};
|
||||
|
||||
let TESTS = [
|
||||
test_sanity,
|
||||
test_generate,
|
||||
test_get_assertion,
|
||||
test_get_assertion_with_offset,
|
||||
test_assertion_lifetime,
|
||||
];
|
||||
|
||||
TESTS = TESTS.concat([test_rsa, test_dsa]);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user