Comparing version 0.2.0 to 0.3.0
@@ -58,3 +58,3 @@ // Load modules | ||
headers: { | ||
authorization: Hawk.getAuthorizationHeader(internals.credentials.dh37fgj492je, 'GET', '/resource/1?b=1&a=2', '127.0.0.1', 8000, 'some-app-data') | ||
authorization: Hawk.getAuthorizationHeader(internals.credentials.dh37fgj492je, 'GET', '/resource/1?b=1&a=2', '127.0.0.1', 8000, { ext: 'some-app-data' }) | ||
} | ||
@@ -61,0 +61,0 @@ }; |
120
lib/index.js
// Load modules | ||
var Crypto = require('crypto'); | ||
var URL = require('url'); | ||
var Crypto = require('./crypto'); | ||
var Err = require('./error'); | ||
var Utils = require('./utils'); | ||
var Uri = require('./uri'); | ||
@@ -14,4 +14,7 @@ | ||
// Export utilities | ||
// Export sub-modules | ||
exports.crypto = Crypto; | ||
exports.error = exports.Error = Err; | ||
exports.uri = Uri; | ||
exports.utils = Utils; | ||
@@ -58,9 +61,9 @@ | ||
* | ||
* timestampSkewSec - number of seconds of permitted clock skew for incoming timestamps. Defaults to 60 seconds. | ||
* timestampSkewSec - optional number of seconds of permitted clock skew for incoming timestamps. Defaults to 60 seconds. | ||
* Provides a +/- skew which means actual allowed window is double the number of seconds. | ||
* | ||
* localtimeOffsetMsec - local clock time offset express in a number of milliseconds (positive or negative). | ||
* localtimeOffsetMsec - optional local clock time offset express in a number of milliseconds (positive or negative). | ||
* Defaults to 0. | ||
* | ||
* ntp - hostname of the ntp server used to synchronize time between the client and the server. The | ||
* ntp - optional hostname of the ntp server used to synchronize time between the client and the server. The | ||
* ntp server name is included when the client's timestamp is stale along with the server's | ||
@@ -76,28 +79,26 @@ * current timestamp. This allows browser-based clients to sync their application clock directly | ||
options.hostHeaderName = (options.hostHeaderName ? options.hostHeaderName.toLowerCase() : 'host'); | ||
options.nonceFunc = options.nonceFunc || function (nonce, ts, callback) { return callback(); }; | ||
options.nonceFunc = options.nonceFunc || function (nonce, ts, callback) { return callback(); }; // No validation | ||
options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds | ||
options.localtimeOffsetMsec = options.localtimeOffsetMsec || 0; // 0 milliseconds | ||
options.ntp = options.ntp || 'pool.ntp.org'; | ||
options.ntp = options.ntp || 'pool.ntp.org'; // pool.ntp.org | ||
// Application time | ||
var now = Date.now() + options.localtimeOffsetMsec; | ||
var now = Date.now() + (options.localtimeOffsetMsec || 0); | ||
// Check required HTTP headers: host, authentication | ||
// Obtain host and port information | ||
var hostHeader = req.headers[options.hostHeaderName]; | ||
if (!hostHeader) { | ||
return callback(Err.badRequest('Missing Host header'), null, null); | ||
var host = Utils.parseHost(req, options.hostHeaderName); | ||
if (!host) { | ||
return callback(Err.badRequest('Invalid Host header')); | ||
} | ||
// Parse HTTP Authorization header | ||
if (!req.headers.authorization) { | ||
return callback(Err.unauthorizedWithTs('', now, options.ntp), null, null); | ||
return callback(Err.unauthorizedWithTs('', now, options.ntp)); | ||
} | ||
// Parse HTTP Authorization header | ||
var headerParts = req.headers.authorization.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something] | ||
if (!headerParts) { | ||
return callback(Err.badRequest('Invalid header syntax'), null, null); | ||
return callback(Err.badRequest('Invalid header syntax')); | ||
} | ||
@@ -107,3 +108,3 @@ | ||
if (scheme.toLowerCase() !== 'hawk') { | ||
return callback(Err.unauthorizedWithTs('', now, options.ntp), null, null); | ||
return callback(Err.unauthorizedWithTs('', now, options.ntp)); | ||
} | ||
@@ -113,3 +114,3 @@ | ||
if (!attributesString) { | ||
return callback(Err.badRequest('Invalid header syntax'), null, null); | ||
return callback(Err.badRequest('Invalid header syntax')); | ||
} | ||
@@ -147,3 +148,3 @@ | ||
if (verify !== '') { | ||
return callback(Err.badRequest(errorMessage || 'Bad header format'), null, null); | ||
return callback(Err.badRequest(errorMessage || 'Bad header format')); | ||
} | ||
@@ -167,17 +168,2 @@ | ||
// Obtain host and port information | ||
var hostHeaderRegex = /^(?:(?:\r\n)?[\t ])*([^:]+)(?::(\d+))?(?:(?:\r\n)?[\t ])*$/; // Does not support IPv6 | ||
var hostParts = hostHeader.match(hostHeaderRegex); | ||
if (!hostParts || | ||
hostParts.length !== 3 || | ||
!hostParts[1]) { | ||
return callback(Err.badRequest('Bad Host header'), null, attributes.ext); | ||
} | ||
var host = hostParts[1]; | ||
var port = (hostParts[2] ? hostParts[2] : (req.connection && req.connection.encrypted ? 443 : 80)); | ||
// Fetch Hawk credentials | ||
@@ -192,3 +178,3 @@ | ||
if (!credentials) { | ||
return callback(Err.unauthorized('Missing credentials'), null, attributes.ext); | ||
return callback(Err.unauthorized('Unknown credentials'), null, attributes.ext); | ||
} | ||
@@ -202,3 +188,3 @@ | ||
if (['hmac-sha-1', 'hmac-sha-256'].indexOf(credentials.algorithm) === -1) { | ||
if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { | ||
return callback(Err.internal('Unknown algorithm'), credentials, attributes.ext); | ||
@@ -209,3 +195,3 @@ } | ||
var mac = exports.calculateMAC(credentials.key, credentials.algorithm, attributes.ts, attributes.nonce, req.method, req.url, host, port, attributes.ext); | ||
var mac = Crypto.calculateMAC(credentials.key, credentials.algorithm, attributes.ts, attributes.nonce, req.method, req.url, host.name, host.port, attributes.ext); | ||
if (!Utils.fixedTimeComparison(mac, attributes.mac)) { | ||
@@ -231,38 +217,2 @@ return callback(Err.unauthorized('Bad mac'), credentials, attributes.ext); | ||
// Calculate the request MAC | ||
exports.calculateMAC = function (key, algorithm, timestamp, nonce, method, uri, host, port, ext) { | ||
// Parse request URI | ||
var url = URL.parse(uri); | ||
// Construct normalized req string | ||
var normalized = timestamp + '\n' + | ||
nonce + '\n' + | ||
method.toUpperCase() + '\n' + | ||
url.pathname + (url.search || '') + '\n' + | ||
host.toLowerCase() + '\n' + | ||
port + '\n' + | ||
(ext || '') + '\n'; | ||
// Lookup hash function | ||
var hashMethod = ''; | ||
switch (algorithm) { | ||
case 'hmac-sha-1': hashMethod = 'sha1'; break; | ||
case 'hmac-sha-256': hashMethod = 'sha256'; break; | ||
default: return ''; | ||
} | ||
// MAC normalized req string | ||
var hmac = Crypto.createHmac(hashMethod, key).update(normalized); | ||
var digest = hmac.digest('base64'); | ||
return digest; | ||
}; | ||
// Generate an Authorization header for a given request | ||
@@ -272,6 +222,14 @@ | ||
* credentials is an object with the following keys: 'id, 'key', 'algorithm'. | ||
* options is an object with the following optional keys: 'ext', 'timestamp', 'nonce', 'localtimeOffsetMsec' | ||
*/ | ||
exports.getAuthorizationHeader = function (credentials, method, uri, host, port, ext, timestamp, nonce) { | ||
exports.getAuthorizationHeader = function (credentials, method, uri, host, port, options) { | ||
options = options || {}; | ||
options.ext = (options.ext === null || options.ext === undefined ? '' : options.ext); // Zero is valid value | ||
// Application time | ||
var now = Date.now() + (options.localtimeOffsetMsec || 0); | ||
// Check request | ||
@@ -289,5 +247,5 @@ | ||
timestamp = timestamp || Math.floor(((new Date()).getTime() / 1000)); | ||
nonce = nonce || Utils.randomString(6); | ||
var mac = exports.calculateMAC(credentials.key, credentials.algorithm, timestamp, nonce, method, uri, host, port, ext); | ||
var timestamp = options.timestamp || Math.floor(now / 1000); | ||
var nonce = options.nonce || Utils.randomString(6); | ||
var mac = Crypto.calculateMAC(credentials.key, credentials.algorithm, timestamp, nonce, method, uri, host, port, options.ext); | ||
@@ -300,3 +258,3 @@ if (!mac) { | ||
var header = 'Hawk id="' + credentials.id + '", ts="' + timestamp + '", nonce="' + nonce + (ext ? '", ext="' + Utils.escapeHeaderAttribute (ext) : '') + '", mac="' + mac + '"'; | ||
var header = 'Hawk id="' + credentials.id + '", ts="' + timestamp + '", nonce="' + nonce + (options.ext ? '", ext="' + Utils.escapeHeaderAttribute (options.ext) : '') + '", mac="' + mac + '"'; | ||
return header; | ||
@@ -303,0 +261,0 @@ }; |
@@ -68,1 +68,27 @@ // Load modules | ||
// Extract host and port from request | ||
exports.parseHost = function (req, hostHeaderName) { | ||
hostHeaderName = (hostHeaderName ? hostHeaderName.toLowerCase() : 'host'); | ||
var hostHeader = req.headers[hostHeaderName]; | ||
if (!hostHeader) { | ||
return null; | ||
} | ||
var hostHeaderRegex = /^(?:(?:\r\n)?[\t ])*([^:]+)(?::(\d+))?(?:(?:\r\n)?[\t ])*$/; // Does not support IPv6 | ||
var hostParts = hostHeader.match(hostHeaderRegex); | ||
if (!hostParts || | ||
hostParts.length !== 3 || | ||
!hostParts[1]) { | ||
return null; | ||
} | ||
return { | ||
name: hostParts[1], | ||
port: (hostParts[2] ? hostParts[2] : (req.connection && req.connection.encrypted ? 443 : 80)) | ||
}; | ||
}; |
{ | ||
"name": "hawk", | ||
"description": "HTTP Hawk Authentication Scheme", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"author": "Eran Hammer <eran@hueniverse.com> (http://hueniverse.com)", | ||
@@ -6,0 +6,0 @@ "contributors": [], |
108
README.md
@@ -6,3 +6,3 @@ ![hawk Logo](https://raw.github.com/hueniverse/hawk/master/images/hawk.png) | ||
Current version: **0.2.0** | ||
Current version: **0.3.0** | ||
@@ -18,3 +18,15 @@ [![Build Status](https://secure.travis-ci.org/hueniverse/hawk.png)](http://travis-ci.org/hueniverse/hawk) | ||
<p></p> | ||
- [**Single URI Authorization**](#single-uri-authorization) | ||
- [Usage Example](#bewit-usage-example) | ||
<p></p> | ||
- [**Security Considerations**](#security-considerations) | ||
- [MAC Keys Transmission](#mac-keys-transmission) | ||
- [Confidentiality of Requests](#confidentiality-of-requests) | ||
- [Spoofing by Counterfeit Servers](#spoofing-by-counterfeit-servers) | ||
- [Plaintext Storage of Credentials](#plaintext-storage-of-credentials) | ||
- [Entropy of Keys](#entropy-of-keys) | ||
- [Coverage Limitations](#coverage-limitations) | ||
- [Future Time Manipulation](#future-time-manipulation) | ||
- [Client Clock Poisoning](#client-clock-poisoning) | ||
- [Bewit Limitations](#bewit-limitations) | ||
<p></p> | ||
@@ -51,3 +63,6 @@ - [**Frequently Asked Questions**](#frequently-asked-questions) | ||
In addition, **Hawk** supports a method for granting third-parties temporary access to individual resources using | ||
a query parameter called _bewit_ (leather straps used to attach a tracking device to the leg of a hawk). | ||
## Time Synchronization | ||
@@ -100,6 +115,6 @@ | ||
Hawk.authenticate(req, credentialsFunc, {}, function (err, isAuthenticated, credentials, ext) { | ||
Hawk.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) { | ||
res.writeHead(isAuthenticated ? 200 : 401, { 'Content-Type': 'text/plain' }); | ||
res.end(isAuthenticated ? 'Hello ' + credentials.user : 'Shoosh!'); | ||
res.writeHead(!err ? 200 : 401, { 'Content-Type': 'text/plain' }); | ||
res.end(!err ? 'Hello ' + credentials.user : 'Shoosh!'); | ||
}); | ||
@@ -132,3 +147,3 @@ }; | ||
headers: { | ||
authorization: Hawk.getAuthorizationHeader(credentials, 'GET', '/resource/1?b=1&a=2', 'example.com', 8000, 'some-app-data') | ||
authorization: Hawk.getAuthorizationHeader(credentials, 'GET', '/resource/1?b=1&a=2', 'example.com', 8000, { ext: 'some-app-data' }) | ||
} | ||
@@ -202,2 +217,75 @@ }; | ||
# Single URI Authorization | ||
There are often cases in which limited and short-term access is granted to protected resource to a third party which does not | ||
have access to the shared credentials. For example, displaying a protected image on a web page accessed by anyone. **Hawk** | ||
provides limited support for such URIs in the form of a _bewit_ - a URI query parameter appended to the request URI which contains | ||
the necessary credentials to authenticate the request. | ||
Because of the significant security risks involved in issuing such access, bewit usage is purposely limited to only GET requests | ||
and for a finite period of time. Both the client and server can issue bewit credentials, however, the server should not use the same | ||
credentials as the client to maintain clear traceability as to who issued which credentials. | ||
In order to simplify implementation, bewit credentials do not support single-use policy and can be replayed multiple times within | ||
the granted access timeframe. | ||
## Bewit Usage Example | ||
Server code: | ||
```javascript | ||
var Http = require('http'); | ||
var Hawk = require('hawk'); | ||
// Credentials lookup function | ||
var credentialsFunc = function (id, callback) { | ||
var credentials = { | ||
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', | ||
algorithm: 'hmac-sha-256' | ||
}; | ||
return callback(null, credentials); | ||
}; | ||
// Create HTTP server | ||
var handler = function (req, res) { | ||
Hawk.uri.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) { | ||
res.writeHead(!err ? 200 : 401, { 'Content-Type': 'text/plain' }); | ||
res.end(!err ? 'Access granted' : 'Shoosh!'); | ||
}); | ||
}; | ||
Http.createServer(handler).listen(8000, 'example.com'); | ||
``` | ||
Bewit code generation: | ||
```javascript | ||
var Request = require('request'); | ||
var Hawk = require('hawk'); | ||
// Client credentials | ||
var credentials = { | ||
id: 'dh37fgj492je', | ||
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', | ||
algorithm: 'hmac-sha-256' | ||
} | ||
// Generate bewit | ||
var duration = 60 * 5; // 5 Minutes | ||
var bewit = Hawk.uri.getBewit(credentials, '/resource/1?b=1&a=2', 'example.com', 8080, duration, { ext: 'some-app-data' }); | ||
var uri = 'http://example.com:8000/resource/1?b=1&a=2' + '&bewit=' + bewit; | ||
``` | ||
# Security Considerations | ||
@@ -285,3 +373,11 @@ | ||
### Bewit Limitations | ||
Special care must be taken when issuing bewit credentials to third parties. Bewit credentials are valid until expiration and cannot | ||
be revoked or limited without using other means. Whatever resource they grant access to will be completely exposed to anyone with | ||
access to the bewit credentials which act as bearer credentials for that particular resource. While bewit usage is limited to GET | ||
requests only and therefore cannot be used to perform transactions or change server state, it can still be used to expose private | ||
and sensitive information. | ||
# Frequently Asked Questions | ||
@@ -355,3 +451,3 @@ | ||
**Hawk** is a derivative work of the [HTTP MAC Authentication Scheme](http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05) proposal | ||
Co-authored by Ben Adida, Adam Barth, and Eran Hammer, which in turn was based on the OAuth 1.0 community specification. | ||
co-authored by Ben Adida, Adam Barth, and Eran Hammer, which in turn was based on the OAuth 1.0 community specification. | ||
@@ -358,0 +454,0 @@ Special thanks to Ben Laurie for his always insightful feedback and advice. |
@@ -43,3 +43,3 @@ // Load modules | ||
req.headers.authorization = Hawk.getAuthorizationHeader(credentials, req.method, req.url, 'example.com', 8080, 'some-app-data'); | ||
req.headers.authorization = Hawk.getAuthorizationHeader(credentials, req.method, req.url, 'example.com', 8080, { ext: 'some-app-data' }); | ||
@@ -68,3 +68,3 @@ Hawk.authenticate(req, credentialsFunc, {}, function (err, credentials, ext) { | ||
req.headers.authorization = Hawk.getAuthorizationHeader(credentials, req.method, req.url, 'example.com', 8080, 'some-app-data'); | ||
req.headers.authorization = Hawk.getAuthorizationHeader(credentials, req.method, req.url, 'example.com', 8080, { ext: 'some-app-data' }); | ||
req.url = '/something/else'; | ||
@@ -255,3 +255,3 @@ | ||
expect(err).to.exist; | ||
expect(err.toResponse().payload.message).to.equal('Missing Host header'); | ||
expect(err.toResponse().payload.message).to.equal('Invalid Host header'); | ||
done(); | ||
@@ -465,3 +465,3 @@ }); | ||
expect(err).to.exist; | ||
expect(err.toResponse().payload.message).to.equal('Bad Host header'); | ||
expect(err.toResponse().payload.message).to.equal('Invalid Host header'); | ||
done(); | ||
@@ -514,3 +514,3 @@ }); | ||
expect(err).to.exist; | ||
expect(err.toResponse().payload.message).to.equal('Missing credentials'); | ||
expect(err.toResponse().payload.message).to.equal('Unknown credentials'); | ||
done(); | ||
@@ -612,11 +612,2 @@ }); | ||
describe('#calculateMAC', function () { | ||
it('should return an empty value on unknown algorithm', function (done) { | ||
expect(Hawk.calculateMAC('dasdfasdf', 'hmac-sha-0', Date.now() / 1000, 'k3k4j5', 'GET', '/resource/something', 'example.com', 8080)).to.equal(''); | ||
done(); | ||
}); | ||
}); | ||
describe('#getAuthorizationHeader', function () { | ||
@@ -632,3 +623,3 @@ | ||
var header = Hawk.getAuthorizationHeader(credentials, 'POST', '/somewhere/over/the/rainbow', 'example.net', 443, 'Bazinga!', 1353809207, 'Ygvqdz'); | ||
var header = Hawk.getAuthorizationHeader(credentials, 'POST', '/somewhere/over/the/rainbow', 'example.net', 443, { ext: 'Bazinga!', timestamp: 1353809207, nonce: 'Ygvqdz' }); | ||
expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="Bazinga!", mac="qSK1cZEkqPwE2ttBX8QSXxO+NE3epFMu4tyVpGKjdnU="'); | ||
@@ -645,3 +636,3 @@ done(); | ||
var header = Hawk.getAuthorizationHeader(credentials, 'POST', '/somewhere/over/the/rainbow', 'example.net', 443, 'Bazinga!', 1353809207); | ||
var header = Hawk.getAuthorizationHeader(credentials, 'POST', '/somewhere/over/the/rainbow', 'example.net', 443, { ext: 'Bazinga!', timestamp: 1353809207 }); | ||
expect(header).to.equal(''); | ||
@@ -659,3 +650,3 @@ done(); | ||
var header = Hawk.getAuthorizationHeader(credentials, 'POST', '/somewhere/over/the/rainbow', 'example.net', 443, 'Bazinga!', 1353809207); | ||
var header = Hawk.getAuthorizationHeader(credentials, 'POST', '/somewhere/over/the/rainbow', 'example.net', 443, { ext: 'Bazinga!', timestamp: 1353809207 }); | ||
expect(header).to.equal(''); | ||
@@ -662,0 +653,0 @@ done(); |
@@ -46,3 +46,3 @@ // Load modules | ||
expect(t2 - t1).to.be.within(-1, 1); | ||
expect(t2 - t1).to.be.within(-2, 2); | ||
done(); | ||
@@ -49,0 +49,0 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
94573
18
1371
449
5