Skip to content
This repository has been archived by the owner on Apr 5, 2024. It is now read-only.

Commit

Permalink
Closes #55
Browse files Browse the repository at this point in the history
  • Loading branch information
Eran Hammer committed Apr 12, 2013
1 parent 0791ab5 commit 939754a
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 46 deletions.
14 changes: 10 additions & 4 deletions README.md
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
49 changes: 19 additions & 30 deletions lib/browser.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
};
Expand Down Expand Up @@ -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');
Expand All @@ -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) {
Expand Down Expand Up @@ -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/
Expand Down
4 changes: 2 additions & 2 deletions lib/client.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
};

Expand Down
17 changes: 15 additions & 2 deletions lib/crypto.js
Expand Up @@ -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');
};
Expand Down
6 changes: 3 additions & 3 deletions lib/server.js
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
};

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion 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",
Expand Down
55 changes: 52 additions & 3 deletions test/browser.js
Expand Up @@ -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' });
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion test/readme.js
Expand Up @@ -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
});

Expand Down

0 comments on commit 939754a

Please sign in to comment.