Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Closes #55

  • Loading branch information...
commit 939754afde99342bed835930f0046a303ec7ca5a 1 parent 0791ab5
@hueniverse authored
View
14 README.md
@@ -3,7 +3,7 @@
<img align="right" src="https://raw.github.com/hueniverse/hawk/master/images/logo.png" /> **Hawk** is an HTTP authentication scheme using a message authentication code (MAC) algorithm to provide partial
HTTP request cryptographic verification. For more complex use cases such as access delegation, see [Oz](https://github.com/hueniverse/oz).
-Current version: **0.12**
+Current version: **0.13**
[![Build Status](https://secure.travis-ci.org/hueniverse/hawk.png)](http://travis-ci.org/hueniverse/hawk)
@@ -345,10 +345,16 @@ Server-Authorization: Hawk mac="XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", h
## Browser Support and Considerations
-An experimental browser script is provided for including using a `<script>` tag in [lib/browser.js](/lib/browser.js).
+A browser script is provided for including using a `<script>` tag in [lib/browser.js](/lib/browser.js).
-**Hawk** relies on the _Server-Authorization_ and _WWW-Authenticate_ headers in its response to communicate with the client. Therefore, in case of CORS requests, it is important to consider sending _Access-Control-Expose-Headers_ with the value _"WWW-Authenticate, Server-Authorization"_ on each response from your server. As explained in the [specifications](http://www.w3.org/TR/cors/#access-control-expose-headers-response-header), it will indicate that these headers can safely be accessed by the client (using getResponseHeader() on the XmlHttpRequest object). Otherwise you will be met with a ["simple response header"](http://www.w3.org/TR/cors/#simple-response-header) which excludes these fields and would prevent the Hawk client from authenticating the requests.
-You can read more about the why and how in this [article](http://www.html5rocks.com/en/tutorials/cors/#toc-adding-cors-support-to-the-server)
+**Hawk** relies on the _Server-Authorization_ and _WWW-Authenticate_ headers in its response to communicate with the client.
+Therefore, in case of CORS requests, it is important to consider sending _Access-Control-Expose-Headers_ with the value
+_"WWW-Authenticate, Server-Authorization"_ on each response from your server. As explained in the
+[specifications](http://www.w3.org/TR/cors/#access-control-expose-headers-response-header), it will indicate that these headers
+can safely be accessed by the client (using getResponseHeader() on the XmlHttpRequest object). Otherwise you will be met with a
+["simple response header"](http://www.w3.org/TR/cors/#simple-response-header) which excludes these fields and would prevent the
+Hawk client from authenticating the requests.You can read more about the why and how in this
+[article](http://www.html5rocks.com/en/tutorials/cors/#toc-adding-cors-support-to-the-server)
# Single URI Authorization
View
49 lib/browser.js
@@ -111,7 +111,7 @@ hawk.client = {
if (!artifacts.hash &&
options.hasOwnProperty('payload')) {
- artifacts.hash = hawk.crypto.calculateHash(options.payload, credentials.algorithm, options.contentType);
+ artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
}
var mac = hawk.crypto.calculateMac('header', credentials, artifacts);
@@ -210,7 +210,7 @@ hawk.client = {
return false;
}
- var calculatedHash = hawk.crypto.calculateHash(options.payload, credentials.algorithm, request.getResponseHeader('content-type'));
+ var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.getResponseHeader('content-type'));
return (calculatedHash === attributes.hash);
}
};
@@ -255,7 +255,7 @@ hawk.crypto = {
return normalized;
},
- calculateHash: function (payload, algorithm, contentType) {
+ calculatePayloadHash: function (payload, algorithm, contentType) {
var hash = CryptoJS.algo[algorithm.toUpperCase()].create();
hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n');
@@ -275,41 +275,38 @@ hawk.crypto = {
hawk.utils = {
- ntpOffset: 0,
+ storage: { // localStorage compatible interface
+ _cache: {},
+ setItem: function (key, value) {
- storage: false,
-
- useLocalStorage: function (storage) {
-
- hawk.utils.storage = storage;
+ hawk.utils.storage._cache[key] = value;
+ },
+ getItem: function (key) {
- if (hawk.utils.storage) {
- hawk.utils.ntpOffset = hawk.utils.getLocalNtpOffset();
+ return hawk.utils.storage._cache[key];
}
},
- setLocalNtpOffset: function (offset) {
+ setStorage: function (storage) {
- hawk.utils.storage.setItem('hawk_ntp_offset', offset);
+ var ntpOffset = hawk.utils.getNtpOffset() || 0;
+ hawk.utils.storage = storage;
+ hawk.utils.setNtpOffset(ntpOffset);
},
- getLocalNtpOffset: function () {
+ setNtpOffset: function (offset) {
- return hawk.utils.storage.getItem('hawk_ntp_offset');
+ hawk.utils.storage.setItem('hawk_ntp_offset', offset);
},
- setNtpOffset: function (offset) {
-
- hawk.utils.ntpOffset = offset;
+ getNtpOffset: function () {
- if (hawk.utils.storage) {
- hawk.utils.setLocalNtpOffset(offset);
- }
+ return parseInt(hawk.utils.storage.getItem('hawk_ntp_offset') || '0', 10);
},
now: function () {
- return Date.now() + hawk.utils.ntpOffset;
+ return Date.now() + hawk.utils.getNtpOffset();
},
escapeHeaderAttribute: function (attribute) {
@@ -421,14 +418,6 @@ hawk.utils = {
};
-try {
- hawk.utils.useLocalStorage(localStorage)
-}
-catch (err) {
- // localStorage not used
-}
-
-
// Based on: Crypto-JS v3.1.2
// Copyright (c) 2009-2013, Jeff Mott. All rights reserved.
// http://code.google.com/p/crypto-js/
View
4 lib/client.js
@@ -105,7 +105,7 @@ exports.header = function (uri, method, options) {
if (!artifacts.hash &&
options.hasOwnProperty('payload')) {
- artifacts.hash = Crypto.calculateHash(options.payload, credentials.algorithm, options.contentType);
+ artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
}
var mac = Crypto.calculateMac('header', credentials, artifacts);
@@ -193,7 +193,7 @@ exports.authenticate = function (res, credentials, artifacts, options) {
return false;
}
- var calculatedHash = Crypto.calculateHash(options.payload, credentials.algorithm, res.headers['content-type']);
+ var calculatedHash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, res.headers['content-type']);
return (calculatedHash === attributes.hash);
};
View
17 lib/crypto.js
@@ -78,12 +78,25 @@ exports.generateNormalizedString = function (type, options) {
};
-exports.calculateHash = function (payload, algorithm, contentType) {
+exports.calculatePayloadHash = function (payload, algorithm, contentType) {
+
+ var hash = exports.createPayloadHash(algorithm, contentType);
+ hash.update(payload || '');
+ return exports.finalizePayloadHash(hash);
+};
+
+
+exports.createPayloadHash = function (algorithm, contentType) {
var hash = Crypto.createHash(algorithm);
hash.update('hawk.' + exports.headerVersion + '.payload\n');
hash.update(Utils.parseContentType(contentType) + '\n');
- hash.update(payload || '');
+ return hash;
+};
+
+
+exports.finalizePayloadHash = function (hash) {
+
hash.update('\n');
return hash.digest('base64');
};
View
6 lib/server.js
@@ -171,7 +171,7 @@ exports.authenticate = function (req, credentialsFunc, options, callback) {
return callback(Boom.unauthorized('Missing required payload hash', 'Hawk'), credentials, artifacts);
}
- var hash = Crypto.calculateHash(options.payload, credentials.algorithm, request.contentType);
+ var hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.contentType);
if (!Cryptiles.fixedTimeComparison(hash, attributes.hash)) {
return callback(Boom.unauthorized('Bad payload hash', 'Hawk'), credentials, artifacts);
}
@@ -212,7 +212,7 @@ exports.authenticate = function (req, credentialsFunc, options, callback) {
exports.authenticatePayload = function (payload, credentials, artifacts, contentType) {
- var calculatedHash = Crypto.calculateHash(payload, credentials.algorithm, contentType);
+ var calculatedHash = Crypto.calculatePayloadHash(payload, credentials.algorithm, contentType);
return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);
};
@@ -267,7 +267,7 @@ exports.header = function (credentials, artifacts, options) {
if (!artifacts.hash &&
options.hasOwnProperty('payload')) {
- artifacts.hash = Crypto.calculateHash(options.payload, credentials.algorithm, options.contentType);
+ artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
}
var mac = Crypto.calculateMac('response', credentials, artifacts);
View
2  package.json
@@ -1,7 +1,7 @@
{
"name": "hawk",
"description": "HTTP Hawk Authentication Scheme",
- "version": "0.12.2",
+ "version": "0.13.0",
"author": "Eran Hammer <eran@hueniverse.com> (http://hueniverse.com)",
"contributors": [],
"repository": "git://github.com/hueniverse/hawk",
View
55 test/browser.js
@@ -197,7 +197,56 @@ describe('Browser', function () {
};
credentialsFunc('123456', function (err, credentials) {
- Browser.utils.useLocalStorage(LocalStorage)
+
+ Browser.utils.setNtpOffset(60 * 60 * 1000);
+ var header = Browser.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials, ext: 'some-app-data' });
+ req.authorization = header.field;
+ expect(req.authorization).to.exist;
+
+ Hawk.server.authenticate(req, credentialsFunc, {}, function (err, credentials, artifacts) {
+
+ expect(err).to.exist;
+ expect(err.message).to.equal('Stale timestamp');
+
+ var res = {
+ headers: {
+ 'www-authenticate': err.response.headers['WWW-Authenticate']
+ },
+ getResponseHeader: function (header) {
+
+ return res.headers[header.toLowerCase()];
+ }
+ };
+
+ expect(Browser.utils.getNtpOffset()).to.equal(60 * 60 * 1000);
+ expect(Browser.client.authenticate(res, credentials, header.artifacts)).to.equal(true);
+ expect(Browser.utils.getNtpOffset()).to.equal(0);
+
+ req.authorization = Browser.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials, ext: 'some-app-data' }).field;
+ expect(req.authorization).to.exist;
+
+ Hawk.server.authenticate(req, credentialsFunc, {}, function (err, credentials, artifacts) {
+
+ expect(err).to.not.exist;
+ expect(credentials.user).to.equal('steve');
+ expect(artifacts.ext).to.equal('some-app-data');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should generate a header with stale ts and successfully authenticate on second call (manual localStorage)', function (done) {
+
+ var req = {
+ method: 'GET',
+ url: '/resource/4?filter=a',
+ host: 'example.com',
+ port: 8080
+ };
+
+ credentialsFunc('123456', function (err, credentials) {
+ Browser.utils.setStorage(LocalStorage)
Browser.utils.setNtpOffset(60 * 60 * 1000);
var header = Browser.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials, ext: 'some-app-data' });
@@ -220,9 +269,9 @@ describe('Browser', function () {
};
expect(parseInt(LocalStorage.getItem('hawk_ntp_offset'))).to.equal(60 * 60 * 1000);
- expect(Browser.utils.ntpOffset).to.equal(60 * 60 * 1000);
+ expect(Browser.utils.getNtpOffset()).to.equal(60 * 60 * 1000);
expect(Browser.client.authenticate(res, credentials, header.artifacts)).to.equal(true);
- expect(Browser.utils.ntpOffset).to.equal(0);
+ expect(Browser.utils.getNtpOffset()).to.equal(0);
expect(parseInt(LocalStorage.getItem('hawk_ntp_offset'))).to.equal(0);
req.authorization = Browser.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials, ext: 'some-app-data' }).field;
View
2  test/readme.js
@@ -85,7 +85,7 @@ describe('Hawk', function () {
resource: '/resource?a=1&b=2',
host: 'example.com',
port: 8000,
- hash: Hawk.crypto.calculateHash(payloadOptions.payload, credentials.algorithm, payloadOptions.contentType),
+ hash: Hawk.crypto.calculatePayloadHash(payloadOptions.payload, credentials.algorithm, payloadOptions.contentType),
ext: options.ext
});
Please sign in to comment.
Something went wrong with that request. Please try again.