hmac-authentication
Advanced tools
Comparing version 0.0.2 to 1.0.0
127
index.js
@@ -5,14 +5,30 @@ /* jshint node: true */ | ||
var bufferEq = require('buffer-equal-constant-time'); | ||
var crypto = require('crypto'); | ||
var url = require('url'); | ||
var exports = module.exports = {}; | ||
module.exports = HmacAuth; | ||
HmacAuth.AuthenticationError = AuthenticationError; | ||
exports.NO_SIGNATURE = 1; | ||
exports.INVALID_FORMAT = 2; | ||
exports.UNSUPPORTED_ALGORITHM = 3; | ||
exports.MATCH = 4; | ||
exports.MISMATCH = 5; | ||
function HmacAuth(digestName, key, signatureHeader, headers) { | ||
this.digestName = digestName.toLowerCase(); | ||
try { | ||
crypto.createHash(digestName); | ||
} catch (_) { | ||
throw new Error( | ||
'HMAC authentication digest is not supported: ' + digestName); | ||
} | ||
this.key = key; | ||
this.signatureHeader = signatureHeader.toLowerCase(); | ||
this.headers = headers.map(function(h) { return h.toLowerCase(); }); | ||
} | ||
HmacAuth.NO_SIGNATURE = 1; | ||
HmacAuth.INVALID_FORMAT = 2; | ||
HmacAuth.UNSUPPORTED_ALGORITHM = 3; | ||
HmacAuth.MATCH = 4; | ||
HmacAuth.MISMATCH = 5; | ||
var resultStrings = [ | ||
'', | ||
'NO_SIGNATURE', | ||
@@ -25,30 +41,54 @@ 'INVALID_FORMAT', | ||
exports.resultCodeToString = function(code) { | ||
if (code < 1 || code >= resultStrings.length) { return; } | ||
return resultStrings[code]; | ||
HmacAuth.resultCodeToString = function(code) { | ||
return resultStrings[code - 1]; | ||
}; | ||
function signedHeaders(req, headers) { | ||
return headers.map(function(header) { return req.get(header) || ''; }); | ||
return headers.map(function(header) { | ||
var value = req.headers[header]; | ||
if (Array.isArray(value)) { value = value.join(','); } | ||
return value || ''; | ||
}); | ||
} | ||
exports.stringToSign = function(req, headers) { | ||
HmacAuth.prototype.stringToSign = function(req) { | ||
var parsedUrl = url.parse(req.url); | ||
var hashUrl = parsedUrl.path + (parsedUrl.hash || ''); | ||
return [ | ||
req.method, signedHeaders(req, headers).join('\n'), req.url | ||
].join('\n'); | ||
req.method, signedHeaders(req, this.headers).join('\n'), hashUrl | ||
].join('\n') + '\n'; | ||
}; | ||
exports.requestSignature = function( | ||
req, rawBody, digestName, headers, secretKey) { | ||
var hmac = crypto.createHmac(digestName, secretKey); | ||
hmac.update(exports.stringToSign(req, headers)); | ||
HmacAuth.prototype.signRequest = function(req, rawBody) { | ||
req.headers[this.signatureHeader] = this.requestSignature(req, rawBody); | ||
}; | ||
HmacAuth.prototype.requestSignature = function(req, rawBody) { | ||
return requestSignature(this, req, rawBody, this.digestName); | ||
}; | ||
function requestSignature(auth, req, rawBody, digestName) { | ||
var hmac = crypto.createHmac(digestName, auth.key); | ||
hmac.update(auth.stringToSign(req)); | ||
hmac.update(rawBody || ''); | ||
return digestName + ' ' + hmac.digest('base64'); | ||
} | ||
HmacAuth.prototype.signatureFromHeader = function(req) { | ||
return req.headers[this.signatureHeader]; | ||
}; | ||
exports.validateRequest = function(req, rawBody, headers, secretKey) { | ||
var header = req.get('Gap-Signature'); | ||
if (!header) { return [exports.NO_SIGNATURE]; } | ||
// Replace bufferEq() once https://github.com/nodejs/node/issues/3043 is | ||
// resolved and the standard library implementation is available. | ||
function compareSignatures(lhs, rhs) { | ||
var lbuf = new Buffer(lhs); | ||
var rbuf = new Buffer(rhs); | ||
return bufferEq(lbuf, rbuf) ? HmacAuth.MATCH : HmacAuth.MISMATCH; | ||
} | ||
HmacAuth.prototype.authenticateRequest = function(req, rawBody) { | ||
var header = this.signatureFromHeader(req); | ||
if (!header) { return [HmacAuth.NO_SIGNATURE]; } | ||
var components = header.split(' '); | ||
if (components.length != 2) { return [exports.INVALID_FORMAT, header]; } | ||
if (components.length !== 2) { return [HmacAuth.INVALID_FORMAT, header]; } | ||
var digestName = components[0]; | ||
@@ -58,17 +98,16 @@ try { | ||
} catch (e) { | ||
return [exports.UNSUPPORTED_ALGORITHM, header]; | ||
return [HmacAuth.UNSUPPORTED_ALGORITHM, header]; | ||
} | ||
var computed = exports.requestSignature( | ||
req, rawBody, digestName, headers, secretKey); | ||
var result = (header == computed) ? exports.MATCH : exports.MISMATCH; | ||
return [result, header, computed]; | ||
var computed = requestSignature(this, req, rawBody, digestName); | ||
return [compareSignatures(header, computed), header, computed]; | ||
}; | ||
function ValidationError(result, header, computed) { | ||
this.name = 'ValidationError'; | ||
function AuthenticationError(signatureHeader, result, header, computed) { | ||
this.name = 'AuthenticationError'; | ||
this.signatureHeader = signatureHeader; | ||
this.result = result; | ||
this.header = header; | ||
this.computed = computed; | ||
this.message = 'hmac signature request validation failed: ' + | ||
exports.resultCodeToString(result); | ||
this.message = signatureHeader + ' authentication failed: ' + | ||
HmacAuth.resultCodeToString(result); | ||
if (header) { this.message += ' header: "' + header + '"'; } | ||
@@ -78,19 +117,23 @@ if (computed) { this.message += ' computed: "' + computed + '"'; } | ||
} | ||
ValidationError.prototype = Object.create(Error.prototype); | ||
ValidationError.prototype.constructor = ValidationError; | ||
exports.ValidationError = ValidationError; | ||
AuthenticationError.prototype = Object.create(Error.prototype); | ||
AuthenticationError.prototype.constructor = AuthenticationError; | ||
exports.middlewareValidator = function(headers, secretKey) { | ||
HmacAuth.middlewareAuthenticator = function( | ||
secretKey, signatureHeader, headers) { | ||
// Since the object is only used for authentication, the digestName can be | ||
// anything valid. The actual digest function used during authentication | ||
// depends on the digest name used as a prefix to the signature header. | ||
var auth = new HmacAuth('sha1', secretKey, signatureHeader, headers); | ||
return function(req, res, buf, encoding) { | ||
var rawBody = buf.toString(encoding); | ||
var validationResult = exports.validateRequest( | ||
req, rawBody, headers, secretKey); | ||
var result = validationResult[0]; | ||
var authenticationResult = auth.authenticateRequest(req, rawBody); | ||
var result = authenticationResult[0]; | ||
if (result != exports.MATCH) { | ||
var header = validationResult[1]; | ||
var computed = validationResult[2]; | ||
throw new ValidationError(result, header, computed); | ||
if (result != HmacAuth.MATCH) { | ||
var header = authenticationResult[1]; | ||
var computed = authenticationResult[2]; | ||
throw new AuthenticationError(signatureHeader, result, header, computed); | ||
} | ||
}; | ||
}; |
{ | ||
"name": "hmac-authentication", | ||
"version": "0.0.2", | ||
"version": "1.0.0", | ||
"description": "Signs and validates HTTP requests based on a shared-secret HMAC signature", | ||
@@ -31,3 +31,6 @@ "main": "index.js", | ||
"node-mocks-http": "^1.4.4" | ||
}, | ||
"dependencies": { | ||
"buffer-equal-constant-time": "^1.0.1" | ||
} | ||
} |
# hmac-authentication npm | ||
Signs and validates HTTP requests based on a shared-secret HMAC signature. | ||
Signs and authenticates HTTP requests based on a shared-secret HMAC signature. | ||
Developed in parallel with the following packages for other languages: | ||
- Go: [github.com/18F/hmacauth](https://github.com/18F/hmacauth/) | ||
- Ruby: [hmac_authentication](https://rubygems.org/gems/hmac_authentication) | ||
## Installation | ||
@@ -11,7 +15,8 @@ | ||
## Validating incoming requests | ||
## Authenticating incoming requests | ||
Assuming you're using [Express](https://www.npmjs.com/package/express), during | ||
initialization of your application, where `config.headers` is a list of | ||
headers factored into the signature and `config.secretKey` is the shared | ||
initialization of your application, where `config.signatureHeader` identifies | ||
the header containing the message signature, `config.headers` is a list of | ||
headers factored into the signature, and `config.secretKey` is the shared | ||
secret between your application and the service making the request: | ||
@@ -22,8 +27,9 @@ | ||
var bodyParser = require('bodyParser'); | ||
var hmacAuthentication = require('hmac-authentication'); | ||
var HmacAuth = require('hmac-authentication'); | ||
var config = require('./config.json'); | ||
function doLaunch(config) { | ||
var middlewareOptions = { | ||
verify: hmacAuthentication.middlewareValidator( | ||
config.headers, config.secretKey) | ||
verify: HmacAuth.middlewareAuthenticator( | ||
config.secretKey, config.signatureHeader, config.headers) | ||
}; | ||
@@ -37,11 +43,41 @@ var server = express(); | ||
If you're not using Express, you can use the function `validateRequest(req, | ||
rawBody, headers, secretKey)` directly, where `rawBody` has already been | ||
converted to a string. | ||
If you're not using Express, you can use something similar to the following: | ||
```js | ||
var HmacAuth = require('hmac-authentication'); | ||
var config = require('./config.json'); | ||
// When only used for authentication, it doesn't matter what the first | ||
// argument is, because the hash algorithm used for authentication will be | ||
// parsed from the incoming request signature header. | ||
var auth = new HmacAuth( | ||
'sha1', config.secretKey, config.signatureHeader, config.headers); | ||
// rawBody must be a string. | ||
function requestHandler(req, rawBody) { | ||
var authenticationResult = auth.authenticateRequest(req, rawBody); | ||
if (authenticationResult[0] != HmacAuth.MATCH) { | ||
// Handle authentication failure... | ||
} | ||
} | ||
``` | ||
## Signing outgoing requests | ||
Call `requestSignature(request, rawBody, digestName, headers, secretKey)` to | ||
sign a request before sending. `rawBody` and `digestName` must be strings. | ||
Do something similar to the following. `rawBody` must be a string. | ||
```js | ||
var HmacAuth = require('hmac-authentication'); | ||
var config = require('./config.json'); | ||
var auth = new HmacAuth( | ||
config.digestName, config.secretKey, config.signatureHeader, config.headers); | ||
function makeRequest(req, rawBody) { | ||
// Prepare request... | ||
auth.signRequest(req, rawBody); | ||
} | ||
``` | ||
## Public domain | ||
@@ -48,0 +84,0 @@ |
@@ -8,3 +8,3 @@ /* jshint node: true */ | ||
var httpMocks = require('node-mocks-http'); | ||
var validator = require('../index'); | ||
var HmacAuth = require('../index'); | ||
@@ -29,18 +29,49 @@ var expect = chai.expect; | ||
var auth = new HmacAuth('SHA1', 'foobar', 'GAP-Signature', HEADERS); | ||
describe('HmacAuth constructor', function() { | ||
it('should lowercase the hash function and all header names', function() { | ||
expect(auth.digestName).to.eql('sha1'); | ||
expect(auth.key).to.eql('foobar'); | ||
expect(auth.signatureHeader).to.eql('gap-signature'); | ||
expect(auth.headers).to.eql([ | ||
'content-length', | ||
'content-md5', | ||
'content-type', | ||
'date', | ||
'authorization', | ||
'x-forwarded-user', | ||
'x-forwarded-email', | ||
'x-forwarded-access-token', | ||
'cookie', | ||
'gap-auth' | ||
]); | ||
}); | ||
it('should throw if the hash function is not supported', function() { | ||
var bogusAuth; | ||
var f = function() { | ||
bogusAuth = new HmacAuth('bogus', 'foobar', 'GAP-Signature', HEADERS); | ||
}; | ||
expect(f).to.throw( | ||
Error, 'HMAC authentication digest is not supported: bogus'); | ||
}); | ||
}); | ||
describe('resultCodeToString', function() { | ||
it('should return undefined for out-of-range values', function() { | ||
expect(validator.resultCodeToString(0)).to.be.undefined; | ||
expect(validator.resultCodeToString(6)).to.be.undefined; | ||
expect(HmacAuth.resultCodeToString(0)).to.be.undefined; | ||
expect(HmacAuth.resultCodeToString(6)).to.be.undefined; | ||
}); | ||
it('should return the correct matching strings', function() { | ||
expect(validator.resultCodeToString(validator.NO_SIGNATURE)) | ||
expect(HmacAuth.resultCodeToString(HmacAuth.NO_SIGNATURE)) | ||
.to.eql('NO_SIGNATURE'); | ||
expect(validator.resultCodeToString(validator.INVALID_FORMAT)) | ||
expect(HmacAuth.resultCodeToString(HmacAuth.INVALID_FORMAT)) | ||
.to.eql('INVALID_FORMAT'); | ||
expect(validator.resultCodeToString(validator.UNSUPPORTED_ALGORITHM)) | ||
expect(HmacAuth.resultCodeToString(HmacAuth.UNSUPPORTED_ALGORITHM)) | ||
.to.eql('UNSUPPORTED_ALGORITHM'); | ||
expect(validator.resultCodeToString(validator.MATCH)) | ||
expect(HmacAuth.resultCodeToString(HmacAuth.MATCH)) | ||
.to.eql('MATCH'); | ||
expect(validator.resultCodeToString(validator.MISMATCH)) | ||
expect(HmacAuth.resultCodeToString(HmacAuth.MISMATCH)) | ||
.to.eql('MISMATCH'); | ||
@@ -71,3 +102,3 @@ }); | ||
expect(validator.stringToSign(req, HEADERS)).to.eql( | ||
expect(auth.stringToSign(req)).to.eql( | ||
['POST', | ||
@@ -85,12 +116,11 @@ payload.length.toString(), | ||
'/foo/bar' | ||
].join('\n')); | ||
expect( | ||
validator.requestSignature(req, payload, 'sha1', HEADERS, 'foobar')) | ||
.to.eql('sha1 722UbRYfC6MnjtIxqEJMDPrW2mk='); | ||
].join('\n') + '\n'); | ||
expect(auth.requestSignature(req, payload)) | ||
.to.eql('sha1 K4IrVDtMCRwwW8Oms0VyZWMjXHI='); | ||
}); | ||
it('should correctly sign a GET request', function() { | ||
it('should correctly sign a GET request with a complete URL', function() { | ||
var httpOptions = { | ||
method: 'GET', | ||
url: '/foo/bar', | ||
url: 'http://localhost/foo/bar?baz=quux%2Fxyzzy#plugh', | ||
headers: { | ||
@@ -104,3 +134,3 @@ 'Date': '2015-09-29', | ||
expect(validator.stringToSign(req, HEADERS)).to.eql( | ||
expect(auth.stringToSign(req)).to.eql( | ||
['GET', | ||
@@ -117,11 +147,40 @@ '', | ||
'mbland', | ||
'/foo/bar?baz=quux%2Fxyzzy#plugh' | ||
].join('\n') + '\n'); | ||
expect(auth.requestSignature(req, undefined)) | ||
.to.eql('sha1 ih5Jce9nsltry63rR4ImNz2hdnk='); | ||
}); | ||
it('should correctly sign a GET w/ multiple values for header', function() { | ||
var httpOptions = { | ||
method: 'GET', | ||
url: '/foo/bar', | ||
headers: { | ||
'Date': '2015-09-29', | ||
'Cookie': ['foo', 'bar', 'baz=quux'], | ||
'Gap-Auth': 'mbland' | ||
} | ||
}; | ||
var req = httpMocks.createRequest(httpOptions); | ||
expect(auth.stringToSign(req)).to.eql( | ||
['GET', | ||
'', | ||
'', | ||
'', | ||
'2015-09-29', | ||
'', | ||
'', | ||
'', | ||
'', | ||
'foo,bar,baz=quux', | ||
'mbland', | ||
'/foo/bar' | ||
].join('\n')); | ||
expect( | ||
validator.requestSignature(req, undefined, 'sha1', HEADERS, 'foobar')) | ||
.to.eql('sha1 JBQJcmSTteQyHZXFUA9glis9BIk='); | ||
].join('\n') + '\n'); | ||
expect(auth.requestSignature(req, undefined)) | ||
.to.eql('sha1 JlRkes1X+qq3Bgc/GcRyLos+4aI='); | ||
}); | ||
}); | ||
describe('validateRequest and middlewareValidator', function() { | ||
describe('authenticateRequest and middlewareAuthenticator', function() { | ||
var createRequest = function(headerSignature) { | ||
@@ -143,27 +202,28 @@ var httpOptions = { | ||
var validateRequest = function(request, secretKey) { | ||
var validate = validator.middlewareValidator(HEADERS, secretKey); | ||
validate(request, undefined, new Buffer(0), 'utf-8'); | ||
var authenticateRequest = function(request, secretKey) { | ||
var authenticate = HmacAuth.middlewareAuthenticator( | ||
secretKey, 'Gap-Signature', HEADERS); | ||
authenticate(request, undefined, new Buffer(0), 'utf-8'); | ||
}; | ||
it('should throw ValidationError with NO_SIGNATURE', function() { | ||
var f = function() { validateRequest(createRequest(), 'foobar'); }; | ||
expect(f).to.throw(validator.ValidationError, 'failed: NO_SIGNATURE'); | ||
it('should throw AuthenticationError with NO_SIGNATURE', function() { | ||
var f = function() { authenticateRequest(createRequest(), 'foobar'); }; | ||
expect(f).to.throw(HmacAuth.AuthenticationError, 'failed: NO_SIGNATURE'); | ||
}); | ||
it('should throw ValidationError with INVALID_FORMAT', function() { | ||
it('should throw AuthenticationError with INVALID_FORMAT', function() { | ||
var badValue = 'should be algorithm and digest value'; | ||
var f = function() { | ||
var request = createRequest(badValue); | ||
validateRequest(request, 'foobar'); | ||
authenticateRequest(request, 'foobar'); | ||
}; | ||
expect(f).to.throw( | ||
validator.ValidationError, | ||
HmacAuth.AuthenticationError, | ||
'failed: INVALID_FORMAT header: "' + badValue + '"'); | ||
}); | ||
it('should throw ValidationError with UNSUPPORTED_ALGORITHM', function() { | ||
it('should throw AuthenticationError with UNSUPPORTED_ALGORITHM', | ||
function() { | ||
var request = createRequest(); | ||
var validSignature = validator.requestSignature( | ||
request, null, 'sha1', HEADERS, 'foobar'); | ||
var validSignature = auth.requestSignature(request, null); | ||
var components = validSignature.split(' '); | ||
@@ -173,7 +233,7 @@ var signatureWithUnsupportedAlgorithm = 'unsupported ' + components[1]; | ||
var f = function() { | ||
validateRequest( | ||
authenticateRequest( | ||
createRequest(signatureWithUnsupportedAlgorithm), 'foobar'); | ||
}; | ||
expect(f).to.throw( | ||
validator.ValidationError, | ||
HmacAuth.AuthenticationError, | ||
'failed: UNSUPPORTED_ALGORITHM ' + | ||
@@ -183,13 +243,11 @@ 'header: "' + signatureWithUnsupportedAlgorithm + '"'); | ||
it('should validate the request with MATCH', function() { | ||
it('should authenticate the request with MATCH', function() { | ||
var request = createRequest(); | ||
var expectedSignature = validator.requestSignature( | ||
request, null, 'sha1', HEADERS, 'foobar'); | ||
request = createRequest(expectedSignature); | ||
validateRequest(request, 'foobar'); | ||
var expectedSignature = auth.requestSignature(request, null); | ||
auth.signRequest(request); | ||
authenticateRequest(request, 'foobar'); | ||
// If we reach this point the result was a MATCH. Call | ||
// validator.validateRequest() directly so we can inspect the values. | ||
var results = validator.validateRequest( | ||
request, undefined, HEADERS, 'foobar'); | ||
// auth.authenticateRequest() directly so we can inspect the values. | ||
var results = auth.authenticateRequest(request, undefined); | ||
var result = results[0]; | ||
@@ -199,3 +257,3 @@ var header = results[1]; | ||
expect(result).to.eql(validator.MATCH); | ||
expect(result).to.eql(HmacAuth.MATCH); | ||
expect(header).to.eql(expectedSignature); | ||
@@ -205,19 +263,17 @@ expect(computed).to.eql(expectedSignature); | ||
it('should throw ValidationError with MISMATCH', function() { | ||
it('should throw AuthenticationError with MISMATCH', function() { | ||
var request = createRequest(); | ||
var foobarSignature = validator.requestSignature( | ||
request, null, 'sha1', HEADERS, 'foobar'); | ||
var barbazSignature = validator.requestSignature( | ||
request, null, 'sha1', HEADERS, 'barbaz'); | ||
var barbazAuth = new HmacAuth('sha1', 'barbaz', 'Gap-Signature', HEADERS); | ||
var f = function() { | ||
validateRequest(createRequest(foobarSignature), 'barbaz'); | ||
auth.signRequest(request); | ||
authenticateRequest(request, 'barbaz'); | ||
}; | ||
expect(f).to.throw( | ||
validator.ValidationError, | ||
HmacAuth.AuthenticationError, | ||
'failed: MISMATCH ' + | ||
'header: "' + foobarSignature + '" ' + | ||
'computed: "' + barbazSignature + '"'); | ||
'header: "' + auth.requestSignature(request, null) + '" ' + | ||
'computed: "' + barbazAuth.requestSignature(request, null) + '"'); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
22880
11
373
0
89
1
+ Addedbuffer-equal-constant-time@1.0.1(transitive)