Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Closes #55

  • Loading branch information...
commit 939754afde99342bed835930f0046a303ec7ca5a 1 parent 0791ab5
Eran Hammer authored
14 README.md
Source Rendered
@@ -3,7 +3,7 @@
3 3 <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
4 4 HTTP request cryptographic verification. For more complex use cases such as access delegation, see [Oz](https://github.com/hueniverse/oz).
5 5
6   -Current version: **0.12**
  6 +Current version: **0.13**
7 7
8 8 [![Build Status](https://secure.travis-ci.org/hueniverse/hawk.png)](http://travis-ci.org/hueniverse/hawk)
9 9
@@ -345,10 +345,16 @@ Server-Authorization: Hawk mac="XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", h
345 345
346 346 ## Browser Support and Considerations
347 347
348   -An experimental browser script is provided for including using a `<script>` tag in [lib/browser.js](/lib/browser.js).
  348 +A browser script is provided for including using a `<script>` tag in [lib/browser.js](/lib/browser.js).
349 349
350   -**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.
351   -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)
  350 +**Hawk** relies on the _Server-Authorization_ and _WWW-Authenticate_ headers in its response to communicate with the client.
  351 +Therefore, in case of CORS requests, it is important to consider sending _Access-Control-Expose-Headers_ with the value
  352 +_"WWW-Authenticate, Server-Authorization"_ on each response from your server. As explained in the
  353 +[specifications](http://www.w3.org/TR/cors/#access-control-expose-headers-response-header), it will indicate that these headers
  354 +can safely be accessed by the client (using getResponseHeader() on the XmlHttpRequest object). Otherwise you will be met with a
  355 +["simple response header"](http://www.w3.org/TR/cors/#simple-response-header) which excludes these fields and would prevent the
  356 +Hawk client from authenticating the requests.You can read more about the why and how in this
  357 +[article](http://www.html5rocks.com/en/tutorials/cors/#toc-adding-cors-support-to-the-server)
352 358
353 359
354 360 # Single URI Authorization
49 lib/browser.js
@@ -111,7 +111,7 @@ hawk.client = {
111 111 if (!artifacts.hash &&
112 112 options.hasOwnProperty('payload')) {
113 113
114   - artifacts.hash = hawk.crypto.calculateHash(options.payload, credentials.algorithm, options.contentType);
  114 + artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
115 115 }
116 116
117 117 var mac = hawk.crypto.calculateMac('header', credentials, artifacts);
@@ -210,7 +210,7 @@ hawk.client = {
210 210 return false;
211 211 }
212 212
213   - var calculatedHash = hawk.crypto.calculateHash(options.payload, credentials.algorithm, request.getResponseHeader('content-type'));
  213 + var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.getResponseHeader('content-type'));
214 214 return (calculatedHash === attributes.hash);
215 215 }
216 216 };
@@ -255,7 +255,7 @@ hawk.crypto = {
255 255 return normalized;
256 256 },
257 257
258   - calculateHash: function (payload, algorithm, contentType) {
  258 + calculatePayloadHash: function (payload, algorithm, contentType) {
259 259
260 260 var hash = CryptoJS.algo[algorithm.toUpperCase()].create();
261 261 hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n');
@@ -275,41 +275,38 @@ hawk.crypto = {
275 275
276 276 hawk.utils = {
277 277
278   - ntpOffset: 0,
  278 + storage: { // localStorage compatible interface
  279 + _cache: {},
  280 + setItem: function (key, value) {
279 281
280   - storage: false,
281   -
282   - useLocalStorage: function (storage) {
283   -
284   - hawk.utils.storage = storage;
  282 + hawk.utils.storage._cache[key] = value;
  283 + },
  284 + getItem: function (key) {
285 285
286   - if (hawk.utils.storage) {
287   - hawk.utils.ntpOffset = hawk.utils.getLocalNtpOffset();
  286 + return hawk.utils.storage._cache[key];
288 287 }
289 288 },
290 289
291   - setLocalNtpOffset: function (offset) {
  290 + setStorage: function (storage) {
292 291
293   - hawk.utils.storage.setItem('hawk_ntp_offset', offset);
  292 + var ntpOffset = hawk.utils.getNtpOffset() || 0;
  293 + hawk.utils.storage = storage;
  294 + hawk.utils.setNtpOffset(ntpOffset);
294 295 },
295 296
296   - getLocalNtpOffset: function () {
  297 + setNtpOffset: function (offset) {
297 298
298   - return hawk.utils.storage.getItem('hawk_ntp_offset');
  299 + hawk.utils.storage.setItem('hawk_ntp_offset', offset);
299 300 },
300 301
301   - setNtpOffset: function (offset) {
302   -
303   - hawk.utils.ntpOffset = offset;
  302 + getNtpOffset: function () {
304 303
305   - if (hawk.utils.storage) {
306   - hawk.utils.setLocalNtpOffset(offset);
307   - }
  304 + return parseInt(hawk.utils.storage.getItem('hawk_ntp_offset') || '0', 10);
308 305 },
309 306
310 307 now: function () {
311 308
312   - return Date.now() + hawk.utils.ntpOffset;
  309 + return Date.now() + hawk.utils.getNtpOffset();
313 310 },
314 311
315 312 escapeHeaderAttribute: function (attribute) {
@@ -421,14 +418,6 @@ hawk.utils = {
421 418 };
422 419
423 420
424   -try {
425   - hawk.utils.useLocalStorage(localStorage)
426   -}
427   -catch (err) {
428   - // localStorage not used
429   -}
430   -
431   -
432 421 // Based on: Crypto-JS v3.1.2
433 422 // Copyright (c) 2009-2013, Jeff Mott. All rights reserved.
434 423 // http://code.google.com/p/crypto-js/
4 lib/client.js
@@ -105,7 +105,7 @@ exports.header = function (uri, method, options) {
105 105 if (!artifacts.hash &&
106 106 options.hasOwnProperty('payload')) {
107 107
108   - artifacts.hash = Crypto.calculateHash(options.payload, credentials.algorithm, options.contentType);
  108 + artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
109 109 }
110 110
111 111 var mac = Crypto.calculateMac('header', credentials, artifacts);
@@ -193,7 +193,7 @@ exports.authenticate = function (res, credentials, artifacts, options) {
193 193 return false;
194 194 }
195 195
196   - var calculatedHash = Crypto.calculateHash(options.payload, credentials.algorithm, res.headers['content-type']);
  196 + var calculatedHash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, res.headers['content-type']);
197 197 return (calculatedHash === attributes.hash);
198 198 };
199 199
17 lib/crypto.js
@@ -78,12 +78,25 @@ exports.generateNormalizedString = function (type, options) {
78 78 };
79 79
80 80
81   -exports.calculateHash = function (payload, algorithm, contentType) {
  81 +exports.calculatePayloadHash = function (payload, algorithm, contentType) {
  82 +
  83 + var hash = exports.createPayloadHash(algorithm, contentType);
  84 + hash.update(payload || '');
  85 + return exports.finalizePayloadHash(hash);
  86 +};
  87 +
  88 +
  89 +exports.createPayloadHash = function (algorithm, contentType) {
82 90
83 91 var hash = Crypto.createHash(algorithm);
84 92 hash.update('hawk.' + exports.headerVersion + '.payload\n');
85 93 hash.update(Utils.parseContentType(contentType) + '\n');
86   - hash.update(payload || '');
  94 + return hash;
  95 +};
  96 +
  97 +
  98 +exports.finalizePayloadHash = function (hash) {
  99 +
87 100 hash.update('\n');
88 101 return hash.digest('base64');
89 102 };
6 lib/server.js
@@ -171,7 +171,7 @@ exports.authenticate = function (req, credentialsFunc, options, callback) {
171 171 return callback(Boom.unauthorized('Missing required payload hash', 'Hawk'), credentials, artifacts);
172 172 }
173 173
174   - var hash = Crypto.calculateHash(options.payload, credentials.algorithm, request.contentType);
  174 + var hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.contentType);
175 175 if (!Cryptiles.fixedTimeComparison(hash, attributes.hash)) {
176 176 return callback(Boom.unauthorized('Bad payload hash', 'Hawk'), credentials, artifacts);
177 177 }
@@ -212,7 +212,7 @@ exports.authenticate = function (req, credentialsFunc, options, callback) {
212 212
213 213 exports.authenticatePayload = function (payload, credentials, artifacts, contentType) {
214 214
215   - var calculatedHash = Crypto.calculateHash(payload, credentials.algorithm, contentType);
  215 + var calculatedHash = Crypto.calculatePayloadHash(payload, credentials.algorithm, contentType);
216 216 return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);
217 217 };
218 218
@@ -267,7 +267,7 @@ exports.header = function (credentials, artifacts, options) {
267 267 if (!artifacts.hash &&
268 268 options.hasOwnProperty('payload')) {
269 269
270   - artifacts.hash = Crypto.calculateHash(options.payload, credentials.algorithm, options.contentType);
  270 + artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
271 271 }
272 272
273 273 var mac = Crypto.calculateMac('response', credentials, artifacts);
2  package.json
... ... @@ -1,7 +1,7 @@
1 1 {
2 2 "name": "hawk",
3 3 "description": "HTTP Hawk Authentication Scheme",
4   - "version": "0.12.2",
  4 + "version": "0.13.0",
5 5 "author": "Eran Hammer <eran@hueniverse.com> (http://hueniverse.com)",
6 6 "contributors": [],
7 7 "repository": "git://github.com/hueniverse/hawk",
55 test/browser.js
@@ -197,7 +197,56 @@ describe('Browser', function () {
197 197 };
198 198
199 199 credentialsFunc('123456', function (err, credentials) {
200   - Browser.utils.useLocalStorage(LocalStorage)
  200 +
  201 + Browser.utils.setNtpOffset(60 * 60 * 1000);
  202 + var header = Browser.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials, ext: 'some-app-data' });
  203 + req.authorization = header.field;
  204 + expect(req.authorization).to.exist;
  205 +
  206 + Hawk.server.authenticate(req, credentialsFunc, {}, function (err, credentials, artifacts) {
  207 +
  208 + expect(err).to.exist;
  209 + expect(err.message).to.equal('Stale timestamp');
  210 +
  211 + var res = {
  212 + headers: {
  213 + 'www-authenticate': err.response.headers['WWW-Authenticate']
  214 + },
  215 + getResponseHeader: function (header) {
  216 +
  217 + return res.headers[header.toLowerCase()];
  218 + }
  219 + };
  220 +
  221 + expect(Browser.utils.getNtpOffset()).to.equal(60 * 60 * 1000);
  222 + expect(Browser.client.authenticate(res, credentials, header.artifacts)).to.equal(true);
  223 + expect(Browser.utils.getNtpOffset()).to.equal(0);
  224 +
  225 + req.authorization = Browser.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials, ext: 'some-app-data' }).field;
  226 + expect(req.authorization).to.exist;
  227 +
  228 + Hawk.server.authenticate(req, credentialsFunc, {}, function (err, credentials, artifacts) {
  229 +
  230 + expect(err).to.not.exist;
  231 + expect(credentials.user).to.equal('steve');
  232 + expect(artifacts.ext).to.equal('some-app-data');
  233 + done();
  234 + });
  235 + });
  236 + });
  237 + });
  238 +
  239 + it('should generate a header with stale ts and successfully authenticate on second call (manual localStorage)', function (done) {
  240 +
  241 + var req = {
  242 + method: 'GET',
  243 + url: '/resource/4?filter=a',
  244 + host: 'example.com',
  245 + port: 8080
  246 + };
  247 +
  248 + credentialsFunc('123456', function (err, credentials) {
  249 + Browser.utils.setStorage(LocalStorage)
201 250
202 251 Browser.utils.setNtpOffset(60 * 60 * 1000);
203 252 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 () {
220 269 };
221 270
222 271 expect(parseInt(LocalStorage.getItem('hawk_ntp_offset'))).to.equal(60 * 60 * 1000);
223   - expect(Browser.utils.ntpOffset).to.equal(60 * 60 * 1000);
  272 + expect(Browser.utils.getNtpOffset()).to.equal(60 * 60 * 1000);
224 273 expect(Browser.client.authenticate(res, credentials, header.artifacts)).to.equal(true);
225   - expect(Browser.utils.ntpOffset).to.equal(0);
  274 + expect(Browser.utils.getNtpOffset()).to.equal(0);
226 275 expect(parseInt(LocalStorage.getItem('hawk_ntp_offset'))).to.equal(0);
227 276
228 277 req.authorization = Browser.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials, ext: 'some-app-data' }).field;
2  test/readme.js
@@ -85,7 +85,7 @@ describe('Hawk', function () {
85 85 resource: '/resource?a=1&b=2',
86 86 host: 'example.com',
87 87 port: 8000,
88   - hash: Hawk.crypto.calculateHash(payloadOptions.payload, credentials.algorithm, payloadOptions.contentType),
  88 + hash: Hawk.crypto.calculatePayloadHash(payloadOptions.payload, credentials.algorithm, payloadOptions.contentType),
89 89 ext: options.ext
90 90 });
91 91

0 comments on commit 939754a

Please sign in to comment.
Something went wrong with that request. Please try again.