├── .eslintignore ├── .npmignore ├── images ├── hawk.png └── logo.png ├── client.js ├── .travis.yml ├── .gitignore ├── lib ├── index.js ├── crypto.js ├── utils.js ├── client.js ├── server.js └── browser.js ├── package.json ├── LICENSE ├── test ├── crypto.js ├── readme.js ├── utils.js ├── index.js ├── uri.js └── client.js ├── example └── usage.js ├── dist └── browser.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | !dist/** 4 | !client.js 5 | !.npmignore 6 | -------------------------------------------------------------------------------- /images/hawk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/hawk/master/images/hawk.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/hawk/master/images/logo.png -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./dist/browser'); 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | - "6" 6 | - "8" 7 | - "node" 8 | 9 | sudo: false 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | npm-debug.log 4 | dump.rdb 5 | node_modules 6 | results.tap 7 | results.xml 8 | config.json 9 | .DS_Store 10 | */.DS_Store 11 | */*/.DS_Store 12 | ._* 13 | */._* 14 | */*/._* 15 | coverage.* 16 | .settings 17 | package-lock.json 18 | 19 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Export sub-modules 4 | 5 | exports.error = exports.Error = require('boom'); 6 | exports.sntp = require('sntp'); 7 | 8 | exports.server = require('./server'); 9 | exports.client = require('./client'); 10 | exports.crypto = require('./crypto'); 11 | exports.utils = require('./utils'); 12 | 13 | exports.uri = { 14 | authenticate: exports.server.authenticateBewit, 15 | getBewit: exports.client.getBewit 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hawk", 3 | "description": "HTTP Hawk Authentication Scheme", 4 | "version": "6.0.2", 5 | "author": "Eran Hammer (http://hueniverse.com)", 6 | "repository": "git://github.com/hueniverse/hawk", 7 | "main": "lib/index.js", 8 | "browser": "dist/browser.js", 9 | "keywords": [ 10 | "http", 11 | "authentication", 12 | "scheme", 13 | "hawk" 14 | ], 15 | "engines": { 16 | "node": ">=4.5.0" 17 | }, 18 | "dependencies": { 19 | "hoek": "4.x.x", 20 | "boom": "4.x.x", 21 | "cryptiles": "3.x.x", 22 | "sntp": "2.x.x" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "^6.1.2", 26 | "babel-preset-es2015": "^6.1.2", 27 | "code": "4.x.x", 28 | "lab": "14.x.x" 29 | }, 30 | "babel": { 31 | "presets": [ 32 | "es2015" 33 | ] 34 | }, 35 | "scripts": { 36 | "build-client": "mkdir -p dist; babel lib/browser.js --out-file dist/browser.js", 37 | "prepublish": "npm run-script build-client", 38 | "test": "lab -a code -t 100 -L", 39 | "test-cov-html": "lab -a code -r html -o coverage.html" 40 | }, 41 | "license": "BSD-3-Clause" 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2017, Eran Hammer and Project contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of any contributors may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | * * * 27 | 28 | The complete list of contributors can be found at: https://github.com/hueniverse/hawk/graphs/contributors 29 | -------------------------------------------------------------------------------- /test/crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Hawk = require('../lib'); 7 | const Lab = require('lab'); 8 | 9 | 10 | // Declare internals 11 | 12 | const internals = {}; 13 | 14 | 15 | // Test shortcuts 16 | 17 | const lab = exports.lab = Lab.script(); 18 | const describe = lab.experiment; 19 | const it = lab.test; 20 | const expect = Code.expect; 21 | 22 | 23 | describe('Crypto', () => { 24 | 25 | describe('generateNormalizedString()', () => { 26 | 27 | it('should return a valid normalized string', (done) => { 28 | 29 | expect(Hawk.crypto.generateNormalizedString('header', { 30 | ts: 1357747017, 31 | nonce: 'k3k4j5', 32 | method: 'GET', 33 | resource: '/resource/something', 34 | host: 'example.com', 35 | port: 8080 36 | })).to.equal('hawk.1.header\n1357747017\nk3k4j5\nGET\n/resource/something\nexample.com\n8080\n\n\n'); 37 | 38 | done(); 39 | }); 40 | 41 | it('should return a valid normalized string (ext)', (done) => { 42 | 43 | expect(Hawk.crypto.generateNormalizedString('header', { 44 | ts: 1357747017, 45 | nonce: 'k3k4j5', 46 | method: 'GET', 47 | resource: '/resource/something', 48 | host: 'example.com', 49 | port: 8080, 50 | ext: 'this is some app data' 51 | })).to.equal('hawk.1.header\n1357747017\nk3k4j5\nGET\n/resource/something\nexample.com\n8080\n\nthis is some app data\n'); 52 | 53 | done(); 54 | }); 55 | 56 | it('should return a valid normalized string (payload + ext)', (done) => { 57 | 58 | expect(Hawk.crypto.generateNormalizedString('header', { 59 | ts: 1357747017, 60 | nonce: 'k3k4j5', 61 | method: 'GET', 62 | resource: '/resource/something', 63 | host: 'example.com', 64 | port: 8080, 65 | hash: 'U4MKKSmiVxk37JCCrAVIjV/OhB3y+NdwoCr6RShbVkE=', 66 | ext: 'this is some app data' 67 | })).to.equal('hawk.1.header\n1357747017\nk3k4j5\nGET\n/resource/something\nexample.com\n8080\nU4MKKSmiVxk37JCCrAVIjV/OhB3y+NdwoCr6RShbVkE=\nthis is some app data\n'); 68 | 69 | done(); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /example/usage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Http = require('http'); 6 | const Request = require('request'); 7 | const Hawk = require('../lib'); 8 | 9 | 10 | // Declare internals 11 | 12 | const internals = { 13 | credentials: { 14 | dh37fgj492je: { 15 | id: 'dh37fgj492je', // Required by Hawk.client.header 16 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 17 | algorithm: 'sha256', 18 | user: 'Steve' 19 | } 20 | } 21 | }; 22 | 23 | 24 | // Credentials lookup function 25 | 26 | const credentialsFunc = function (id, callback) { 27 | 28 | return callback(null, internals.credentials[id]); 29 | }; 30 | 31 | 32 | // Create HTTP server 33 | 34 | const handler = function (req, res) { 35 | 36 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials, artifacts) => { 37 | 38 | const payload = (!err ? 'Hello ' + credentials.user + ' ' + artifacts.ext : 'Shoosh!'); 39 | const headers = { 40 | 'Content-Type': 'text/plain', 41 | 'Server-Authorization': Hawk.server.header(credentials, artifacts, { payload, contentType: 'text/plain' }) 42 | }; 43 | 44 | res.writeHead(!err ? 200 : 401, headers); 45 | res.end(payload); 46 | }); 47 | }; 48 | 49 | Http.createServer(handler).listen(8000, '127.0.0.1'); 50 | 51 | 52 | // Send unauthenticated request 53 | 54 | Request('http://127.0.0.1:8000/resource/1?b=1&a=2', (err, response, body) => { 55 | 56 | if (err) { 57 | console.log(err); 58 | } 59 | 60 | console.log(response.statusCode + ': ' + body); 61 | }); 62 | 63 | 64 | // Send authenticated request 65 | 66 | credentialsFunc('dh37fgj492je', (err, credentials) => { 67 | 68 | if (err) { 69 | process.exit(1); 70 | } 71 | 72 | const header = Hawk.client.header('http://127.0.0.1:8000/resource/1?b=1&a=2', 'GET', { credentials, ext: 'and welcome!' }); 73 | const options = { 74 | uri: 'http://127.0.0.1:8000/resource/1?b=1&a=2', 75 | method: 'GET', 76 | headers: { 77 | authorization: header.field 78 | } 79 | }; 80 | 81 | Request(options, (err, response, body) => { 82 | 83 | if (err) { 84 | process.exit(1); 85 | } 86 | 87 | const isValid = Hawk.client.authenticate(response, credentials, header.artifacts, { payload: body }); 88 | console.log(response.statusCode + ': ' + body + (isValid ? ' (valid)' : ' (invalid)')); 89 | process.exit(0); 90 | }); 91 | }); 92 | 93 | -------------------------------------------------------------------------------- /test/readme.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Hawk = require('../lib'); 7 | const Hoek = require('hoek'); 8 | const Lab = require('lab'); 9 | 10 | 11 | // Declare internals 12 | 13 | const internals = {}; 14 | 15 | 16 | // Test shortcuts 17 | 18 | const lab = exports.lab = Lab.script(); 19 | const describe = lab.experiment; 20 | const it = lab.test; 21 | const expect = Code.expect; 22 | 23 | 24 | describe('README', () => { 25 | 26 | describe('core', () => { 27 | 28 | const credentials = { 29 | id: 'dh37fgj492je', 30 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 31 | algorithm: 'sha256' 32 | }; 33 | 34 | const options = { 35 | credentials, 36 | timestamp: 1353832234, 37 | nonce: 'j4h3g2', 38 | ext: 'some-app-ext-data' 39 | }; 40 | 41 | it('should generate a header protocol example', (done) => { 42 | 43 | const header = Hawk.client.header('http://example.com:8000/resource/1?b=1&a=2', 'GET', options).field; 44 | 45 | expect(header).to.equal('Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="'); 46 | done(); 47 | }); 48 | 49 | it('should generate a normalized string protocol example', (done) => { 50 | 51 | const normalized = Hawk.crypto.generateNormalizedString('header', { 52 | credentials, 53 | ts: options.timestamp, 54 | nonce: options.nonce, 55 | method: 'GET', 56 | resource: '/resource?a=1&b=2', 57 | host: 'example.com', 58 | port: 8000, 59 | ext: options.ext 60 | }); 61 | 62 | expect(normalized).to.equal('hawk.1.header\n1353832234\nj4h3g2\nGET\n/resource?a=1&b=2\nexample.com\n8000\n\nsome-app-ext-data\n'); 63 | done(); 64 | }); 65 | 66 | const payloadOptions = Hoek.clone(options); 67 | payloadOptions.payload = 'Thank you for flying Hawk'; 68 | payloadOptions.contentType = 'text/plain'; 69 | 70 | it('should generate a header protocol example (with payload)', (done) => { 71 | 72 | const header = Hawk.client.header('http://example.com:8000/resource/1?b=1&a=2', 'POST', payloadOptions).field; 73 | 74 | expect(header).to.equal('Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", hash="Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=", ext="some-app-ext-data", mac="aSe1DERmZuRl3pI36/9BdZmnErTw3sNzOOAUlfeKjVw="'); 75 | done(); 76 | }); 77 | 78 | it('should generate a normalized string protocol example (with payload)', (done) => { 79 | 80 | const normalized = Hawk.crypto.generateNormalizedString('header', { 81 | credentials, 82 | ts: options.timestamp, 83 | nonce: options.nonce, 84 | method: 'POST', 85 | resource: '/resource?a=1&b=2', 86 | host: 'example.com', 87 | port: 8000, 88 | hash: Hawk.crypto.calculatePayloadHash(payloadOptions.payload, credentials.algorithm, payloadOptions.contentType), 89 | ext: options.ext 90 | }); 91 | 92 | expect(normalized).to.equal('hawk.1.header\n1353832234\nj4h3g2\nPOST\n/resource?a=1&b=2\nexample.com\n8000\nYi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=\nsome-app-ext-data\n'); 93 | done(); 94 | }); 95 | }); 96 | }); 97 | 98 | -------------------------------------------------------------------------------- /lib/crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Crypto = require('crypto'); 6 | const Url = require('url'); 7 | const Utils = require('./utils'); 8 | 9 | 10 | // Declare internals 11 | 12 | const internals = {}; 13 | 14 | 15 | // MAC normalization format version 16 | 17 | exports.headerVersion = '1'; // Prevent comparison of mac values generated with different normalized string formats 18 | 19 | 20 | // Supported HMAC algorithms 21 | 22 | exports.algorithms = ['sha1', 'sha256']; 23 | 24 | 25 | // Calculate the request MAC 26 | 27 | /* 28 | type: 'header', // 'header', 'bewit', 'response' 29 | credentials: { 30 | key: 'aoijedoaijsdlaksjdl', 31 | algorithm: 'sha256' // 'sha1', 'sha256' 32 | }, 33 | options: { 34 | method: 'GET', 35 | resource: '/resource?a=1&b=2', 36 | host: 'example.com', 37 | port: 8080, 38 | ts: 1357718381034, 39 | nonce: 'd3d345f', 40 | hash: 'U4MKKSmiVxk37JCCrAVIjV/OhB3y+NdwoCr6RShbVkE=', 41 | ext: 'app-specific-data', 42 | app: 'hf48hd83qwkj', // Application id (Oz) 43 | dlg: 'd8djwekds9cj' // Delegated by application id (Oz), requires options.app 44 | } 45 | */ 46 | 47 | exports.calculateMac = function (type, credentials, options) { 48 | 49 | const normalized = exports.generateNormalizedString(type, options); 50 | 51 | const hmac = Crypto.createHmac(credentials.algorithm, credentials.key).update(normalized); 52 | const digest = hmac.digest('base64'); 53 | return digest; 54 | }; 55 | 56 | 57 | exports.generateNormalizedString = function (type, options) { 58 | 59 | let resource = options.resource || ''; 60 | if (resource && 61 | resource[0] !== '/') { 62 | 63 | const url = Url.parse(resource, false); 64 | resource = url.path; // Includes query 65 | } 66 | 67 | let normalized = 'hawk.' + exports.headerVersion + '.' + type + '\n' + 68 | options.ts + '\n' + 69 | options.nonce + '\n' + 70 | (options.method || '').toUpperCase() + '\n' + 71 | resource + '\n' + 72 | options.host.toLowerCase() + '\n' + 73 | options.port + '\n' + 74 | (options.hash || '') + '\n'; 75 | 76 | if (options.ext) { 77 | normalized = normalized + options.ext.replace('\\', '\\\\').replace('\n', '\\n'); 78 | } 79 | 80 | normalized = normalized + '\n'; 81 | 82 | if (options.app) { 83 | normalized = normalized + options.app + '\n' + 84 | (options.dlg || '') + '\n'; 85 | } 86 | 87 | return normalized; 88 | }; 89 | 90 | 91 | exports.calculatePayloadHash = function (payload, algorithm, contentType) { 92 | 93 | const hash = exports.initializePayloadHash(algorithm, contentType); 94 | hash.update(payload || ''); 95 | return exports.finalizePayloadHash(hash); 96 | }; 97 | 98 | 99 | exports.initializePayloadHash = function (algorithm, contentType) { 100 | 101 | const hash = Crypto.createHash(algorithm); 102 | hash.update('hawk.' + exports.headerVersion + '.payload\n'); 103 | hash.update(Utils.parseContentType(contentType) + '\n'); 104 | return hash; 105 | }; 106 | 107 | 108 | exports.finalizePayloadHash = function (hash) { 109 | 110 | hash.update('\n'); 111 | return hash.digest('base64'); 112 | }; 113 | 114 | 115 | exports.calculateTsMac = function (ts, credentials) { 116 | 117 | const hmac = Crypto.createHmac(credentials.algorithm, credentials.key); 118 | hmac.update('hawk.' + exports.headerVersion + '.ts\n' + ts + '\n'); 119 | return hmac.digest('base64'); 120 | }; 121 | 122 | 123 | exports.timestampMessage = function (credentials, localtimeOffsetMsec) { 124 | 125 | const now = Utils.nowSecs(localtimeOffsetMsec); 126 | const tsm = exports.calculateTsMac(now, credentials); 127 | return { ts: now, tsm }; 128 | }; 129 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Hawk = require('../lib'); 7 | const Lab = require('lab'); 8 | const Package = require('../package.json'); 9 | 10 | 11 | // Declare internals 12 | 13 | const internals = {}; 14 | 15 | 16 | // Test shortcuts 17 | 18 | const lab = exports.lab = Lab.script(); 19 | const describe = lab.experiment; 20 | const it = lab.test; 21 | const expect = Code.expect; 22 | 23 | 24 | describe('Utils', () => { 25 | 26 | describe('parseHost()', () => { 27 | 28 | it('returns port 80 for non tls node request', (done) => { 29 | 30 | const req = { 31 | method: 'POST', 32 | url: '/resource/4?filter=a', 33 | headers: { 34 | host: 'example.com', 35 | 'content-type': 'text/plain;x=y' 36 | } 37 | }; 38 | 39 | expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(80); 40 | done(); 41 | }); 42 | 43 | it('returns port 443 for non tls node request', (done) => { 44 | 45 | const req = { 46 | method: 'POST', 47 | url: '/resource/4?filter=a', 48 | headers: { 49 | host: 'example.com', 50 | 'content-type': 'text/plain;x=y' 51 | }, 52 | connection: { 53 | encrypted: true 54 | } 55 | }; 56 | 57 | expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(443); 58 | done(); 59 | }); 60 | 61 | it('returns port 443 for non tls node request (IPv6)', (done) => { 62 | 63 | const req = { 64 | method: 'POST', 65 | url: '/resource/4?filter=a', 66 | headers: { 67 | host: '[123:123:123]', 68 | 'content-type': 'text/plain;x=y' 69 | }, 70 | connection: { 71 | encrypted: true 72 | } 73 | }; 74 | 75 | expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(443); 76 | done(); 77 | }); 78 | 79 | it('parses IPv6 headers', (done) => { 80 | 81 | const req = { 82 | method: 'POST', 83 | url: '/resource/4?filter=a', 84 | headers: { 85 | host: '[123:123:123]:8000', 86 | 'content-type': 'text/plain;x=y' 87 | }, 88 | connection: { 89 | encrypted: true 90 | } 91 | }; 92 | 93 | const host = Hawk.utils.parseHost(req, 'Host'); 94 | expect(host.port).to.equal('8000'); 95 | expect(host.name).to.equal('[123:123:123]'); 96 | done(); 97 | }); 98 | 99 | it('errors on header too long', (done) => { 100 | 101 | let long = ''; 102 | for (let i = 0; i < 5000; ++i) { 103 | long += 'x'; 104 | } 105 | 106 | expect(Hawk.utils.parseHost({ headers: { host: long } })).to.be.null(); 107 | done(); 108 | }); 109 | }); 110 | 111 | describe('parseAuthorizationHeader()', () => { 112 | 113 | it('errors on header too long', (done) => { 114 | 115 | let long = 'Scheme a="'; 116 | for (let i = 0; i < 5000; ++i) { 117 | long += 'x'; 118 | } 119 | long += '"'; 120 | 121 | const err = Hawk.utils.parseAuthorizationHeader(long, ['a']); 122 | expect(err).to.be.instanceof(Error); 123 | expect(err.message).to.equal('Header length too long'); 124 | done(); 125 | }); 126 | }); 127 | 128 | describe('version()', () => { 129 | 130 | it('returns the correct package version number', (done) => { 131 | 132 | expect(Hawk.utils.version()).to.equal(Package.version); 133 | done(); 134 | }); 135 | }); 136 | 137 | describe('unauthorized()', () => { 138 | 139 | it('returns a hawk 401', (done) => { 140 | 141 | expect(Hawk.utils.unauthorized('kaboom').output.headers['WWW-Authenticate']).to.equal('Hawk error="kaboom"'); 142 | done(); 143 | }); 144 | 145 | it('supports attributes', (done) => { 146 | 147 | expect(Hawk.utils.unauthorized('kaboom', { a: 'b' }).output.headers['WWW-Authenticate']).to.equal('Hawk a="b", error="kaboom"'); 148 | done(); 149 | }); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Sntp = require('sntp'); 6 | const Boom = require('boom'); 7 | 8 | 9 | // Declare internals 10 | 11 | const internals = {}; 12 | 13 | 14 | exports.version = function () { 15 | 16 | return require('../package.json').version; 17 | }; 18 | 19 | 20 | exports.limits = { 21 | maxMatchLength: 4096 // Limit the length of uris and headers to avoid a DoS attack on string matching 22 | }; 23 | 24 | 25 | // Extract host and port from request 26 | 27 | // $1 $2 28 | internals.hostHeaderRegex = /^(?:(?:\r\n)?\s)*((?:[^:]+)|(?:\[[^\]]+\]))(?::(\d+))?(?:(?:\r\n)?\s)*$/; // (IPv4, hostname)|(IPv6) 29 | 30 | 31 | exports.parseHost = function (req, hostHeaderName) { 32 | 33 | hostHeaderName = (hostHeaderName ? hostHeaderName.toLowerCase() : 'host'); 34 | const hostHeader = req.headers[hostHeaderName]; 35 | if (!hostHeader) { 36 | return null; 37 | } 38 | 39 | if (hostHeader.length > exports.limits.maxMatchLength) { 40 | return null; 41 | } 42 | 43 | const hostParts = hostHeader.match(internals.hostHeaderRegex); 44 | if (!hostParts) { 45 | return null; 46 | } 47 | 48 | return { 49 | name: hostParts[1], 50 | port: (hostParts[2] ? hostParts[2] : (req.connection && req.connection.encrypted ? 443 : 80)) 51 | }; 52 | }; 53 | 54 | 55 | // Parse Content-Type header content 56 | 57 | exports.parseContentType = function (header) { 58 | 59 | if (!header) { 60 | return ''; 61 | } 62 | 63 | return header.split(';')[0].trim().toLowerCase(); 64 | }; 65 | 66 | 67 | // Convert node's to request configuration object 68 | 69 | exports.parseRequest = function (req, options) { 70 | 71 | if (!req.headers) { 72 | return req; 73 | } 74 | 75 | // Obtain host and port information 76 | 77 | let host; 78 | if (!options.host || 79 | !options.port) { 80 | 81 | host = exports.parseHost(req, options.hostHeaderName); 82 | if (!host) { 83 | return new Error('Invalid Host header'); 84 | } 85 | } 86 | 87 | const request = { 88 | method: req.method, 89 | url: req.url, 90 | host: options.host || host.name, 91 | port: options.port || host.port, 92 | authorization: req.headers.authorization, 93 | contentType: req.headers['content-type'] || '' 94 | }; 95 | 96 | return request; 97 | }; 98 | 99 | 100 | exports.now = function (localtimeOffsetMsec) { 101 | 102 | return Sntp.now() + (localtimeOffsetMsec || 0); 103 | }; 104 | 105 | 106 | exports.nowSecs = function (localtimeOffsetMsec) { 107 | 108 | return Math.floor(exports.now(localtimeOffsetMsec) / 1000); 109 | }; 110 | 111 | 112 | internals.authHeaderRegex = /^(\w+)(?:\s+(.*))?$/; // Header: scheme[ something] 113 | internals.attributeRegex = /^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/; // !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9 114 | 115 | 116 | // Parse Hawk HTTP Authorization header 117 | 118 | exports.parseAuthorizationHeader = function (header, keys) { 119 | 120 | keys = keys || ['id', 'ts', 'nonce', 'hash', 'ext', 'mac', 'app', 'dlg']; 121 | 122 | if (!header) { 123 | return Boom.unauthorized(null, 'Hawk'); 124 | } 125 | 126 | if (header.length > exports.limits.maxMatchLength) { 127 | return Boom.badRequest('Header length too long'); 128 | } 129 | 130 | const headerParts = header.match(internals.authHeaderRegex); 131 | if (!headerParts) { 132 | return Boom.badRequest('Invalid header syntax'); 133 | } 134 | 135 | const scheme = headerParts[1]; 136 | if (scheme.toLowerCase() !== 'hawk') { 137 | return Boom.unauthorized(null, 'Hawk'); 138 | } 139 | 140 | const attributesString = headerParts[2]; 141 | if (!attributesString) { 142 | return Boom.badRequest('Invalid header syntax'); 143 | } 144 | 145 | const attributes = {}; 146 | let errorMessage = ''; 147 | const verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, ($0, $1, $2) => { 148 | 149 | // Check valid attribute names 150 | 151 | if (keys.indexOf($1) === -1) { 152 | errorMessage = 'Unknown attribute: ' + $1; 153 | return; 154 | } 155 | 156 | // Allowed attribute value characters 157 | 158 | if ($2.match(internals.attributeRegex) === null) { 159 | errorMessage = 'Bad attribute value: ' + $1; 160 | return; 161 | } 162 | 163 | // Check for duplicates 164 | 165 | if (attributes.hasOwnProperty($1)) { 166 | errorMessage = 'Duplicate attribute: ' + $1; 167 | return; 168 | } 169 | 170 | attributes[$1] = $2; 171 | return ''; 172 | }); 173 | 174 | if (verify !== '') { 175 | return Boom.badRequest(errorMessage || 'Bad header format'); 176 | } 177 | 178 | return attributes; 179 | }; 180 | 181 | 182 | exports.unauthorized = function (message, attributes) { 183 | 184 | return Boom.unauthorized(message || null, 'Hawk', attributes); 185 | }; 186 | 187 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Url = require('url'); 6 | const Hoek = require('hoek'); 7 | const Cryptiles = require('cryptiles'); 8 | const Crypto = require('./crypto'); 9 | const Utils = require('./utils'); 10 | 11 | 12 | // Declare internals 13 | 14 | const internals = {}; 15 | 16 | 17 | // Generate an Authorization header for a given request 18 | 19 | /* 20 | uri: 'http://example.com/resource?a=b' or object from Url.parse() 21 | method: HTTP verb (e.g. 'GET', 'POST') 22 | options: { 23 | 24 | // Required 25 | 26 | credentials: { 27 | id: 'dh37fgj492je', 28 | key: 'aoijedoaijsdlaksjdl', 29 | algorithm: 'sha256' // 'sha1', 'sha256' 30 | }, 31 | 32 | // Optional 33 | 34 | ext: 'application-specific', // Application specific data sent via the ext attribute 35 | timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds 36 | nonce: '2334f34f', // A pre-generated nonce 37 | localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) 38 | payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) 39 | contentType: 'application/json', // Payload content-type (ignored if hash provided) 40 | hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash 41 | app: '24s23423f34dx', // Oz application id 42 | dlg: '234sz34tww3sd' // Oz delegated-by application id 43 | } 44 | */ 45 | 46 | exports.header = function (uri, method, options) { 47 | 48 | const result = { 49 | field: '', 50 | artifacts: {} 51 | }; 52 | 53 | // Validate inputs 54 | 55 | if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') || 56 | !method || typeof method !== 'string' || 57 | !options || typeof options !== 'object') { 58 | 59 | result.err = 'Invalid argument type'; 60 | return result; 61 | } 62 | 63 | // Application time 64 | 65 | const timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec); 66 | 67 | // Validate credentials 68 | 69 | const credentials = options.credentials; 70 | if (!credentials || 71 | !credentials.id || 72 | !credentials.key || 73 | !credentials.algorithm) { 74 | 75 | result.err = 'Invalid credential object'; 76 | return result; 77 | } 78 | 79 | if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { 80 | result.err = 'Unknown algorithm'; 81 | return result; 82 | } 83 | 84 | // Parse URI 85 | 86 | if (typeof uri === 'string') { 87 | uri = Url.parse(uri); 88 | } 89 | 90 | // Calculate signature 91 | 92 | const artifacts = { 93 | ts: timestamp, 94 | nonce: options.nonce || Cryptiles.randomString(6), 95 | method, 96 | resource: uri.pathname + (uri.search || ''), // Maintain trailing '?' 97 | host: uri.hostname, 98 | port: uri.port || (uri.protocol === 'http:' ? 80 : 443), 99 | hash: options.hash, 100 | ext: options.ext, 101 | app: options.app, 102 | dlg: options.dlg 103 | }; 104 | 105 | result.artifacts = artifacts; 106 | 107 | // Calculate payload hash 108 | 109 | if (!artifacts.hash && 110 | (options.payload || options.payload === '')) { 111 | 112 | artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); 113 | } 114 | 115 | const mac = Crypto.calculateMac('header', credentials, artifacts); 116 | 117 | // Construct header 118 | 119 | const hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed 120 | let header = 'Hawk id="' + credentials.id + 121 | '", ts="' + artifacts.ts + 122 | '", nonce="' + artifacts.nonce + 123 | (artifacts.hash ? '", hash="' + artifacts.hash : '') + 124 | (hasExt ? '", ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) : '') + 125 | '", mac="' + mac + '"'; 126 | 127 | if (artifacts.app) { 128 | header = header + ', app="' + artifacts.app + 129 | (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"'; 130 | } 131 | 132 | result.field = header; 133 | 134 | return result; 135 | }; 136 | 137 | 138 | // Validate server response 139 | 140 | /* 141 | res: node's response object 142 | artifacts: object received from header().artifacts 143 | options: { 144 | payload: optional payload received 145 | required: specifies if a Server-Authorization header is required. Defaults to 'false' 146 | } 147 | */ 148 | 149 | exports.authenticate = function (res, credentials, artifacts, options, callback) { 150 | 151 | artifacts = Hoek.clone(artifacts); 152 | options = options || {}; 153 | 154 | let wwwAttributes = null; 155 | let serverAuthAttributes = null; 156 | 157 | const finalize = function (err) { 158 | 159 | if (callback) { 160 | const headers = { 161 | 'www-authenticate': wwwAttributes, 162 | 'server-authorization': serverAuthAttributes 163 | }; 164 | 165 | return callback(err, headers); 166 | } 167 | 168 | return !err; 169 | }; 170 | 171 | if (res.headers['www-authenticate']) { 172 | 173 | // Parse HTTP WWW-Authenticate header 174 | 175 | wwwAttributes = Utils.parseAuthorizationHeader(res.headers['www-authenticate'], ['ts', 'tsm', 'error']); 176 | if (wwwAttributes instanceof Error) { 177 | wwwAttributes = null; 178 | return finalize(new Error('Invalid WWW-Authenticate header')); 179 | } 180 | 181 | // Validate server timestamp (not used to update clock since it is done via the SNPT client) 182 | 183 | if (wwwAttributes.ts) { 184 | const tsm = Crypto.calculateTsMac(wwwAttributes.ts, credentials); 185 | if (tsm !== wwwAttributes.tsm) { 186 | return finalize(new Error('Invalid server timestamp hash')); 187 | } 188 | } 189 | } 190 | 191 | // Parse HTTP Server-Authorization header 192 | 193 | if (!res.headers['server-authorization'] && 194 | !options.required) { 195 | 196 | return finalize(); 197 | } 198 | 199 | serverAuthAttributes = Utils.parseAuthorizationHeader(res.headers['server-authorization'], ['mac', 'ext', 'hash']); 200 | if (serverAuthAttributes instanceof Error) { 201 | serverAuthAttributes = null; 202 | return finalize(new Error('Invalid Server-Authorization header')); 203 | } 204 | 205 | artifacts.ext = serverAuthAttributes.ext; 206 | artifacts.hash = serverAuthAttributes.hash; 207 | 208 | const mac = Crypto.calculateMac('response', credentials, artifacts); 209 | if (mac !== serverAuthAttributes.mac) { 210 | return finalize(new Error('Bad response mac')); 211 | } 212 | 213 | if (!options.payload && 214 | options.payload !== '') { 215 | 216 | return finalize(); 217 | } 218 | 219 | if (!serverAuthAttributes.hash) { 220 | return finalize(new Error('Missing response hash attribute')); 221 | } 222 | 223 | const calculatedHash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, res.headers['content-type']); 224 | if (calculatedHash !== serverAuthAttributes.hash) { 225 | return finalize(new Error('Bad response payload mac')); 226 | } 227 | 228 | return finalize(); 229 | }; 230 | 231 | 232 | // Generate a bewit value for a given URI 233 | 234 | /* 235 | uri: 'http://example.com/resource?a=b' or object from Url.parse() 236 | options: { 237 | 238 | // Required 239 | 240 | credentials: { 241 | id: 'dh37fgj492je', 242 | key: 'aoijedoaijsdlaksjdl', 243 | algorithm: 'sha256' // 'sha1', 'sha256' 244 | }, 245 | ttlSec: 60 * 60, // TTL in seconds 246 | 247 | // Optional 248 | 249 | ext: 'application-specific', // Application specific data sent via the ext attribute 250 | localtimeOffsetMsec: 400 // Time offset to sync with server time 251 | }; 252 | */ 253 | 254 | exports.getBewit = function (uri, options) { 255 | 256 | // Validate inputs 257 | 258 | if (!uri || 259 | (typeof uri !== 'string' && typeof uri !== 'object') || 260 | !options || 261 | typeof options !== 'object' || 262 | !options.ttlSec) { 263 | 264 | return ''; 265 | } 266 | 267 | options.ext = (options.ext === null || options.ext === undefined ? '' : options.ext); // Zero is valid value 268 | 269 | // Application time 270 | 271 | const now = Utils.now(options.localtimeOffsetMsec); 272 | 273 | // Validate credentials 274 | 275 | const credentials = options.credentials; 276 | if (!credentials || 277 | !credentials.id || 278 | !credentials.key || 279 | !credentials.algorithm) { 280 | 281 | return ''; 282 | } 283 | 284 | if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { 285 | return ''; 286 | } 287 | 288 | // Parse URI 289 | 290 | if (typeof uri === 'string') { 291 | uri = Url.parse(uri); 292 | } 293 | 294 | // Calculate signature 295 | 296 | const exp = Math.floor(now / 1000) + options.ttlSec; 297 | const mac = Crypto.calculateMac('bewit', credentials, { 298 | ts: exp, 299 | nonce: '', 300 | method: 'GET', 301 | resource: uri.pathname + (uri.search || ''), // Maintain trailing '?' 302 | host: uri.hostname, 303 | port: uri.port || (uri.protocol === 'http:' ? 80 : 443), 304 | ext: options.ext 305 | }); 306 | 307 | // Construct bewit: id\exp\mac\ext 308 | 309 | const bewit = credentials.id + '\\' + exp + '\\' + mac + '\\' + options.ext; 310 | return Hoek.base64urlEncode(bewit); 311 | }; 312 | 313 | 314 | // Generate an authorization string for a message 315 | 316 | /* 317 | host: 'example.com', 318 | port: 8000, 319 | message: '{"some":"payload"}', // UTF-8 encoded string for body hash generation 320 | options: { 321 | 322 | // Required 323 | 324 | credentials: { 325 | id: 'dh37fgj492je', 326 | key: 'aoijedoaijsdlaksjdl', 327 | algorithm: 'sha256' // 'sha1', 'sha256' 328 | }, 329 | 330 | // Optional 331 | 332 | timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds 333 | nonce: '2334f34f', // A pre-generated nonce 334 | localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) 335 | } 336 | */ 337 | 338 | exports.message = function (host, port, message, options) { 339 | 340 | // Validate inputs 341 | 342 | if (!host || typeof host !== 'string' || 343 | !port || typeof port !== 'number' || 344 | message === null || message === undefined || typeof message !== 'string' || 345 | !options || typeof options !== 'object') { 346 | 347 | return null; 348 | } 349 | 350 | // Application time 351 | 352 | const timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec); 353 | 354 | // Validate credentials 355 | 356 | const credentials = options.credentials; 357 | if (!credentials || 358 | !credentials.id || 359 | !credentials.key || 360 | !credentials.algorithm) { 361 | 362 | // Invalid credential object 363 | return null; 364 | } 365 | 366 | if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { 367 | return null; 368 | } 369 | 370 | // Calculate signature 371 | 372 | const artifacts = { 373 | ts: timestamp, 374 | nonce: options.nonce || Cryptiles.randomString(6), 375 | host, 376 | port, 377 | hash: Crypto.calculatePayloadHash(message, credentials.algorithm) 378 | }; 379 | 380 | // Construct authorization 381 | 382 | const result = { 383 | id: credentials.id, 384 | ts: artifacts.ts, 385 | nonce: artifacts.nonce, 386 | hash: artifacts.hash, 387 | mac: Crypto.calculateMac('message', credentials, artifacts) 388 | }; 389 | 390 | return result; 391 | }; 392 | 393 | 394 | 395 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Url = require('url'); 6 | const Code = require('code'); 7 | const Hawk = require('../lib'); 8 | const Lab = require('lab'); 9 | 10 | 11 | // Declare internals 12 | 13 | const internals = {}; 14 | 15 | 16 | // Test shortcuts 17 | 18 | const lab = exports.lab = Lab.script(); 19 | const describe = lab.experiment; 20 | const it = lab.test; 21 | const expect = Code.expect; 22 | 23 | 24 | describe('Hawk', () => { 25 | 26 | const credentialsFunc = function (id, callback) { 27 | 28 | const credentials = { 29 | id, 30 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 31 | algorithm: (id === '1' ? 'sha1' : 'sha256'), 32 | user: 'steve' 33 | }; 34 | 35 | return callback(null, credentials); 36 | }; 37 | 38 | it('generates a header then successfully parse it (configuration)', (done) => { 39 | 40 | const req = { 41 | method: 'GET', 42 | url: '/resource/4?filter=a', 43 | host: 'example.com', 44 | port: 8080 45 | }; 46 | 47 | credentialsFunc('123456', (err, credentials1) => { 48 | 49 | expect(err).to.not.exist(); 50 | 51 | req.authorization = Hawk.client.header(Url.parse('http://example.com:8080/resource/4?filter=a'), req.method, { credentials: credentials1, ext: 'some-app-data' }).field; 52 | expect(req.authorization).to.exist(); 53 | 54 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 55 | 56 | expect(err).to.not.exist(); 57 | expect(credentials2.user).to.equal('steve'); 58 | expect(artifacts.ext).to.equal('some-app-data'); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | 64 | it('generates a header then successfully parse it (node request)', (done) => { 65 | 66 | const req = { 67 | method: 'POST', 68 | url: '/resource/4?filter=a', 69 | headers: { 70 | host: 'example.com:8080', 71 | 'content-type': 'text/plain;x=y' 72 | } 73 | }; 74 | 75 | const payload = 'some not so random text'; 76 | 77 | credentialsFunc('123456', (err, credentials1) => { 78 | 79 | expect(err).to.not.exist(); 80 | 81 | const reqHeader = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', payload, contentType: req.headers['content-type'] }); 82 | req.headers.authorization = reqHeader.field; 83 | 84 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 85 | 86 | expect(err).to.not.exist(); 87 | expect(credentials2.user).to.equal('steve'); 88 | expect(artifacts.ext).to.equal('some-app-data'); 89 | expect(Hawk.server.authenticatePayload(payload, credentials2, artifacts, req.headers['content-type'])).to.equal(true); 90 | 91 | const res = { 92 | headers: { 93 | 'content-type': 'text/plain' 94 | } 95 | }; 96 | 97 | res.headers['server-authorization'] = Hawk.server.header(credentials2, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' }); 98 | expect(res.headers['server-authorization']).to.exist(); 99 | 100 | expect(Hawk.client.authenticate(res, credentials2, artifacts, { payload: 'some reply' })).to.equal(true); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | 106 | it('generates a header then successfully parse it (absolute request uri)', (done) => { 107 | 108 | const req = { 109 | method: 'POST', 110 | url: 'http://example.com:8080/resource/4?filter=a', 111 | headers: { 112 | host: 'example.com:8080', 113 | 'content-type': 'text/plain;x=y' 114 | } 115 | }; 116 | 117 | const payload = 'some not so random text'; 118 | 119 | credentialsFunc('123456', (err, credentials1) => { 120 | 121 | expect(err).to.not.exist(); 122 | 123 | const reqHeader = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', payload, contentType: req.headers['content-type'] }); 124 | req.headers.authorization = reqHeader.field; 125 | 126 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 127 | 128 | expect(err).to.not.exist(); 129 | expect(credentials2.user).to.equal('steve'); 130 | expect(artifacts.ext).to.equal('some-app-data'); 131 | expect(Hawk.server.authenticatePayload(payload, credentials2, artifacts, req.headers['content-type'])).to.equal(true); 132 | 133 | const res = { 134 | headers: { 135 | 'content-type': 'text/plain' 136 | } 137 | }; 138 | 139 | res.headers['server-authorization'] = Hawk.server.header(credentials2, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' }); 140 | expect(res.headers['server-authorization']).to.exist(); 141 | 142 | expect(Hawk.client.authenticate(res, credentials2, artifacts, { payload: 'some reply' })).to.equal(true); 143 | done(); 144 | }); 145 | }); 146 | }); 147 | 148 | it('generates a header then successfully parse it (no server header options)', (done) => { 149 | 150 | const req = { 151 | method: 'POST', 152 | url: '/resource/4?filter=a', 153 | headers: { 154 | host: 'example.com:8080', 155 | 'content-type': 'text/plain;x=y' 156 | } 157 | }; 158 | 159 | const payload = 'some not so random text'; 160 | 161 | credentialsFunc('123456', (err, credentials1) => { 162 | 163 | expect(err).to.not.exist(); 164 | 165 | const reqHeader = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', payload, contentType: req.headers['content-type'] }); 166 | req.headers.authorization = reqHeader.field; 167 | 168 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 169 | 170 | expect(err).to.not.exist(); 171 | expect(credentials2.user).to.equal('steve'); 172 | expect(artifacts.ext).to.equal('some-app-data'); 173 | expect(Hawk.server.authenticatePayload(payload, credentials2, artifacts, req.headers['content-type'])).to.equal(true); 174 | 175 | const res = { 176 | headers: { 177 | 'content-type': 'text/plain' 178 | } 179 | }; 180 | 181 | res.headers['server-authorization'] = Hawk.server.header(credentials2, artifacts); 182 | expect(res.headers['server-authorization']).to.exist(); 183 | 184 | expect(Hawk.client.authenticate(res, credentials2, artifacts)).to.equal(true); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | 190 | it('generates a header then fails to parse it (missing server header hash)', (done) => { 191 | 192 | const req = { 193 | method: 'POST', 194 | url: '/resource/4?filter=a', 195 | headers: { 196 | host: 'example.com:8080', 197 | 'content-type': 'text/plain;x=y' 198 | } 199 | }; 200 | 201 | const payload = 'some not so random text'; 202 | 203 | credentialsFunc('123456', (err, credentials1) => { 204 | 205 | expect(err).to.not.exist(); 206 | 207 | const reqHeader = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', payload, contentType: req.headers['content-type'] }); 208 | req.headers.authorization = reqHeader.field; 209 | 210 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 211 | 212 | expect(err).to.not.exist(); 213 | expect(credentials2.user).to.equal('steve'); 214 | expect(artifacts.ext).to.equal('some-app-data'); 215 | expect(Hawk.server.authenticatePayload(payload, credentials2, artifacts, req.headers['content-type'])).to.equal(true); 216 | 217 | const res = { 218 | headers: { 219 | 'content-type': 'text/plain' 220 | } 221 | }; 222 | 223 | res.headers['server-authorization'] = Hawk.server.header(credentials2, artifacts); 224 | expect(res.headers['server-authorization']).to.exist(); 225 | 226 | expect(Hawk.client.authenticate(res, credentials2, artifacts, { payload: 'some reply' })).to.equal(false); 227 | done(); 228 | }); 229 | }); 230 | }); 231 | 232 | it('generates a header then successfully parse it (with hash)', (done) => { 233 | 234 | const req = { 235 | method: 'GET', 236 | url: '/resource/4?filter=a', 237 | host: 'example.com', 238 | port: 8080 239 | }; 240 | 241 | credentialsFunc('123456', (err, credentials1) => { 242 | 243 | expect(err).to.not.exist(); 244 | 245 | req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, payload: 'hola!', ext: 'some-app-data' }).field; 246 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 247 | 248 | expect(err).to.not.exist(); 249 | expect(credentials2.user).to.equal('steve'); 250 | expect(artifacts.ext).to.equal('some-app-data'); 251 | done(); 252 | }); 253 | }); 254 | }); 255 | 256 | it('generates a header then successfully parse it then validate payload', (done) => { 257 | 258 | const req = { 259 | method: 'GET', 260 | url: '/resource/4?filter=a', 261 | host: 'example.com', 262 | port: 8080 263 | }; 264 | 265 | credentialsFunc('123456', (err, credentials1) => { 266 | 267 | expect(err).to.not.exist(); 268 | 269 | req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, payload: 'hola!', ext: 'some-app-data' }).field; 270 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 271 | 272 | expect(err).to.not.exist(); 273 | expect(credentials2.user).to.equal('steve'); 274 | expect(artifacts.ext).to.equal('some-app-data'); 275 | expect(Hawk.server.authenticatePayload('hola!', credentials2, artifacts)).to.be.true(); 276 | expect(Hawk.server.authenticatePayload('hello!', credentials2, artifacts)).to.be.false(); 277 | done(); 278 | }); 279 | }); 280 | }); 281 | 282 | it('generates a header then successfully parses and validates payload', (done) => { 283 | 284 | const req = { 285 | method: 'GET', 286 | url: '/resource/4?filter=a', 287 | host: 'example.com', 288 | port: 8080 289 | }; 290 | 291 | credentialsFunc('123456', (err, credentials1) => { 292 | 293 | expect(err).to.not.exist(); 294 | 295 | req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, payload: 'hola!', ext: 'some-app-data' }).field; 296 | Hawk.server.authenticate(req, credentialsFunc, { payload: 'hola!' }, (err, credentials2, artifacts) => { 297 | 298 | expect(err).to.not.exist(); 299 | expect(credentials2.user).to.equal('steve'); 300 | expect(artifacts.ext).to.equal('some-app-data'); 301 | done(); 302 | }); 303 | }); 304 | }); 305 | 306 | it('generates a header then successfully parse it (app)', (done) => { 307 | 308 | const req = { 309 | method: 'GET', 310 | url: '/resource/4?filter=a', 311 | host: 'example.com', 312 | port: 8080 313 | }; 314 | 315 | credentialsFunc('123456', (err, credentials1) => { 316 | 317 | expect(err).to.not.exist(); 318 | 319 | req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', app: 'asd23ased' }).field; 320 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 321 | 322 | expect(err).to.not.exist(); 323 | expect(credentials2.user).to.equal('steve'); 324 | expect(artifacts.ext).to.equal('some-app-data'); 325 | expect(artifacts.app).to.equal('asd23ased'); 326 | done(); 327 | }); 328 | }); 329 | }); 330 | 331 | it('generates a header then successfully parse it (app, dlg)', (done) => { 332 | 333 | const req = { 334 | method: 'GET', 335 | url: '/resource/4?filter=a', 336 | host: 'example.com', 337 | port: 8080 338 | }; 339 | 340 | credentialsFunc('123456', (err, credentials1) => { 341 | 342 | expect(err).to.not.exist(); 343 | 344 | req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', app: 'asd23ased', dlg: '23434szr3q4d' }).field; 345 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 346 | 347 | expect(err).to.not.exist(); 348 | expect(credentials2.user).to.equal('steve'); 349 | expect(artifacts.ext).to.equal('some-app-data'); 350 | expect(artifacts.app).to.equal('asd23ased'); 351 | expect(artifacts.dlg).to.equal('23434szr3q4d'); 352 | done(); 353 | }); 354 | }); 355 | }); 356 | 357 | it('generates a header then fail authentication due to bad hash', (done) => { 358 | 359 | const req = { 360 | method: 'GET', 361 | url: '/resource/4?filter=a', 362 | host: 'example.com', 363 | port: 8080 364 | }; 365 | 366 | credentialsFunc('123456', (err, credentials1) => { 367 | 368 | expect(err).to.not.exist(); 369 | 370 | req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, payload: 'hola!', ext: 'some-app-data' }).field; 371 | Hawk.server.authenticate(req, credentialsFunc, { payload: 'byebye!' }, (err, credentials2, artifacts) => { 372 | 373 | expect(err).to.exist(); 374 | expect(err.output.payload.message).to.equal('Bad payload hash'); 375 | done(); 376 | }); 377 | }); 378 | }); 379 | 380 | it('generates a header for one resource then fail to authenticate another', (done) => { 381 | 382 | const req = { 383 | method: 'GET', 384 | url: '/resource/4?filter=a', 385 | host: 'example.com', 386 | port: 8080 387 | }; 388 | 389 | credentialsFunc('123456', (err, credentials1) => { 390 | 391 | expect(err).to.not.exist(); 392 | 393 | req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data' }).field; 394 | req.url = '/something/else'; 395 | 396 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials2, artifacts) => { 397 | 398 | expect(err).to.exist(); 399 | expect(credentials2).to.exist(); 400 | done(); 401 | }); 402 | }); 403 | }); 404 | }); 405 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Boom = require('boom'); 6 | const Hoek = require('hoek'); 7 | const Cryptiles = require('cryptiles'); 8 | const Crypto = require('./crypto'); 9 | const Utils = require('./utils'); 10 | 11 | 12 | // Declare internals 13 | 14 | const internals = {}; 15 | 16 | 17 | // Hawk authentication 18 | 19 | /* 20 | req: node's HTTP request object or an object as follows: 21 | 22 | const request = { 23 | method: 'GET', 24 | url: '/resource/4?a=1&b=2', 25 | host: 'example.com', 26 | port: 8080, 27 | authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="' 28 | }; 29 | 30 | credentialsFunc: required function to lookup the set of Hawk credentials based on the provided credentials id. 31 | The credentials include the MAC key, MAC algorithm, and other attributes (such as username) 32 | needed by the application. This function is the equivalent of verifying the username and 33 | password in Basic authentication. 34 | 35 | const credentialsFunc = function (id, callback) { 36 | 37 | // Lookup credentials in database 38 | db.lookup(id, function (err, item) { 39 | 40 | if (err || !item) { 41 | return callback(err); 42 | } 43 | 44 | const credentials = { 45 | // Required 46 | key: item.key, 47 | algorithm: item.algorithm, 48 | // Application specific 49 | user: item.user 50 | }; 51 | 52 | return callback(null, credentials); 53 | }); 54 | }; 55 | 56 | options: { 57 | 58 | hostHeaderName: optional header field name, used to override the default 'Host' header when used 59 | behind a cache of a proxy. Apache2 changes the value of the 'Host' header while preserving 60 | the original (which is what the module must verify) in the 'x-forwarded-host' header field. 61 | Only used when passed a node Http.ServerRequest object. 62 | 63 | nonceFunc: optional nonce validation function. The function signature is function(key, nonce, ts, callback) 64 | where 'callback' must be called using the signature function(err). 65 | 66 | timestampSkewSec: optional number of seconds of permitted clock skew for incoming timestamps. Defaults to 60 seconds. 67 | Provides a +/- skew which means actual allowed window is double the number of seconds. 68 | 69 | localtimeOffsetMsec: optional local clock time offset express in a number of milliseconds (positive or negative). 70 | Defaults to 0. 71 | 72 | payload: optional payload for validation. The client calculates the hash value and includes it via the 'hash' 73 | header attribute. The server always ensures the value provided has been included in the request 74 | MAC. When this option is provided, it validates the hash value itself. Validation is done by calculating 75 | a hash value over the entire payload (assuming it has already be normalized to the same format and 76 | encoding used by the client to calculate the hash on request). If the payload is not available at the time 77 | of authentication, the authenticatePayload() method can be used by passing it the credentials and 78 | attributes.hash returned in the authenticate callback. 79 | 80 | host: optional host name override. Only used when passed a node request object. 81 | port: optional port override. Only used when passed a node request object. 82 | } 83 | 84 | callback: function (err, credentials, artifacts) { } 85 | */ 86 | 87 | exports.authenticate = function (req, credentialsFunc, options, callback) { 88 | 89 | callback = Hoek.nextTick(callback); 90 | 91 | // Default options 92 | 93 | options.nonceFunc = options.nonceFunc || internals.nonceFunc; 94 | options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds 95 | 96 | // Application time 97 | 98 | const now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing 99 | 100 | // Convert node Http request object to a request configuration object 101 | 102 | const request = Utils.parseRequest(req, options); 103 | if (request instanceof Error) { 104 | return callback(Boom.badRequest(request.message)); 105 | } 106 | 107 | // Parse HTTP Authorization header 108 | 109 | const attributes = Utils.parseAuthorizationHeader(request.authorization); 110 | if (attributes instanceof Error) { 111 | return callback(attributes); 112 | } 113 | 114 | // Construct artifacts container 115 | 116 | const artifacts = { 117 | method: request.method, 118 | host: request.host, 119 | port: request.port, 120 | resource: request.url, 121 | ts: attributes.ts, 122 | nonce: attributes.nonce, 123 | hash: attributes.hash, 124 | ext: attributes.ext, 125 | app: attributes.app, 126 | dlg: attributes.dlg, 127 | mac: attributes.mac, 128 | id: attributes.id 129 | }; 130 | 131 | // Verify required header attributes 132 | 133 | if (!attributes.id || 134 | !attributes.ts || 135 | !attributes.nonce || 136 | !attributes.mac) { 137 | 138 | return callback(Boom.badRequest('Missing attributes'), null, artifacts); 139 | } 140 | 141 | // Fetch Hawk credentials 142 | 143 | credentialsFunc(attributes.id, (err, credentials) => { 144 | 145 | if (err) { 146 | return callback(err, credentials || null, artifacts); 147 | } 148 | 149 | if (!credentials) { 150 | return callback(Utils.unauthorized('Unknown credentials'), null, artifacts); 151 | } 152 | 153 | if (!credentials.key || 154 | !credentials.algorithm) { 155 | 156 | return callback(Boom.internal('Invalid credentials'), credentials, artifacts); 157 | } 158 | 159 | if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { 160 | return callback(Boom.internal('Unknown algorithm'), credentials, artifacts); 161 | } 162 | 163 | // Calculate MAC 164 | 165 | const mac = Crypto.calculateMac('header', credentials, artifacts); 166 | if (!Cryptiles.fixedTimeComparison(mac, attributes.mac)) { 167 | return callback(Utils.unauthorized('Bad mac'), credentials, artifacts); 168 | } 169 | 170 | // Check payload hash 171 | 172 | if (options.payload || 173 | options.payload === '') { 174 | 175 | if (!attributes.hash) { 176 | return callback(Utils.unauthorized('Missing required payload hash'), credentials, artifacts); 177 | } 178 | 179 | const hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.contentType); 180 | if (!Cryptiles.fixedTimeComparison(hash, attributes.hash)) { 181 | return callback(Utils.unauthorized('Bad payload hash'), credentials, artifacts); 182 | } 183 | } 184 | 185 | // Check nonce 186 | 187 | options.nonceFunc(credentials.key, attributes.nonce, attributes.ts, (err) => { 188 | 189 | if (err) { 190 | return callback(Utils.unauthorized('Invalid nonce'), credentials, artifacts); 191 | } 192 | 193 | // Check timestamp staleness 194 | 195 | if (Math.abs((attributes.ts * 1000) - now) > (options.timestampSkewSec * 1000)) { 196 | const tsm = Crypto.timestampMessage(credentials, options.localtimeOffsetMsec); 197 | return callback(Utils.unauthorized('Stale timestamp', tsm), credentials, artifacts); 198 | } 199 | 200 | // Successful authentication 201 | 202 | return callback(null, credentials, artifacts); 203 | }); 204 | }); 205 | }; 206 | 207 | 208 | // Authenticate payload hash - used when payload cannot be provided during authenticate() 209 | 210 | /* 211 | payload: raw request payload 212 | credentials: from authenticate callback 213 | artifacts: from authenticate callback 214 | contentType: req.headers['content-type'] 215 | */ 216 | 217 | exports.authenticatePayload = function (payload, credentials, artifacts, contentType) { 218 | 219 | const calculatedHash = Crypto.calculatePayloadHash(payload, credentials.algorithm, contentType); 220 | return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash); 221 | }; 222 | 223 | 224 | // Authenticate payload hash - used when payload cannot be provided during authenticate() 225 | 226 | /* 227 | calculatedHash: the payload hash calculated using Crypto.calculatePayloadHash() 228 | artifacts: from authenticate callback 229 | */ 230 | 231 | exports.authenticatePayloadHash = function (calculatedHash, artifacts) { 232 | 233 | return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash); 234 | }; 235 | 236 | 237 | // Generate a Server-Authorization header for a given response 238 | 239 | /* 240 | credentials: {}, // Object received from authenticate() 241 | artifacts: {} // Object received from authenticate(); 'mac', 'hash', and 'ext' - ignored 242 | options: { 243 | ext: 'application-specific', // Application specific data sent via the ext attribute 244 | payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) 245 | contentType: 'application/json', // Payload content-type (ignored if hash provided) 246 | hash: 'U4MKKSmiVxk37JCCrAVIjV=' // Pre-calculated payload hash 247 | } 248 | */ 249 | 250 | exports.header = function (credentials, artifacts, options) { 251 | 252 | // Prepare inputs 253 | 254 | options = options || {}; 255 | 256 | if (!artifacts || 257 | typeof artifacts !== 'object' || 258 | typeof options !== 'object') { 259 | 260 | return ''; 261 | } 262 | 263 | artifacts = Hoek.clone(artifacts); 264 | delete artifacts.mac; 265 | artifacts.hash = options.hash; 266 | artifacts.ext = options.ext; 267 | 268 | // Validate credentials 269 | 270 | if (!credentials || 271 | !credentials.key || 272 | !credentials.algorithm) { 273 | 274 | // Invalid credential object 275 | return ''; 276 | } 277 | 278 | if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { 279 | return ''; 280 | } 281 | 282 | // Calculate payload hash 283 | 284 | if (!artifacts.hash && 285 | (options.payload || options.payload === '')) { 286 | 287 | artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); 288 | } 289 | 290 | const mac = Crypto.calculateMac('response', credentials, artifacts); 291 | 292 | // Construct header 293 | 294 | let header = 'Hawk mac="' + mac + '"' + 295 | (artifacts.hash ? ', hash="' + artifacts.hash + '"' : ''); 296 | 297 | if (artifacts.ext !== null && 298 | artifacts.ext !== undefined && 299 | artifacts.ext !== '') { // Other falsey values allowed 300 | 301 | header = header + ', ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) + '"'; 302 | } 303 | 304 | return header; 305 | }; 306 | 307 | 308 | /* 309 | * Arguments and options are the same as authenticate() with the exception that the only supported options are: 310 | * 'hostHeaderName', 'localtimeOffsetMsec', 'host', 'port' 311 | */ 312 | 313 | 314 | // 1 2 3 4 315 | internals.bewitRegex = /^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/; 316 | 317 | 318 | exports.authenticateBewit = function (req, credentialsFunc, options, callback) { 319 | 320 | callback = Hoek.nextTick(callback); 321 | 322 | // Application time 323 | 324 | const now = Utils.now(options.localtimeOffsetMsec); 325 | 326 | // Convert node Http request object to a request configuration object 327 | 328 | const request = Utils.parseRequest(req, options); 329 | if (request instanceof Error) { 330 | return callback(Boom.badRequest(request.message)); 331 | } 332 | 333 | // Extract bewit 334 | 335 | if (request.url.length > Utils.limits.maxMatchLength) { 336 | return callback(Boom.badRequest('Resource path exceeds max length')); 337 | } 338 | 339 | const resource = request.url.match(internals.bewitRegex); 340 | if (!resource) { 341 | return callback(Utils.unauthorized()); 342 | } 343 | 344 | // Bewit not empty 345 | 346 | if (!resource[3]) { 347 | return callback(Utils.unauthorized('Empty bewit')); 348 | } 349 | 350 | // Verify method is GET 351 | 352 | if (request.method !== 'GET' && 353 | request.method !== 'HEAD') { 354 | 355 | return callback(Utils.unauthorized('Invalid method')); 356 | } 357 | 358 | // No other authentication 359 | 360 | if (request.authorization) { 361 | return callback(Boom.badRequest('Multiple authentications')); 362 | } 363 | 364 | // Parse bewit 365 | 366 | const bewitString = Hoek.base64urlDecode(resource[3]); 367 | if (bewitString instanceof Error) { 368 | return callback(Boom.badRequest('Invalid bewit encoding')); 369 | } 370 | 371 | // Bewit format: id\exp\mac\ext ('\' is used because it is a reserved header attribute character) 372 | 373 | const bewitParts = bewitString.split('\\'); 374 | if (bewitParts.length !== 4) { 375 | return callback(Boom.badRequest('Invalid bewit structure')); 376 | } 377 | 378 | const bewit = { 379 | id: bewitParts[0], 380 | exp: parseInt(bewitParts[1], 10), 381 | mac: bewitParts[2], 382 | ext: bewitParts[3] || '' 383 | }; 384 | 385 | if (!bewit.id || 386 | !bewit.exp || 387 | !bewit.mac) { 388 | 389 | return callback(Boom.badRequest('Missing bewit attributes')); 390 | } 391 | 392 | // Construct URL without bewit 393 | 394 | let url = resource[1]; 395 | if (resource[4]) { 396 | url = url + resource[2] + resource[4]; 397 | } 398 | 399 | // Check expiration 400 | 401 | if (bewit.exp * 1000 <= now) { 402 | return callback(Utils.unauthorized('Access expired'), null, bewit); 403 | } 404 | 405 | // Fetch Hawk credentials 406 | 407 | credentialsFunc(bewit.id, (err, credentials) => { 408 | 409 | if (err) { 410 | return callback(err, credentials || null, bewit.ext); 411 | } 412 | 413 | if (!credentials) { 414 | return callback(Utils.unauthorized('Unknown credentials'), null, bewit); 415 | } 416 | 417 | if (!credentials.key || 418 | !credentials.algorithm) { 419 | 420 | return callback(Boom.internal('Invalid credentials'), credentials, bewit); 421 | } 422 | 423 | if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { 424 | return callback(Boom.internal('Unknown algorithm'), credentials, bewit); 425 | } 426 | 427 | // Calculate MAC 428 | 429 | const mac = Crypto.calculateMac('bewit', credentials, { 430 | ts: bewit.exp, 431 | nonce: '', 432 | method: 'GET', 433 | resource: url, 434 | host: request.host, 435 | port: request.port, 436 | ext: bewit.ext 437 | }); 438 | 439 | if (!Cryptiles.fixedTimeComparison(mac, bewit.mac)) { 440 | return callback(Utils.unauthorized('Bad mac'), credentials, bewit); 441 | } 442 | 443 | // Successful authentication 444 | 445 | return callback(null, credentials, bewit); 446 | }); 447 | }; 448 | 449 | 450 | /* 451 | * options are the same as authenticate() with the exception that the only supported options are: 452 | * 'nonceFunc', 'timestampSkewSec', 'localtimeOffsetMsec' 453 | */ 454 | 455 | exports.authenticateMessage = function (host, port, message, authorization, credentialsFunc, options, callback) { 456 | 457 | callback = Hoek.nextTick(callback); 458 | 459 | // Default options 460 | 461 | options.nonceFunc = options.nonceFunc || internals.nonceFunc; 462 | options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds 463 | 464 | // Application time 465 | 466 | const now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing 467 | 468 | // Validate authorization 469 | 470 | if (!authorization.id || 471 | !authorization.ts || 472 | !authorization.nonce || 473 | !authorization.hash || 474 | !authorization.mac) { 475 | 476 | return callback(Boom.badRequest('Invalid authorization')); 477 | } 478 | 479 | // Fetch Hawk credentials 480 | 481 | credentialsFunc(authorization.id, (err, credentials) => { 482 | 483 | if (err) { 484 | return callback(err, credentials || null); 485 | } 486 | 487 | if (!credentials) { 488 | return callback(Utils.unauthorized('Unknown credentials')); 489 | } 490 | 491 | if (!credentials.key || 492 | !credentials.algorithm) { 493 | 494 | return callback(Boom.internal('Invalid credentials'), credentials); 495 | } 496 | 497 | if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { 498 | return callback(Boom.internal('Unknown algorithm'), credentials); 499 | } 500 | 501 | // Construct artifacts container 502 | 503 | const artifacts = { 504 | ts: authorization.ts, 505 | nonce: authorization.nonce, 506 | host, 507 | port, 508 | hash: authorization.hash 509 | }; 510 | 511 | // Calculate MAC 512 | 513 | const mac = Crypto.calculateMac('message', credentials, artifacts); 514 | if (!Cryptiles.fixedTimeComparison(mac, authorization.mac)) { 515 | return callback(Utils.unauthorized('Bad mac'), credentials); 516 | } 517 | 518 | // Check payload hash 519 | 520 | const hash = Crypto.calculatePayloadHash(message, credentials.algorithm); 521 | if (!Cryptiles.fixedTimeComparison(hash, authorization.hash)) { 522 | return callback(Utils.unauthorized('Bad message hash'), credentials); 523 | } 524 | 525 | // Check nonce 526 | 527 | options.nonceFunc(credentials.key, authorization.nonce, authorization.ts, (err) => { 528 | 529 | if (err) { 530 | return callback(Utils.unauthorized('Invalid nonce'), credentials); 531 | } 532 | 533 | // Check timestamp staleness 534 | 535 | if (Math.abs((authorization.ts * 1000) - now) > (options.timestampSkewSec * 1000)) { 536 | return callback(Utils.unauthorized('Stale timestamp'), credentials); 537 | } 538 | 539 | // Successful authentication 540 | 541 | return callback(null, credentials); 542 | }); 543 | }); 544 | }; 545 | 546 | 547 | internals.nonceFunc = function (key, nonce, ts, nonceCallback) { 548 | 549 | return nonceCallback(); // No validation 550 | }; 551 | -------------------------------------------------------------------------------- /test/uri.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Url = require('url'); 6 | const Code = require('code'); 7 | const Hawk = require('../lib'); 8 | const Hoek = require('hoek'); 9 | const Lab = require('lab'); 10 | 11 | 12 | // Declare internals 13 | 14 | const internals = {}; 15 | 16 | 17 | // Test shortcuts 18 | 19 | const lab = exports.lab = Lab.script(); 20 | const describe = lab.experiment; 21 | const it = lab.test; 22 | const expect = Code.expect; 23 | 24 | 25 | describe('Uri', () => { 26 | 27 | const credentialsFunc = function (id, callback) { 28 | 29 | const credentials = { 30 | id, 31 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 32 | algorithm: (id === '1' ? 'sha1' : 'sha256'), 33 | user: 'steve' 34 | }; 35 | 36 | return callback(null, credentials); 37 | }; 38 | 39 | it('should generate a bewit then successfully authenticate it', (done) => { 40 | 41 | const req = { 42 | method: 'GET', 43 | url: '/resource/4?a=1&b=2', 44 | host: 'example.com', 45 | port: 80 46 | }; 47 | 48 | credentialsFunc('123456', (err, credentials1) => { 49 | 50 | expect(err).to.not.exist(); 51 | 52 | const bewit = Hawk.uri.getBewit('http://example.com/resource/4?a=1&b=2', { credentials: credentials1, ttlSec: 60 * 60 * 24 * 365 * 100, ext: 'some-app-data' }); 53 | req.url += '&bewit=' + bewit; 54 | 55 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials2, attributes) => { 56 | 57 | expect(err).to.not.exist(); 58 | expect(credentials2.user).to.equal('steve'); 59 | expect(attributes.ext).to.equal('some-app-data'); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | it('should generate a bewit then successfully authenticate it (no ext)', (done) => { 66 | 67 | const req = { 68 | method: 'GET', 69 | url: '/resource/4?a=1&b=2', 70 | host: 'example.com', 71 | port: 80 72 | }; 73 | 74 | credentialsFunc('123456', (err, credentials1) => { 75 | 76 | expect(err).to.not.exist(); 77 | 78 | const bewit = Hawk.uri.getBewit('http://example.com/resource/4?a=1&b=2', { credentials: credentials1, ttlSec: 60 * 60 * 24 * 365 * 100 }); 79 | req.url += '&bewit=' + bewit; 80 | 81 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials2, attributes) => { 82 | 83 | expect(err).to.not.exist(); 84 | expect(credentials2.user).to.equal('steve'); 85 | done(); 86 | }); 87 | }); 88 | }); 89 | 90 | it('should successfully authenticate a request (last param)', (done) => { 91 | 92 | const req = { 93 | method: 'GET', 94 | url: '/resource/4?a=1&b=2&bewit=MTIzNDU2XDQ1MTE0ODQ2MjFcMzFjMmNkbUJFd1NJRVZDOVkva1NFb2c3d3YrdEVNWjZ3RXNmOGNHU2FXQT1cc29tZS1hcHAtZGF0YQ', 95 | host: 'example.com', 96 | port: 8080 97 | }; 98 | 99 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 100 | 101 | expect(err).to.not.exist(); 102 | expect(credentials.user).to.equal('steve'); 103 | expect(attributes.ext).to.equal('some-app-data'); 104 | done(); 105 | }); 106 | }); 107 | 108 | it('should successfully authenticate a request (first param)', (done) => { 109 | 110 | const req = { 111 | method: 'GET', 112 | url: '/resource/4?bewit=MTIzNDU2XDQ1MTE0ODQ2MjFcMzFjMmNkbUJFd1NJRVZDOVkva1NFb2c3d3YrdEVNWjZ3RXNmOGNHU2FXQT1cc29tZS1hcHAtZGF0YQ&a=1&b=2', 113 | host: 'example.com', 114 | port: 8080 115 | }; 116 | 117 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 118 | 119 | expect(err).to.not.exist(); 120 | expect(credentials.user).to.equal('steve'); 121 | expect(attributes.ext).to.equal('some-app-data'); 122 | done(); 123 | }); 124 | }); 125 | 126 | it('should successfully authenticate a request (only param)', (done) => { 127 | 128 | const req = { 129 | method: 'GET', 130 | url: '/resource/4?bewit=MTIzNDU2XDQ1MTE0ODQ2NDFcZm1CdkNWT3MvcElOTUUxSTIwbWhrejQ3UnBwTmo4Y1VrSHpQd3Q5OXJ1cz1cc29tZS1hcHAtZGF0YQ', 131 | host: 'example.com', 132 | port: 8080 133 | }; 134 | 135 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 136 | 137 | expect(err).to.not.exist(); 138 | expect(credentials.user).to.equal('steve'); 139 | expect(attributes.ext).to.equal('some-app-data'); 140 | done(); 141 | }); 142 | }); 143 | 144 | it('should fail on multiple authentication', (done) => { 145 | 146 | const req = { 147 | method: 'GET', 148 | url: '/resource/4?bewit=MTIzNDU2XDQ1MTE0ODQ2NDFcZm1CdkNWT3MvcElOTUUxSTIwbWhrejQ3UnBwTmo4Y1VrSHpQd3Q5OXJ1cz1cc29tZS1hcHAtZGF0YQ', 149 | host: 'example.com', 150 | port: 8080, 151 | authorization: 'Basic asdasdasdasd' 152 | }; 153 | 154 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 155 | 156 | expect(err).to.exist(); 157 | expect(err.output.payload.message).to.equal('Multiple authentications'); 158 | done(); 159 | }); 160 | }); 161 | 162 | it('should fail on method other than GET', (done) => { 163 | 164 | credentialsFunc('123456', (err, credentials1) => { 165 | 166 | expect(err).to.not.exist(); 167 | 168 | const req = { 169 | method: 'POST', 170 | url: '/resource/4?filter=a', 171 | host: 'example.com', 172 | port: 8080 173 | }; 174 | 175 | const exp = Math.floor(Hawk.utils.now() / 1000) + 60; 176 | const ext = 'some-app-data'; 177 | const mac = Hawk.crypto.calculateMac('bewit', credentials1, { 178 | ts: exp, 179 | nonce: '', 180 | method: req.method, 181 | resource: req.url, 182 | host: req.host, 183 | port: req.port, 184 | ext 185 | }); 186 | 187 | const bewit = credentials1.id + '\\' + exp + '\\' + mac + '\\' + ext; 188 | 189 | req.url += '&bewit=' + Hoek.base64urlEncode(bewit); 190 | 191 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials2, attributes) => { 192 | 193 | expect(err).to.exist(); 194 | expect(err.output.payload.message).to.equal('Invalid method'); 195 | done(); 196 | }); 197 | }); 198 | }); 199 | 200 | it('should fail on invalid host header', (done) => { 201 | 202 | const req = { 203 | method: 'GET', 204 | url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', 205 | headers: { 206 | host: 'example.com:something' 207 | } 208 | }; 209 | 210 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 211 | 212 | expect(err).to.exist(); 213 | expect(err.output.payload.message).to.equal('Invalid Host header'); 214 | done(); 215 | }); 216 | }); 217 | 218 | it('should fail on empty bewit', (done) => { 219 | 220 | const req = { 221 | method: 'GET', 222 | url: '/resource/4?bewit=', 223 | host: 'example.com', 224 | port: 8080 225 | }; 226 | 227 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 228 | 229 | expect(err).to.exist(); 230 | expect(err.output.payload.message).to.equal('Empty bewit'); 231 | expect(err.isMissing).to.not.exist(); 232 | done(); 233 | }); 234 | }); 235 | 236 | it('should fail on invalid bewit', (done) => { 237 | 238 | const req = { 239 | method: 'GET', 240 | url: '/resource/4?bewit=*', 241 | host: 'example.com', 242 | port: 8080 243 | }; 244 | 245 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 246 | 247 | expect(err).to.exist(); 248 | expect(err.output.payload.message).to.equal('Invalid bewit encoding'); 249 | expect(err.isMissing).to.not.exist(); 250 | done(); 251 | }); 252 | }); 253 | 254 | it('should fail on missing bewit', (done) => { 255 | 256 | const req = { 257 | method: 'GET', 258 | url: '/resource/4', 259 | host: 'example.com', 260 | port: 8080 261 | }; 262 | 263 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 264 | 265 | expect(err).to.exist(); 266 | expect(err.output.payload.message).to.not.exist(); 267 | expect(err.isMissing).to.equal(true); 268 | done(); 269 | }); 270 | }); 271 | 272 | it('should fail on invalid bewit structure', (done) => { 273 | 274 | const req = { 275 | method: 'GET', 276 | url: '/resource/4?bewit=abc', 277 | host: 'example.com', 278 | port: 8080 279 | }; 280 | 281 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 282 | 283 | expect(err).to.exist(); 284 | expect(err.output.payload.message).to.equal('Invalid bewit structure'); 285 | done(); 286 | }); 287 | }); 288 | 289 | it('should fail on empty bewit attribute', (done) => { 290 | 291 | const req = { 292 | method: 'GET', 293 | url: '/resource/4?bewit=YVxcY1xk', 294 | host: 'example.com', 295 | port: 8080 296 | }; 297 | 298 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 299 | 300 | expect(err).to.exist(); 301 | expect(err.output.payload.message).to.equal('Missing bewit attributes'); 302 | done(); 303 | }); 304 | }); 305 | 306 | it('should fail on missing bewit id attribute', (done) => { 307 | 308 | const req = { 309 | method: 'GET', 310 | url: '/resource/4?bewit=XDQ1NTIxNDc2MjJcK0JFbFhQMXhuWjcvd1Nrbm1ldGhlZm5vUTNHVjZNSlFVRHk4NWpTZVJ4VT1cc29tZS1hcHAtZGF0YQ', 311 | host: 'example.com', 312 | port: 8080 313 | }; 314 | 315 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 316 | 317 | expect(err).to.exist(); 318 | expect(err.output.payload.message).to.equal('Missing bewit attributes'); 319 | done(); 320 | }); 321 | }); 322 | 323 | it('should fail on expired access', (done) => { 324 | 325 | const req = { 326 | method: 'GET', 327 | url: '/resource/4?a=1&b=2&bewit=MTIzNDU2XDEzNTY0MTg1ODNcWk1wZlMwWU5KNHV0WHpOMmRucTRydEk3NXNXTjFjeWVITTcrL0tNZFdVQT1cc29tZS1hcHAtZGF0YQ', 328 | host: 'example.com', 329 | port: 8080 330 | }; 331 | 332 | Hawk.uri.authenticate(req, credentialsFunc, {}, (err, credentials, attributes) => { 333 | 334 | expect(err).to.exist(); 335 | expect(err.output.payload.message).to.equal('Access expired'); 336 | done(); 337 | }); 338 | }); 339 | 340 | it('should fail on credentials function error', (done) => { 341 | 342 | const req = { 343 | method: 'GET', 344 | url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', 345 | host: 'example.com', 346 | port: 8080 347 | }; 348 | 349 | Hawk.uri.authenticate(req, (id, callback) => { 350 | 351 | callback(Hawk.error.badRequest('Boom')); 352 | }, {}, (err, credentials, attributes) => { 353 | 354 | expect(err).to.exist(); 355 | expect(err.output.payload.message).to.equal('Boom'); 356 | done(); 357 | }); 358 | }); 359 | 360 | it('should fail on credentials function error with credentials', (done) => { 361 | 362 | const req = { 363 | method: 'GET', 364 | url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', 365 | host: 'example.com', 366 | port: 8080 367 | }; 368 | 369 | Hawk.uri.authenticate(req, (id, callback) => { 370 | 371 | callback(Hawk.error.badRequest('Boom'), { some: 'value' }); 372 | }, {}, (err, credentials, attributes) => { 373 | 374 | expect(err).to.exist(); 375 | expect(err.output.payload.message).to.equal('Boom'); 376 | expect(credentials.some).to.equal('value'); 377 | done(); 378 | }); 379 | }); 380 | 381 | it('should fail on null credentials function response', (done) => { 382 | 383 | const req = { 384 | method: 'GET', 385 | url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', 386 | host: 'example.com', 387 | port: 8080 388 | }; 389 | 390 | Hawk.uri.authenticate(req, (id, callback) => { 391 | 392 | callback(null, null); 393 | }, {}, (err, credentials, attributes) => { 394 | 395 | expect(err).to.exist(); 396 | expect(err.output.payload.message).to.equal('Unknown credentials'); 397 | done(); 398 | }); 399 | }); 400 | 401 | it('should fail on invalid credentials function response', (done) => { 402 | 403 | const req = { 404 | method: 'GET', 405 | url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', 406 | host: 'example.com', 407 | port: 8080 408 | }; 409 | 410 | Hawk.uri.authenticate(req, (id, callback) => { 411 | 412 | callback(null, {}); 413 | }, {}, (err, credentials, attributes) => { 414 | 415 | expect(err).to.exist(); 416 | expect(err.message).to.equal('Invalid credentials'); 417 | done(); 418 | }); 419 | }); 420 | 421 | it('should fail on invalid credentials function response (unknown algorithm)', (done) => { 422 | 423 | const req = { 424 | method: 'GET', 425 | url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', 426 | host: 'example.com', 427 | port: 8080 428 | }; 429 | 430 | Hawk.uri.authenticate(req, (id, callback) => { 431 | 432 | callback(null, { key: 'xxx', algorithm: 'xxx' }); 433 | }, {}, (err, credentials, attributes) => { 434 | 435 | expect(err).to.exist(); 436 | expect(err.message).to.equal('Unknown algorithm'); 437 | done(); 438 | }); 439 | }); 440 | 441 | it('should fail on invalid credentials function response (bad mac)', (done) => { 442 | 443 | const req = { 444 | method: 'GET', 445 | url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', 446 | host: 'example.com', 447 | port: 8080 448 | }; 449 | 450 | Hawk.uri.authenticate(req, (id, callback) => { 451 | 452 | callback(null, { key: 'xxx', algorithm: 'sha256' }); 453 | }, {}, (err, credentials, attributes) => { 454 | 455 | expect(err).to.exist(); 456 | expect(err.output.payload.message).to.equal('Bad mac'); 457 | done(); 458 | }); 459 | }); 460 | 461 | describe('getBewit()', () => { 462 | 463 | it('returns a valid bewit value', (done) => { 464 | 465 | const credentials = { 466 | id: '123456', 467 | key: '2983d45yun89q', 468 | algorithm: 'sha256' 469 | }; 470 | 471 | const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' }); 472 | expect(bewit).to.equal('MTIzNDU2XDEzNTY0MjA3MDdca3NjeHdOUjJ0SnBQMVQxekRMTlBiQjVVaUtJVTl0T1NKWFRVZEc3WDloOD1ceGFuZHlhbmR6'); 473 | done(); 474 | }); 475 | 476 | it('returns a valid bewit value (explicit port)', (done) => { 477 | 478 | const credentials = { 479 | id: '123456', 480 | key: '2983d45yun89q', 481 | algorithm: 'sha256' 482 | }; 483 | 484 | const bewit = Hawk.uri.getBewit('https://example.com:8080/somewhere/over/the/rainbow', { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' }); 485 | expect(bewit).to.equal('MTIzNDU2XDEzNTY0MjA3MDdcaFpiSjNQMmNLRW80a3kwQzhqa1pBa1J5Q1p1ZWc0V1NOYnhWN3ZxM3hIVT1ceGFuZHlhbmR6'); 486 | done(); 487 | }); 488 | 489 | it('returns a valid bewit value (null ext)', (done) => { 490 | 491 | const credentials = { 492 | id: '123456', 493 | key: '2983d45yun89q', 494 | algorithm: 'sha256' 495 | }; 496 | 497 | const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: null }); 498 | expect(bewit).to.equal('MTIzNDU2XDEzNTY0MjA3MDdcSUdZbUxnSXFMckNlOEN4dktQczRKbFdJQStValdKSm91d2dBUmlWaENBZz1c'); 499 | done(); 500 | }); 501 | 502 | it('returns a valid bewit value (parsed uri)', (done) => { 503 | 504 | const credentials = { 505 | id: '123456', 506 | key: '2983d45yun89q', 507 | algorithm: 'sha256' 508 | }; 509 | 510 | const bewit = Hawk.uri.getBewit(Url.parse('https://example.com/somewhere/over/the/rainbow'), { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' }); 511 | expect(bewit).to.equal('MTIzNDU2XDEzNTY0MjA3MDdca3NjeHdOUjJ0SnBQMVQxekRMTlBiQjVVaUtJVTl0T1NKWFRVZEc3WDloOD1ceGFuZHlhbmR6'); 512 | done(); 513 | }); 514 | 515 | it('errors on invalid options', (done) => { 516 | 517 | const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', 4); 518 | expect(bewit).to.equal(''); 519 | done(); 520 | }); 521 | 522 | it('errors on missing uri', (done) => { 523 | 524 | const credentials = { 525 | id: '123456', 526 | key: '2983d45yun89q', 527 | algorithm: 'sha256' 528 | }; 529 | 530 | const bewit = Hawk.uri.getBewit('', { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' }); 531 | expect(bewit).to.equal(''); 532 | done(); 533 | }); 534 | 535 | it('errors on invalid uri', (done) => { 536 | 537 | const credentials = { 538 | id: '123456', 539 | key: '2983d45yun89q', 540 | algorithm: 'sha256' 541 | }; 542 | 543 | const bewit = Hawk.uri.getBewit(5, { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' }); 544 | expect(bewit).to.equal(''); 545 | done(); 546 | }); 547 | 548 | it('errors on invalid credentials (id)', (done) => { 549 | 550 | const credentials = { 551 | key: '2983d45yun89q', 552 | algorithm: 'sha256' 553 | }; 554 | 555 | const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 3000, ext: 'xandyandz' }); 556 | expect(bewit).to.equal(''); 557 | done(); 558 | }); 559 | 560 | it('errors on missing credentials', (done) => { 561 | 562 | const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { ttlSec: 3000, ext: 'xandyandz' }); 563 | expect(bewit).to.equal(''); 564 | done(); 565 | }); 566 | 567 | it('errors on invalid credentials (key)', (done) => { 568 | 569 | const credentials = { 570 | id: '123456', 571 | algorithm: 'sha256' 572 | }; 573 | 574 | const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 3000, ext: 'xandyandz' }); 575 | expect(bewit).to.equal(''); 576 | done(); 577 | }); 578 | 579 | it('errors on invalid algorithm', (done) => { 580 | 581 | const credentials = { 582 | id: '123456', 583 | key: '2983d45yun89q', 584 | algorithm: 'hmac-sha-0' 585 | }; 586 | 587 | const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 300, ext: 'xandyandz' }); 588 | expect(bewit).to.equal(''); 589 | done(); 590 | }); 591 | 592 | it('errors on missing options', (done) => { 593 | 594 | const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow'); 595 | expect(bewit).to.equal(''); 596 | done(); 597 | }); 598 | }); 599 | }); 600 | 601 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Hawk = require('../lib'); 7 | const Lab = require('lab'); 8 | 9 | 10 | // Declare internals 11 | 12 | const internals = {}; 13 | 14 | 15 | // Test shortcuts 16 | 17 | const lab = exports.lab = Lab.script(); 18 | const describe = lab.experiment; 19 | const it = lab.test; 20 | const expect = Code.expect; 21 | 22 | 23 | describe('Client', () => { 24 | 25 | describe('header()', () => { 26 | 27 | it('returns a valid authorization header (sha1)', (done) => { 28 | 29 | const credentials = { 30 | id: '123456', 31 | key: '2983d45yun89q', 32 | algorithm: 'sha1' 33 | }; 34 | 35 | const header = Hawk.client.header('http://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about' }).field; 36 | expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="'); 37 | done(); 38 | }); 39 | 40 | it('returns a valid authorization header (sha256)', (done) => { 41 | 42 | const credentials = { 43 | id: '123456', 44 | key: '2983d45yun89q', 45 | algorithm: 'sha256' 46 | }; 47 | 48 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about', contentType: 'text/plain' }).field; 49 | expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ext="Bazinga!", mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'); 50 | done(); 51 | }); 52 | 53 | it('returns a valid authorization header (no ext)', (done) => { 54 | 55 | const credentials = { 56 | id: '123456', 57 | key: '2983d45yun89q', 58 | algorithm: 'sha256' 59 | }; 60 | 61 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about', contentType: 'text/plain' }).field; 62 | expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'); 63 | done(); 64 | }); 65 | 66 | it('returns a valid authorization header (null ext)', (done) => { 67 | 68 | const credentials = { 69 | id: '123456', 70 | key: '2983d45yun89q', 71 | algorithm: 'sha256' 72 | }; 73 | 74 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about', contentType: 'text/plain', ext: null }).field; 75 | expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'); 76 | done(); 77 | }); 78 | 79 | it('returns a valid authorization header (empty payload)', (done) => { 80 | 81 | const credentials = { 82 | id: '123456', 83 | key: '2983d45yun89q', 84 | algorithm: 'sha256' 85 | }; 86 | 87 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, timestamp: 1353809207, nonce: 'Ygvqdz', payload: '', contentType: 'text/plain' }).field; 88 | expect(header).to.equal('Hawk id=\"123456\", ts=\"1353809207\", nonce=\"Ygvqdz\", hash=\"q/t+NNAkQZNlq/aAD6PlexImwQTxwgT2MahfTa9XRLA=\", mac=\"U5k16YEzn3UnBHKeBzsDXn067Gu3R4YaY6xOt9PYRZM=\"'); 89 | done(); 90 | }); 91 | 92 | it('returns a valid authorization header (pre hashed payload)', (done) => { 93 | 94 | const credentials = { 95 | id: '123456', 96 | key: '2983d45yun89q', 97 | algorithm: 'sha256' 98 | }; 99 | 100 | const options = { credentials, timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about', contentType: 'text/plain' }; 101 | options.hash = Hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); 102 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', options).field; 103 | expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'); 104 | done(); 105 | }); 106 | 107 | it('errors on missing uri', (done) => { 108 | 109 | const header = Hawk.client.header('', 'POST'); 110 | expect(header.field).to.equal(''); 111 | expect(header.err).to.equal('Invalid argument type'); 112 | done(); 113 | }); 114 | 115 | it('errors on invalid uri', (done) => { 116 | 117 | const header = Hawk.client.header(4, 'POST'); 118 | expect(header.field).to.equal(''); 119 | expect(header.err).to.equal('Invalid argument type'); 120 | done(); 121 | }); 122 | 123 | it('errors on missing method', (done) => { 124 | 125 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', ''); 126 | expect(header.field).to.equal(''); 127 | expect(header.err).to.equal('Invalid argument type'); 128 | done(); 129 | }); 130 | 131 | it('errors on invalid method', (done) => { 132 | 133 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 5); 134 | expect(header.field).to.equal(''); 135 | expect(header.err).to.equal('Invalid argument type'); 136 | done(); 137 | }); 138 | 139 | it('errors on missing options', (done) => { 140 | 141 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST'); 142 | expect(header.field).to.equal(''); 143 | expect(header.err).to.equal('Invalid argument type'); 144 | done(); 145 | }); 146 | 147 | it('errors on invalid credentials (id)', (done) => { 148 | 149 | const credentials = { 150 | key: '2983d45yun89q', 151 | algorithm: 'sha256' 152 | }; 153 | 154 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207 }); 155 | expect(header.field).to.equal(''); 156 | expect(header.err).to.equal('Invalid credential object'); 157 | done(); 158 | }); 159 | 160 | it('errors on missing credentials', (done) => { 161 | 162 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { ext: 'Bazinga!', timestamp: 1353809207 }); 163 | expect(header.field).to.equal(''); 164 | expect(header.err).to.equal('Invalid credential object'); 165 | done(); 166 | }); 167 | 168 | it('errors on invalid credentials', (done) => { 169 | 170 | const credentials = { 171 | id: '123456', 172 | algorithm: 'sha256' 173 | }; 174 | 175 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207 }); 176 | expect(header.field).to.equal(''); 177 | expect(header.err).to.equal('Invalid credential object'); 178 | done(); 179 | }); 180 | 181 | it('errors on invalid algorithm', (done) => { 182 | 183 | const credentials = { 184 | id: '123456', 185 | key: '2983d45yun89q', 186 | algorithm: 'hmac-sha-0' 187 | }; 188 | 189 | const header = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, payload: 'something, anything!', ext: 'Bazinga!', timestamp: 1353809207 }); 190 | expect(header.field).to.equal(''); 191 | expect(header.err).to.equal('Unknown algorithm'); 192 | done(); 193 | }); 194 | }); 195 | 196 | describe('authenticate()', () => { 197 | 198 | it('returns false on invalid header', (done) => { 199 | 200 | const res = { 201 | headers: { 202 | 'server-authorization': 'Hawk mac="abc", bad="xyz"' 203 | } 204 | }; 205 | 206 | expect(Hawk.client.authenticate(res, {})).to.equal(false); 207 | done(); 208 | }); 209 | 210 | it('returns false on invalid header (callback)', (done) => { 211 | 212 | const res = { 213 | headers: { 214 | 'server-authorization': 'Hawk mac="abc", bad="xyz"' 215 | } 216 | }; 217 | 218 | Hawk.client.authenticate(res, {}, null, null, (err) => { 219 | 220 | expect(err).to.exist(); 221 | expect(err.message).to.equal('Invalid Server-Authorization header'); 222 | done(); 223 | }); 224 | }); 225 | 226 | it('returns false on invalid mac', (done) => { 227 | 228 | const res = { 229 | headers: { 230 | 'content-type': 'text/plain', 231 | 'server-authorization': 'Hawk mac="_IJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' 232 | } 233 | }; 234 | 235 | const artifacts = { 236 | method: 'POST', 237 | host: 'example.com', 238 | port: '8080', 239 | resource: '/resource/4?filter=a', 240 | ts: '1362336900', 241 | nonce: 'eb5S_L', 242 | hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', 243 | ext: 'some-app-data', 244 | app: undefined, 245 | dlg: undefined, 246 | mac: 'BlmSe8K+pbKIb6YsZCnt4E1GrYvY1AaYayNR82dGpIk=', 247 | id: '123456' 248 | }; 249 | 250 | const credentials = { 251 | id: '123456', 252 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 253 | algorithm: 'sha256', 254 | user: 'steve' 255 | }; 256 | 257 | expect(Hawk.client.authenticate(res, credentials, artifacts)).to.equal(false); 258 | done(); 259 | }); 260 | 261 | it('returns true on ignoring hash', (done) => { 262 | 263 | const res = { 264 | headers: { 265 | 'content-type': 'text/plain', 266 | 'server-authorization': 'Hawk mac="XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' 267 | } 268 | }; 269 | 270 | const artifacts = { 271 | method: 'POST', 272 | host: 'example.com', 273 | port: '8080', 274 | resource: '/resource/4?filter=a', 275 | ts: '1362336900', 276 | nonce: 'eb5S_L', 277 | hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', 278 | ext: 'some-app-data', 279 | app: undefined, 280 | dlg: undefined, 281 | mac: 'BlmSe8K+pbKIb6YsZCnt4E1GrYvY1AaYayNR82dGpIk=', 282 | id: '123456' 283 | }; 284 | 285 | const credentials = { 286 | id: '123456', 287 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 288 | algorithm: 'sha256', 289 | user: 'steve' 290 | }; 291 | 292 | expect(Hawk.client.authenticate(res, credentials, artifacts)).to.equal(true); 293 | done(); 294 | }); 295 | 296 | it('validates response payload', (done) => { 297 | 298 | const payload = 'some reply'; 299 | 300 | const res = { 301 | headers: { 302 | 'content-type': 'text/plain', 303 | 'server-authorization': 'Hawk mac="odsVGUq0rCoITaiNagW22REIpqkwP9zt5FyqqOW9Zj8=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' 304 | } 305 | }; 306 | 307 | const credentials = { 308 | id: '123456', 309 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 310 | algorithm: 'sha256', 311 | user: 'steve' 312 | }; 313 | 314 | const artifacts = { 315 | method: 'POST', 316 | host: 'example.com', 317 | port: '8080', 318 | resource: '/resource/4?filter=a', 319 | ts: '1453070933', 320 | nonce: '3hOHpR', 321 | hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', 322 | ext: 'some-app-data', 323 | app: undefined, 324 | dlg: undefined, 325 | mac: '/DitzeD66F2f7O535SERbX9p+oh9ZnNLqSNHG+c7/vs=', 326 | id: '123456' 327 | }; 328 | 329 | expect(Hawk.client.authenticate(res, credentials, artifacts, { payload })).to.equal(true); 330 | done(); 331 | }); 332 | 333 | it('validates response payload (callback)', (done) => { 334 | 335 | const payload = 'some reply'; 336 | 337 | const res = { 338 | headers: { 339 | 'content-type': 'text/plain', 340 | 'server-authorization': 'Hawk mac="odsVGUq0rCoITaiNagW22REIpqkwP9zt5FyqqOW9Zj8=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' 341 | } 342 | }; 343 | 344 | const credentials = { 345 | id: '123456', 346 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 347 | algorithm: 'sha256', 348 | user: 'steve' 349 | }; 350 | 351 | const artifacts = { 352 | method: 'POST', 353 | host: 'example.com', 354 | port: '8080', 355 | resource: '/resource/4?filter=a', 356 | ts: '1453070933', 357 | nonce: '3hOHpR', 358 | hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', 359 | ext: 'some-app-data', 360 | app: undefined, 361 | dlg: undefined, 362 | mac: '/DitzeD66F2f7O535SERbX9p+oh9ZnNLqSNHG+c7/vs=', 363 | id: '123456' 364 | }; 365 | 366 | Hawk.client.authenticate(res, credentials, artifacts, { payload }, (err, headers) => { 367 | 368 | expect(err).to.not.exist(); 369 | expect(headers).to.equal({ 370 | 'www-authenticate': null, 371 | 'server-authorization': { 372 | mac: 'odsVGUq0rCoITaiNagW22REIpqkwP9zt5FyqqOW9Zj8=', 373 | hash: 'f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=', 374 | ext: 'response-specific' 375 | } 376 | }); 377 | done(); 378 | }); 379 | }); 380 | 381 | it('errors on invalid response payload', (done) => { 382 | 383 | const payload = 'wrong reply'; 384 | 385 | const res = { 386 | headers: { 387 | 'content-type': 'text/plain', 388 | 'server-authorization': 'Hawk mac="odsVGUq0rCoITaiNagW22REIpqkwP9zt5FyqqOW9Zj8=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' 389 | } 390 | }; 391 | 392 | const credentials = { 393 | id: '123456', 394 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 395 | algorithm: 'sha256', 396 | user: 'steve' 397 | }; 398 | 399 | const artifacts = { 400 | method: 'POST', 401 | host: 'example.com', 402 | port: '8080', 403 | resource: '/resource/4?filter=a', 404 | ts: '1453070933', 405 | nonce: '3hOHpR', 406 | hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', 407 | ext: 'some-app-data', 408 | app: undefined, 409 | dlg: undefined, 410 | mac: '/DitzeD66F2f7O535SERbX9p+oh9ZnNLqSNHG+c7/vs=', 411 | id: '123456' 412 | }; 413 | 414 | expect(Hawk.client.authenticate(res, credentials, artifacts, { payload })).to.equal(false); 415 | done(); 416 | }); 417 | 418 | it('fails on invalid WWW-Authenticate header format', (done) => { 419 | 420 | const header = 'Hawk ts="1362346425875", tsm="PhwayS28vtnn3qbv0mqRBYSXebN/zggEtucfeZ620Zo=", x="Stale timestamp"'; 421 | expect(Hawk.client.authenticate({ headers: { 'www-authenticate': header } }, {})).to.equal(false); 422 | done(); 423 | }); 424 | 425 | it('fails on invalid WWW-Authenticate header format', (done) => { 426 | 427 | const credentials = { 428 | id: '123456', 429 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 430 | algorithm: 'sha256', 431 | user: 'steve' 432 | }; 433 | 434 | const header = 'Hawk ts="1362346425875", tsm="hwayS28vtnn3qbv0mqRBYSXebN/zggEtucfeZ620Zo=", error="Stale timestamp"'; 435 | expect(Hawk.client.authenticate({ headers: { 'www-authenticate': header } }, credentials)).to.equal(false); 436 | done(); 437 | }); 438 | 439 | it('skips tsm validation when missing ts', (done) => { 440 | 441 | const header = 'Hawk error="Stale timestamp"'; 442 | expect(Hawk.client.authenticate({ headers: { 'www-authenticate': header } }, {})).to.equal(true); 443 | done(); 444 | }); 445 | }); 446 | 447 | describe('message()', () => { 448 | 449 | it('generates authorization', (done) => { 450 | 451 | const credentials = { 452 | id: '123456', 453 | key: '2983d45yun89q', 454 | algorithm: 'sha1' 455 | }; 456 | 457 | const auth = Hawk.client.message('example.com', 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' }); 458 | expect(auth).to.exist(); 459 | expect(auth.ts).to.equal(1353809207); 460 | expect(auth.nonce).to.equal('abc123'); 461 | done(); 462 | }); 463 | 464 | it('errors on invalid host', (done) => { 465 | 466 | const credentials = { 467 | id: '123456', 468 | key: '2983d45yun89q', 469 | algorithm: 'sha1' 470 | }; 471 | 472 | const auth = Hawk.client.message(5, 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' }); 473 | expect(auth).to.not.exist(); 474 | done(); 475 | }); 476 | 477 | it('errors on invalid port', (done) => { 478 | 479 | const credentials = { 480 | id: '123456', 481 | key: '2983d45yun89q', 482 | algorithm: 'sha1' 483 | }; 484 | 485 | const auth = Hawk.client.message('example.com', '80', 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' }); 486 | expect(auth).to.not.exist(); 487 | done(); 488 | }); 489 | 490 | it('errors on missing host', (done) => { 491 | 492 | const credentials = { 493 | id: '123456', 494 | key: '2983d45yun89q', 495 | algorithm: 'sha1' 496 | }; 497 | 498 | const auth = Hawk.client.message(undefined, 0, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' }); 499 | expect(auth).to.not.exist(); 500 | done(); 501 | }); 502 | 503 | it('errors on missing port', (done) => { 504 | 505 | const credentials = { 506 | id: '123456', 507 | key: '2983d45yun89q', 508 | algorithm: 'sha1' 509 | }; 510 | 511 | const auth = Hawk.client.message('example.com', undefined, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' }); 512 | expect(auth).to.not.exist(); 513 | done(); 514 | }); 515 | 516 | it('errors on null message', (done) => { 517 | 518 | const credentials = { 519 | id: '123456', 520 | key: '2983d45yun89q', 521 | algorithm: 'sha1' 522 | }; 523 | 524 | const auth = Hawk.client.message('example.com', 80, null, { credentials, timestamp: 1353809207, nonce: 'abc123' }); 525 | expect(auth).to.not.exist(); 526 | done(); 527 | }); 528 | 529 | it('errors on missing message', (done) => { 530 | 531 | const credentials = { 532 | id: '123456', 533 | key: '2983d45yun89q', 534 | algorithm: 'sha1' 535 | }; 536 | 537 | const auth = Hawk.client.message('example.com', 80, undefined, { credentials, timestamp: 1353809207, nonce: 'abc123' }); 538 | expect(auth).to.not.exist(); 539 | done(); 540 | }); 541 | 542 | it('errors on invalid message', (done) => { 543 | 544 | const credentials = { 545 | id: '123456', 546 | key: '2983d45yun89q', 547 | algorithm: 'sha1' 548 | }; 549 | 550 | const auth = Hawk.client.message('example.com', 80, 5, { credentials, timestamp: 1353809207, nonce: 'abc123' }); 551 | expect(auth).to.not.exist(); 552 | done(); 553 | }); 554 | 555 | it('errors on missing options', (done) => { 556 | 557 | const auth = Hawk.client.message('example.com', 80, 'I am the boodyman'); 558 | expect(auth).to.not.exist(); 559 | done(); 560 | }); 561 | 562 | it('errors on invalid credentials (id)', (done) => { 563 | 564 | const credentials = { 565 | key: '2983d45yun89q', 566 | algorithm: 'sha1' 567 | }; 568 | 569 | const auth = Hawk.client.message('example.com', 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' }); 570 | expect(auth).to.not.exist(); 571 | done(); 572 | }); 573 | 574 | it('errors on invalid credentials (key)', (done) => { 575 | 576 | const credentials = { 577 | id: '123456', 578 | algorithm: 'sha1' 579 | }; 580 | 581 | const auth = Hawk.client.message('example.com', 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' }); 582 | expect(auth).to.not.exist(); 583 | done(); 584 | }); 585 | }); 586 | }); 587 | -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | HTTP Hawk Authentication Scheme 5 | Copyright (c) 2012-2016, Eran Hammer 6 | BSD Licensed 7 | */ 8 | 9 | 10 | // Declare namespace 11 | 12 | const hawk = { 13 | internals: {} 14 | }; 15 | 16 | 17 | hawk.client = { 18 | 19 | // Generate an Authorization header for a given request 20 | 21 | /* 22 | uri: 'http://example.com/resource?a=b' or object generated by hawk.utils.parseUri() 23 | method: HTTP verb (e.g. 'GET', 'POST') 24 | options: { 25 | 26 | // Required 27 | 28 | credentials: { 29 | id: 'dh37fgj492je', 30 | key: 'aoijedoaijsdlaksjdl', 31 | algorithm: 'sha256' // 'sha1', 'sha256' 32 | }, 33 | 34 | // Optional 35 | 36 | ext: 'application-specific', // Application specific data sent via the ext attribute 37 | timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds 38 | nonce: '2334f34f', // A pre-generated nonce 39 | localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) 40 | payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) 41 | contentType: 'application/json', // Payload content-type (ignored if hash provided) 42 | hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash 43 | app: '24s23423f34dx', // Oz application id 44 | dlg: '234sz34tww3sd' // Oz delegated-by application id 45 | } 46 | */ 47 | 48 | header: function (uri, method, options) { 49 | 50 | const result = { 51 | field: '', 52 | artifacts: {} 53 | }; 54 | 55 | // Validate inputs 56 | 57 | if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') || 58 | !method || typeof method !== 'string' || 59 | !options || typeof options !== 'object') { 60 | 61 | result.err = 'Invalid argument type'; 62 | return result; 63 | } 64 | 65 | // Application time 66 | 67 | const timestamp = options.timestamp || hawk.utils.nowSec(options.localtimeOffsetMsec); 68 | 69 | // Validate credentials 70 | 71 | const credentials = options.credentials; 72 | if (!credentials || 73 | !credentials.id || 74 | !credentials.key || 75 | !credentials.algorithm) { 76 | 77 | result.err = 'Invalid credentials object'; 78 | return result; 79 | } 80 | 81 | if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { 82 | result.err = 'Unknown algorithm'; 83 | return result; 84 | } 85 | 86 | // Parse URI 87 | 88 | if (typeof uri === 'string') { 89 | uri = hawk.utils.parseUri(uri); 90 | } 91 | 92 | // Calculate signature 93 | 94 | const artifacts = { 95 | ts: timestamp, 96 | nonce: options.nonce || hawk.utils.randomString(6), 97 | method, 98 | resource: uri.resource, 99 | host: uri.host, 100 | port: uri.port, 101 | hash: options.hash, 102 | ext: options.ext, 103 | app: options.app, 104 | dlg: options.dlg 105 | }; 106 | 107 | result.artifacts = artifacts; 108 | 109 | // Calculate payload hash 110 | 111 | if (!artifacts.hash && 112 | (options.payload || options.payload === '')) { 113 | 114 | artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); 115 | } 116 | 117 | const mac = hawk.crypto.calculateMac('header', credentials, artifacts); 118 | 119 | // Construct header 120 | 121 | const hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed 122 | let header = 'Hawk id="' + credentials.id + 123 | '", ts="' + artifacts.ts + 124 | '", nonce="' + artifacts.nonce + 125 | (artifacts.hash ? '", hash="' + artifacts.hash : '') + 126 | (hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') + 127 | '", mac="' + mac + '"'; 128 | 129 | if (artifacts.app) { 130 | header += ', app="' + artifacts.app + 131 | (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"'; 132 | } 133 | 134 | result.field = header; 135 | 136 | return result; 137 | }, 138 | 139 | // Generate a bewit value for a given URI 140 | 141 | /* 142 | uri: 'http://example.com/resource?a=b' 143 | options: { 144 | 145 | // Required 146 | 147 | credentials: { 148 | id: 'dh37fgj492je', 149 | key: 'aoijedoaijsdlaksjdl', 150 | algorithm: 'sha256' // 'sha1', 'sha256' 151 | }, 152 | ttlSec: 60 * 60, // TTL in seconds 153 | 154 | // Optional 155 | 156 | ext: 'application-specific', // Application specific data sent via the ext attribute 157 | localtimeOffsetMsec: 400 // Time offset to sync with server time 158 | }; 159 | */ 160 | 161 | bewit: function (uri, options) { 162 | 163 | // Validate inputs 164 | 165 | if (!uri || 166 | (typeof uri !== 'string') || 167 | !options || 168 | typeof options !== 'object' || 169 | !options.ttlSec) { 170 | 171 | return ''; 172 | } 173 | 174 | options.ext = (options.ext === null || options.ext === undefined ? '' : options.ext); // Zero is valid value 175 | 176 | // Application time 177 | 178 | const now = hawk.utils.nowSec(options.localtimeOffsetMsec); 179 | 180 | // Validate credentials 181 | 182 | const credentials = options.credentials; 183 | if (!credentials || 184 | !credentials.id || 185 | !credentials.key || 186 | !credentials.algorithm) { 187 | 188 | return ''; 189 | } 190 | 191 | if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { 192 | return ''; 193 | } 194 | 195 | // Parse URI 196 | 197 | uri = hawk.utils.parseUri(uri); 198 | 199 | // Calculate signature 200 | 201 | const exp = now + options.ttlSec; 202 | const mac = hawk.crypto.calculateMac('bewit', credentials, { 203 | ts: exp, 204 | nonce: '', 205 | method: 'GET', 206 | resource: uri.resource, // Maintain trailing '?' and query params 207 | host: uri.host, 208 | port: uri.port, 209 | ext: options.ext 210 | }); 211 | 212 | // Construct bewit: id\exp\mac\ext 213 | 214 | const bewit = credentials.id + '\\' + exp + '\\' + mac + '\\' + options.ext; 215 | return hawk.utils.base64urlEncode(bewit); 216 | }, 217 | 218 | // Validate server response 219 | 220 | /* 221 | request: object created via 'new XMLHttpRequest()' after response received or fetch API 'Response' 222 | artifacts: object received from header().artifacts 223 | options: { 224 | payload: optional payload received 225 | required: specifies if a Server-Authorization header is required. Defaults to 'false' 226 | } 227 | */ 228 | 229 | authenticate: function (request, credentials, artifacts, options) { 230 | 231 | options = options || {}; 232 | 233 | const getHeader = function (name) { 234 | 235 | // Fetch API or plain headers 236 | 237 | if (request.headers) { 238 | return (typeof request.headers.get === 'function' ? request.headers.get(name) : request.headers[name]); 239 | } 240 | 241 | // XMLHttpRequest 242 | 243 | return (request.getResponseHeader ? request.getResponseHeader(name) : request.getHeader(name)); 244 | }; 245 | 246 | const wwwAuthenticate = getHeader('www-authenticate'); 247 | if (wwwAuthenticate) { 248 | 249 | // Parse HTTP WWW-Authenticate header 250 | 251 | const wwwAttributes = hawk.utils.parseAuthorizationHeader(wwwAuthenticate, ['ts', 'tsm', 'error']); 252 | if (!wwwAttributes) { 253 | return false; 254 | } 255 | 256 | if (wwwAttributes.ts) { 257 | const tsm = hawk.crypto.calculateTsMac(wwwAttributes.ts, credentials); 258 | if (tsm !== wwwAttributes.tsm) { 259 | return false; 260 | } 261 | 262 | hawk.utils.setNtpSecOffset(wwwAttributes.ts - Math.floor(Date.now() / 1000)); // Keep offset at 1 second precision 263 | } 264 | } 265 | 266 | // Parse HTTP Server-Authorization header 267 | 268 | const serverAuthorization = getHeader('server-authorization'); 269 | if (!serverAuthorization && 270 | !options.required) { 271 | 272 | return true; 273 | } 274 | 275 | const attributes = hawk.utils.parseAuthorizationHeader(serverAuthorization, ['mac', 'ext', 'hash']); 276 | if (!attributes) { 277 | return false; 278 | } 279 | 280 | const modArtifacts = { 281 | ts: artifacts.ts, 282 | nonce: artifacts.nonce, 283 | method: artifacts.method, 284 | resource: artifacts.resource, 285 | host: artifacts.host, 286 | port: artifacts.port, 287 | hash: attributes.hash, 288 | ext: attributes.ext, 289 | app: artifacts.app, 290 | dlg: artifacts.dlg 291 | }; 292 | 293 | const mac = hawk.crypto.calculateMac('response', credentials, modArtifacts); 294 | if (mac !== attributes.mac) { 295 | return false; 296 | } 297 | 298 | if (!options.payload && 299 | options.payload !== '') { 300 | 301 | return true; 302 | } 303 | 304 | if (!attributes.hash) { 305 | return false; 306 | } 307 | 308 | const calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, getHeader('content-type')); 309 | return (calculatedHash === attributes.hash); 310 | }, 311 | 312 | message: function (host, port, message, options) { 313 | 314 | // Validate inputs 315 | 316 | if (!host || typeof host !== 'string' || 317 | !port || typeof port !== 'number' || 318 | message === null || message === undefined || typeof message !== 'string' || 319 | !options || typeof options !== 'object') { 320 | 321 | return null; 322 | } 323 | 324 | // Application time 325 | 326 | const timestamp = options.timestamp || hawk.utils.nowSec(options.localtimeOffsetMsec); 327 | 328 | // Validate credentials 329 | 330 | const credentials = options.credentials; 331 | if (!credentials || 332 | !credentials.id || 333 | !credentials.key || 334 | !credentials.algorithm) { 335 | 336 | // Invalid credential object 337 | return null; 338 | } 339 | 340 | if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { 341 | return null; 342 | } 343 | 344 | // Calculate signature 345 | 346 | const artifacts = { 347 | ts: timestamp, 348 | nonce: options.nonce || hawk.utils.randomString(6), 349 | host, 350 | port, 351 | hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm) 352 | }; 353 | 354 | // Construct authorization 355 | 356 | const result = { 357 | id: credentials.id, 358 | ts: artifacts.ts, 359 | nonce: artifacts.nonce, 360 | hash: artifacts.hash, 361 | mac: hawk.crypto.calculateMac('message', credentials, artifacts) 362 | }; 363 | 364 | return result; 365 | }, 366 | 367 | authenticateTimestamp: function (message, credentials, updateClock) { // updateClock defaults to true 368 | 369 | const tsm = hawk.crypto.calculateTsMac(message.ts, credentials); 370 | if (tsm !== message.tsm) { 371 | return false; 372 | } 373 | 374 | if (updateClock !== false) { 375 | hawk.utils.setNtpSecOffset(message.ts - Math.floor(Date.now() / 1000)); // Keep offset at 1 second precision 376 | } 377 | 378 | return true; 379 | } 380 | }; 381 | 382 | 383 | hawk.crypto = { 384 | 385 | headerVersion: '1', 386 | 387 | algorithms: ['sha1', 'sha256'], 388 | 389 | calculateMac: function (type, credentials, options) { 390 | 391 | const normalized = hawk.crypto.generateNormalizedString(type, options); 392 | 393 | const hmac = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()](normalized, credentials.key); 394 | return hmac.toString(CryptoJS.enc.Base64); 395 | }, 396 | 397 | generateNormalizedString: function (type, options) { 398 | 399 | let normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' + 400 | options.ts + '\n' + 401 | options.nonce + '\n' + 402 | (options.method || '').toUpperCase() + '\n' + 403 | (options.resource || '') + '\n' + 404 | options.host.toLowerCase() + '\n' + 405 | options.port + '\n' + 406 | (options.hash || '') + '\n'; 407 | 408 | if (options.ext) { 409 | normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n'); 410 | } 411 | 412 | normalized += '\n'; 413 | 414 | if (options.app) { 415 | normalized += options.app + '\n' + 416 | (options.dlg || '') + '\n'; 417 | } 418 | 419 | return normalized; 420 | }, 421 | 422 | calculatePayloadHash: function (payload, algorithm, contentType) { 423 | 424 | const hash = CryptoJS.algo[algorithm.toUpperCase()].create(); 425 | hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n'); 426 | hash.update(hawk.utils.parseContentType(contentType) + '\n'); 427 | hash.update(payload); 428 | hash.update('\n'); 429 | return hash.finalize().toString(CryptoJS.enc.Base64); 430 | }, 431 | 432 | calculateTsMac: function (ts, credentials) { 433 | 434 | const hash = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()]('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n', credentials.key); 435 | return hash.toString(CryptoJS.enc.Base64); 436 | } 437 | }; 438 | 439 | 440 | // localStorage compatible interface 441 | 442 | hawk.internals.LocalStorage = function () { 443 | 444 | this._cache = {}; 445 | this.length = 0; 446 | 447 | this.getItem = function (key) { 448 | 449 | return this._cache.hasOwnProperty(key) ? String(this._cache[key]) : null; 450 | }; 451 | 452 | this.setItem = function (key, value) { 453 | 454 | this._cache[key] = String(value); 455 | this.length = Object.keys(this._cache).length; 456 | }; 457 | 458 | this.removeItem = function (key) { 459 | 460 | delete this._cache[key]; 461 | this.length = Object.keys(this._cache).length; 462 | }; 463 | 464 | this.clear = function () { 465 | 466 | this._cache = {}; 467 | this.length = 0; 468 | }; 469 | 470 | this.key = function (i) { 471 | 472 | return Object.keys(this._cache)[i || 0]; 473 | }; 474 | }; 475 | 476 | 477 | hawk.utils = { 478 | 479 | storage: new hawk.internals.LocalStorage(), 480 | 481 | setStorage: function (storage) { 482 | 483 | const ntpOffset = hawk.utils.storage.getItem('hawk_ntp_offset'); 484 | hawk.utils.storage = storage; 485 | if (ntpOffset) { 486 | hawk.utils.setNtpSecOffset(ntpOffset); 487 | } 488 | }, 489 | 490 | setNtpSecOffset: function (offset) { 491 | 492 | try { 493 | hawk.utils.storage.setItem('hawk_ntp_offset', offset); 494 | } 495 | catch (err) { 496 | console.error('[hawk] could not write to storage.'); 497 | console.error(err); 498 | } 499 | }, 500 | 501 | getNtpSecOffset: function () { 502 | 503 | const offset = hawk.utils.storage.getItem('hawk_ntp_offset'); 504 | if (!offset) { 505 | return 0; 506 | } 507 | 508 | return parseInt(offset, 10); 509 | }, 510 | 511 | now: function (localtimeOffsetMsec) { 512 | 513 | return Date.now() + (localtimeOffsetMsec || 0) + (hawk.utils.getNtpSecOffset() * 1000); 514 | }, 515 | 516 | nowSec: function (localtimeOffsetMsec) { 517 | 518 | return Math.floor(hawk.utils.now(localtimeOffsetMsec) / 1000); 519 | }, 520 | 521 | escapeHeaderAttribute: function (attribute) { 522 | 523 | return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); 524 | }, 525 | 526 | parseContentType: function (header) { 527 | 528 | if (!header) { 529 | return ''; 530 | } 531 | 532 | return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase(); 533 | }, 534 | 535 | parseAuthorizationHeader: function (header, keys) { 536 | 537 | if (!header) { 538 | return null; 539 | } 540 | 541 | const headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something] 542 | if (!headerParts) { 543 | return null; 544 | } 545 | 546 | const scheme = headerParts[1]; 547 | if (scheme.toLowerCase() !== 'hawk') { 548 | return null; 549 | } 550 | 551 | const attributesString = headerParts[2]; 552 | if (!attributesString) { 553 | return null; 554 | } 555 | 556 | const attributes = {}; 557 | const verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, ($0, $1, $2) => { 558 | 559 | // Check valid attribute names 560 | 561 | if (keys.indexOf($1) === -1) { 562 | return; 563 | } 564 | 565 | // Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9 566 | 567 | if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) { 568 | return; 569 | } 570 | 571 | // Check for duplicates 572 | 573 | if (attributes.hasOwnProperty($1)) { 574 | return; 575 | } 576 | 577 | attributes[$1] = $2; 578 | return ''; 579 | }); 580 | 581 | if (verify !== '') { 582 | return null; 583 | } 584 | 585 | return attributes; 586 | }, 587 | 588 | randomString: function (size) { 589 | 590 | const randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 591 | const len = randomSource.length; 592 | 593 | const result = []; 594 | for (let i = 0; i < size; ++i) { 595 | result[i] = randomSource[Math.floor(Math.random() * len)]; 596 | } 597 | 598 | return result.join(''); 599 | }, 600 | 601 | // 1 2 3 4 602 | uriRegex: /^([^:]+)\:\/\/(?:[^@/]*@)?([^\/:]+)(?:\:(\d+))?([^#]*)(?:#.*)?$/, // scheme://credentials@host:port/resource#fragment 603 | parseUri: function (input) { 604 | 605 | const parts = input.match(hawk.utils.uriRegex); 606 | if (!parts) { 607 | return { host: '', port: '', resource: '' }; 608 | } 609 | 610 | const scheme = parts[1].toLowerCase(); 611 | const uri = { 612 | host: parts[2], 613 | port: parts[3] || (scheme === 'http' ? '80' : (scheme === 'https' ? '443' : '')), 614 | resource: parts[4] 615 | }; 616 | 617 | return uri; 618 | }, 619 | 620 | base64urlEncode: function (value) { 621 | 622 | const wordArray = CryptoJS.enc.Utf8.parse(value); 623 | const encoded = CryptoJS.enc.Base64.stringify(wordArray); 624 | return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, ''); 625 | } 626 | }; 627 | 628 | 629 | // $lab:coverage:off$ 630 | /* eslint-disable */ 631 | 632 | // Based on: Crypto-JS v3.1.2 633 | // Copyright (c) 2009-2013, Jeff Mott. All rights reserved. 634 | // http://code.google.com/p/crypto-js/ 635 | // http://code.google.com/p/crypto-js/wiki/License 636 | 637 | var CryptoJS = CryptoJS || function (h, r) { var k = {}, l = k.lib = {}, n = function () { }, f = l.Base = { extend: function (a) { n.prototype = this; var b = new n; a && b.mixIn(a); b.hasOwnProperty("init") || (b.init = function () { b.$super.init.apply(this, arguments) }); b.init.prototype = b; b.$super = this; return b }, create: function () { var a = this.extend(); a.init.apply(a, arguments); return a }, init: function () { }, mixIn: function (a) { for (let b in a) a.hasOwnProperty(b) && (this[b] = a[b]); a.hasOwnProperty("toString") && (this.toString = a.toString) }, clone: function () { return this.init.prototype.extend(this) } }, j = l.WordArray = f.extend({ init: function (a, b) { a = this.words = a || []; this.sigBytes = b != r ? b : 4 * a.length }, toString: function (a) { return (a || s).stringify(this) }, concat: function (a) { var b = this.words, d = a.words, c = this.sigBytes; a = a.sigBytes; this.clamp(); if (c % 4) for (let e = 0; e < a; e++) b[c + e >>> 2] |= (d[e >>> 2] >>> 24 - 8 * (e % 4) & 255) << 24 - 8 * ((c + e) % 4); else if (65535 < d.length) for (let e = 0; e < a; e += 4) b[c + e >>> 2] = d[e >>> 2]; else b.push.apply(b, d); this.sigBytes += a; return this }, clamp: function () { var a = this.words, b = this.sigBytes; a[b >>> 2] &= 4294967295 << 32 - 8 * (b % 4); a.length = h.ceil(b / 4) }, clone: function () { var a = f.clone.call(this); a.words = this.words.slice(0); return a }, random: function (a) { for (let b = [], d = 0; d < a; d += 4) b.push(4294967296 * h.random() | 0); return new j.init(b, a) } }), m = k.enc = {}, s = m.Hex = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) { var e = b[c >>> 2] >>> 24 - 8 * (c % 4) & 255; d.push((e >>> 4).toString(16)); d.push((e & 15).toString(16)) } return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c += 2) d[c >>> 3] |= parseInt(a.substr(c, 2), 16) << 24 - 4 * (c % 8); return new j.init(d, b / 2) } }, p = m.Latin1 = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) d.push(String.fromCharCode(b[c >>> 2] >>> 24 - 8 * (c % 4) & 255)); return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c++) d[c >>> 2] |= (a.charCodeAt(c) & 255) << 24 - 8 * (c % 4); return new j.init(d, b) } }, t = m.Utf8 = { stringify: function (a) { try { return decodeURIComponent(escape(p.stringify(a))) } catch (b) { throw Error("Malformed UTF-8 data"); } }, parse: function (a) { return p.parse(unescape(encodeURIComponent(a))) } }, q = l.BufferedBlockAlgorithm = f.extend({ reset: function () { this._data = new j.init; this._nDataBytes = 0 }, _append: function (a) { "string" == typeof a && (a = t.parse(a)); this._data.concat(a); this._nDataBytes += a.sigBytes }, _process: function (a) { var b = this._data, d = b.words, c = b.sigBytes, e = this.blockSize, f = c / (4 * e), f = a ? h.ceil(f) : h.max((f | 0) - this._minBufferSize, 0); a = f * e; c = h.min(4 * a, c); if (a) { for (var g = 0; g < a; g += e) this._doProcessBlock(d, g); g = d.splice(0, a); b.sigBytes -= c } return new j.init(g, c) }, clone: function () { var a = f.clone.call(this); a._data = this._data.clone(); return a }, _minBufferSize: 0 }); l.Hasher = q.extend({ cfg: f.extend(), init: function (a) { this.cfg = this.cfg.extend(a); this.reset() }, reset: function () { q.reset.call(this); this._doReset() }, update: function (a) { this._append(a); this._process(); return this }, finalize: function (a) { a && this._append(a); return this._doFinalize() }, blockSize: 16, _createHelper: function (a) { return function (b, d) { return (new a.init(d)).finalize(b) } }, _createHmacHelper: function (a) { return function (b, d) { return (new u.HMAC.init(a, d)).finalize(b) } } }); var u = k.algo = {}; return k }(Math); 638 | (() => { var k = CryptoJS, b = k.lib, m = b.WordArray, l = b.Hasher, d = [], b = k.algo.SHA1 = l.extend({ _doReset: function () { this._hash = new m.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]) }, _doProcessBlock: function (n, p) { for (var a = this._hash.words, e = a[0], f = a[1], h = a[2], j = a[3], b = a[4], c = 0; 80 > c; c++) { if (16 > c) d[c] = n[p + c] | 0; else { var g = d[c - 3] ^ d[c - 8] ^ d[c - 14] ^ d[c - 16]; d[c] = g << 1 | g >>> 31 } g = (e << 5 | e >>> 27) + b + d[c]; g = 20 > c ? g + ((f & h | ~f & j) + 1518500249) : 40 > c ? g + ((f ^ h ^ j) + 1859775393) : 60 > c ? g + ((f & h | f & j | h & j) - 1894007588) : g + ((f ^ h ^ j) - 899497514); b = j; j = h; h = f << 30 | f >>> 2; f = e; e = g } a[0] = a[0] + e | 0; a[1] = a[1] + f | 0; a[2] = a[2] + h | 0; a[3] = a[3] + j | 0; a[4] = a[4] + b | 0 }, _doFinalize: function () { var b = this._data, d = b.words, a = 8 * this._nDataBytes, e = 8 * b.sigBytes; d[e >>> 5] |= 128 << 24 - e % 32; d[(e + 64 >>> 9 << 4) + 14] = Math.floor(a / 4294967296); d[(e + 64 >>> 9 << 4) + 15] = a; b.sigBytes = 4 * d.length; this._process(); return this._hash }, clone: function () { var b = l.clone.call(this); b._hash = this._hash.clone(); return b } }); k.SHA1 = l._createHelper(b); k.HmacSHA1 = l._createHmacHelper(b) })(); 639 | (function (k) { for (var g = CryptoJS, h = g.lib, v = h.WordArray, j = h.Hasher, h = g.algo, s = [], t = [], u = function (q) { return 4294967296 * (q - (q | 0)) | 0 }, l = 2, b = 0; 64 > b;) { var d; a: { d = l; for (var w = k.sqrt(d), r = 2; r <= w; r++) if (!(d % r)) { d = !1; break a } d = !0 } d && (8 > b && (s[b] = u(k.pow(l, 0.5))), t[b] = u(k.pow(l, 1 / 3)), b++); l++ } var n = [], h = h.SHA256 = j.extend({ _doReset: function () { this._hash = new v.init(s.slice(0)) }, _doProcessBlock: function (q, h) { for (var a = this._hash.words, c = a[0], d = a[1], b = a[2], k = a[3], f = a[4], g = a[5], j = a[6], l = a[7], e = 0; 64 > e; e++) { if (16 > e) n[e] = q[h + e] | 0; else { var m = n[e - 15], p = n[e - 2]; n[e] = ((m << 25 | m >>> 7) ^ (m << 14 | m >>> 18) ^ m >>> 3) + n[e - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + n[e - 16] } m = l + ((f << 26 | f >>> 6) ^ (f << 21 | f >>> 11) ^ (f << 7 | f >>> 25)) + (f & g ^ ~f & j) + t[e] + n[e]; p = ((c << 30 | c >>> 2) ^ (c << 19 | c >>> 13) ^ (c << 10 | c >>> 22)) + (c & d ^ c & b ^ d & b); l = j; j = g; g = f; f = k + m | 0; k = b; b = d; d = c; c = m + p | 0 } a[0] = a[0] + c | 0; a[1] = a[1] + d | 0; a[2] = a[2] + b | 0; a[3] = a[3] + k | 0; a[4] = a[4] + f | 0; a[5] = a[5] + g | 0; a[6] = a[6] + j | 0; a[7] = a[7] + l | 0 }, _doFinalize: function () { var d = this._data, b = d.words, a = 8 * this._nDataBytes, c = 8 * d.sigBytes; b[c >>> 5] |= 128 << 24 - c % 32; b[(c + 64 >>> 9 << 4) + 14] = k.floor(a / 4294967296); b[(c + 64 >>> 9 << 4) + 15] = a; d.sigBytes = 4 * b.length; this._process(); return this._hash }, clone: function () { var b = j.clone.call(this); b._hash = this._hash.clone(); return b } }); g.SHA256 = j._createHelper(h); g.HmacSHA256 = j._createHmacHelper(h) })(Math); 640 | (() => { var c = CryptoJS, k = c.enc.Utf8; c.algo.HMAC = c.lib.Base.extend({ init: function (a, b) { a = this._hasher = new a.init; "string" == typeof b && (b = k.parse(b)); var c = a.blockSize, e = 4 * c; b.sigBytes > e && (b = a.finalize(b)); b.clamp(); for (var f = this._oKey = b.clone(), g = this._iKey = b.clone(), h = f.words, j = g.words, d = 0; d < c; d++) h[d] ^= 1549556828, j[d] ^= 909522486; f.sigBytes = g.sigBytes = e; this.reset() }, reset: function () { var a = this._hasher; a.reset(); a.update(this._iKey) }, update: function (a) { this._hasher.update(a); return this }, finalize: function (a) { var b = this._hasher; a = b.finalize(a); b.reset(); return b.finalize(this._oKey.clone().concat(a)) } }) })(); 641 | (() => { var h = CryptoJS, j = h.lib.WordArray; h.enc.Base64 = { stringify: function (b) { var e = b.words, f = b.sigBytes, c = this._map; b.clamp(); b = []; for (var a = 0; a < f; a += 3) for (var d = (e[a >>> 2] >>> 24 - 8 * (a % 4) & 255) << 16 | (e[a + 1 >>> 2] >>> 24 - 8 * ((a + 1) % 4) & 255) << 8 | e[a + 2 >>> 2] >>> 24 - 8 * ((a + 2) % 4) & 255, g = 0; 4 > g && a + 0.75 * g < f; g++) b.push(c.charAt(d >>> 6 * (3 - g) & 63)); if (e = c.charAt(64)) for (; b.length % 4;) b.push(e); return b.join("") }, parse: function (b) { var e = b.length, f = this._map, c = f.charAt(64); c && (c = b.indexOf(c), -1 != c && (e = c)); for (var c = [], a = 0, d = 0; d < e; d++) if (d % 4) { var g = f.indexOf(b.charAt(d - 1)) << 2 * (d % 4), h = f.indexOf(b.charAt(d)) >>> 6 - 2 * (d % 4); c[a >>> 2] |= (g | h) << 24 - 8 * (a % 4); a++ } return j.create(c, a) }, _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" } })(); 642 | 643 | 644 | hawk.crypto.utils = CryptoJS; 645 | 646 | // Export if used as a module 647 | 648 | if (typeof module !== 'undefined' && module.exports) { 649 | module.exports = hawk; 650 | } 651 | 652 | /* eslint-enable */ 653 | // $lab:coverage:on$ 654 | -------------------------------------------------------------------------------- /dist/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | HTTP Hawk Authentication Scheme 5 | Copyright (c) 2012-2016, Eran Hammer 6 | BSD Licensed 7 | */ 8 | 9 | // Declare namespace 10 | 11 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 12 | 13 | var hawk = { 14 | internals: {} 15 | }; 16 | 17 | hawk.client = { 18 | 19 | // Generate an Authorization header for a given request 20 | 21 | /* 22 | uri: 'http://example.com/resource?a=b' or object generated by hawk.utils.parseUri() 23 | method: HTTP verb (e.g. 'GET', 'POST') 24 | options: { 25 | // Required 26 | credentials: { 27 | id: 'dh37fgj492je', 28 | key: 'aoijedoaijsdlaksjdl', 29 | algorithm: 'sha256' // 'sha1', 'sha256' 30 | }, 31 | // Optional 32 | ext: 'application-specific', // Application specific data sent via the ext attribute 33 | timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds 34 | nonce: '2334f34f', // A pre-generated nonce 35 | localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) 36 | payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) 37 | contentType: 'application/json', // Payload content-type (ignored if hash provided) 38 | hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash 39 | app: '24s23423f34dx', // Oz application id 40 | dlg: '234sz34tww3sd' // Oz delegated-by application id 41 | } 42 | */ 43 | 44 | header: function header(uri, method, options) { 45 | 46 | var result = { 47 | field: '', 48 | artifacts: {} 49 | }; 50 | 51 | // Validate inputs 52 | 53 | if (!uri || typeof uri !== 'string' && (typeof uri === 'undefined' ? 'undefined' : _typeof(uri)) !== 'object' || !method || typeof method !== 'string' || !options || (typeof options === 'undefined' ? 'undefined' : _typeof(options)) !== 'object') { 54 | 55 | result.err = 'Invalid argument type'; 56 | return result; 57 | } 58 | 59 | // Application time 60 | 61 | var timestamp = options.timestamp || hawk.utils.nowSec(options.localtimeOffsetMsec); 62 | 63 | // Validate credentials 64 | 65 | var credentials = options.credentials; 66 | if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { 67 | 68 | result.err = 'Invalid credentials object'; 69 | return result; 70 | } 71 | 72 | if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { 73 | result.err = 'Unknown algorithm'; 74 | return result; 75 | } 76 | 77 | // Parse URI 78 | 79 | if (typeof uri === 'string') { 80 | uri = hawk.utils.parseUri(uri); 81 | } 82 | 83 | // Calculate signature 84 | 85 | var artifacts = { 86 | ts: timestamp, 87 | nonce: options.nonce || hawk.utils.randomString(6), 88 | method: method, 89 | resource: uri.resource, 90 | host: uri.host, 91 | port: uri.port, 92 | hash: options.hash, 93 | ext: options.ext, 94 | app: options.app, 95 | dlg: options.dlg 96 | }; 97 | 98 | result.artifacts = artifacts; 99 | 100 | // Calculate payload hash 101 | 102 | if (!artifacts.hash && (options.payload || options.payload === '')) { 103 | 104 | artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); 105 | } 106 | 107 | var mac = hawk.crypto.calculateMac('header', credentials, artifacts); 108 | 109 | // Construct header 110 | 111 | var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed 112 | var header = 'Hawk id="' + credentials.id + '", ts="' + artifacts.ts + '", nonce="' + artifacts.nonce + (artifacts.hash ? '", hash="' + artifacts.hash : '') + (hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') + '", mac="' + mac + '"'; 113 | 114 | if (artifacts.app) { 115 | header += ', app="' + artifacts.app + (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"'; 116 | } 117 | 118 | result.field = header; 119 | 120 | return result; 121 | }, 122 | 123 | // Generate a bewit value for a given URI 124 | 125 | /* 126 | uri: 'http://example.com/resource?a=b' 127 | options: { 128 | // Required 129 | credentials: { 130 | id: 'dh37fgj492je', 131 | key: 'aoijedoaijsdlaksjdl', 132 | algorithm: 'sha256' // 'sha1', 'sha256' 133 | }, 134 | ttlSec: 60 * 60, // TTL in seconds 135 | // Optional 136 | ext: 'application-specific', // Application specific data sent via the ext attribute 137 | localtimeOffsetMsec: 400 // Time offset to sync with server time 138 | }; 139 | */ 140 | 141 | bewit: function bewit(uri, options) { 142 | 143 | // Validate inputs 144 | 145 | if (!uri || typeof uri !== 'string' || !options || (typeof options === 'undefined' ? 'undefined' : _typeof(options)) !== 'object' || !options.ttlSec) { 146 | 147 | return ''; 148 | } 149 | 150 | options.ext = options.ext === null || options.ext === undefined ? '' : options.ext; // Zero is valid value 151 | 152 | // Application time 153 | 154 | var now = hawk.utils.nowSec(options.localtimeOffsetMsec); 155 | 156 | // Validate credentials 157 | 158 | var credentials = options.credentials; 159 | if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { 160 | 161 | return ''; 162 | } 163 | 164 | if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { 165 | return ''; 166 | } 167 | 168 | // Parse URI 169 | 170 | uri = hawk.utils.parseUri(uri); 171 | 172 | // Calculate signature 173 | 174 | var exp = now + options.ttlSec; 175 | var mac = hawk.crypto.calculateMac('bewit', credentials, { 176 | ts: exp, 177 | nonce: '', 178 | method: 'GET', 179 | resource: uri.resource, // Maintain trailing '?' and query params 180 | host: uri.host, 181 | port: uri.port, 182 | ext: options.ext 183 | }); 184 | 185 | // Construct bewit: id\exp\mac\ext 186 | 187 | var bewit = credentials.id + '\\' + exp + '\\' + mac + '\\' + options.ext; 188 | return hawk.utils.base64urlEncode(bewit); 189 | }, 190 | 191 | // Validate server response 192 | 193 | /* 194 | request: object created via 'new XMLHttpRequest()' after response received or fetch API 'Response' 195 | artifacts: object received from header().artifacts 196 | options: { 197 | payload: optional payload received 198 | required: specifies if a Server-Authorization header is required. Defaults to 'false' 199 | } 200 | */ 201 | 202 | authenticate: function authenticate(request, credentials, artifacts, options) { 203 | 204 | options = options || {}; 205 | 206 | var getHeader = function getHeader(name) { 207 | 208 | // Fetch API or plain headers 209 | 210 | if (request.headers) { 211 | return typeof request.headers.get === 'function' ? request.headers.get(name) : request.headers[name]; 212 | } 213 | 214 | // XMLHttpRequest 215 | 216 | return request.getResponseHeader ? request.getResponseHeader(name) : request.getHeader(name); 217 | }; 218 | 219 | var wwwAuthenticate = getHeader('www-authenticate'); 220 | if (wwwAuthenticate) { 221 | 222 | // Parse HTTP WWW-Authenticate header 223 | 224 | var wwwAttributes = hawk.utils.parseAuthorizationHeader(wwwAuthenticate, ['ts', 'tsm', 'error']); 225 | if (!wwwAttributes) { 226 | return false; 227 | } 228 | 229 | if (wwwAttributes.ts) { 230 | var tsm = hawk.crypto.calculateTsMac(wwwAttributes.ts, credentials); 231 | if (tsm !== wwwAttributes.tsm) { 232 | return false; 233 | } 234 | 235 | hawk.utils.setNtpSecOffset(wwwAttributes.ts - Math.floor(Date.now() / 1000)); // Keep offset at 1 second precision 236 | } 237 | } 238 | 239 | // Parse HTTP Server-Authorization header 240 | 241 | var serverAuthorization = getHeader('server-authorization'); 242 | if (!serverAuthorization && !options.required) { 243 | 244 | return true; 245 | } 246 | 247 | var attributes = hawk.utils.parseAuthorizationHeader(serverAuthorization, ['mac', 'ext', 'hash']); 248 | if (!attributes) { 249 | return false; 250 | } 251 | 252 | var modArtifacts = { 253 | ts: artifacts.ts, 254 | nonce: artifacts.nonce, 255 | method: artifacts.method, 256 | resource: artifacts.resource, 257 | host: artifacts.host, 258 | port: artifacts.port, 259 | hash: attributes.hash, 260 | ext: attributes.ext, 261 | app: artifacts.app, 262 | dlg: artifacts.dlg 263 | }; 264 | 265 | var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts); 266 | if (mac !== attributes.mac) { 267 | return false; 268 | } 269 | 270 | if (!options.payload && options.payload !== '') { 271 | 272 | return true; 273 | } 274 | 275 | if (!attributes.hash) { 276 | return false; 277 | } 278 | 279 | var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, getHeader('content-type')); 280 | return calculatedHash === attributes.hash; 281 | }, 282 | 283 | message: function message(host, port, _message, options) { 284 | 285 | // Validate inputs 286 | 287 | if (!host || typeof host !== 'string' || !port || typeof port !== 'number' || _message === null || _message === undefined || typeof _message !== 'string' || !options || (typeof options === 'undefined' ? 'undefined' : _typeof(options)) !== 'object') { 288 | 289 | return null; 290 | } 291 | 292 | // Application time 293 | 294 | var timestamp = options.timestamp || hawk.utils.nowSec(options.localtimeOffsetMsec); 295 | 296 | // Validate credentials 297 | 298 | var credentials = options.credentials; 299 | if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { 300 | 301 | // Invalid credential object 302 | return null; 303 | } 304 | 305 | if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { 306 | return null; 307 | } 308 | 309 | // Calculate signature 310 | 311 | var artifacts = { 312 | ts: timestamp, 313 | nonce: options.nonce || hawk.utils.randomString(6), 314 | host: host, 315 | port: port, 316 | hash: hawk.crypto.calculatePayloadHash(_message, credentials.algorithm) 317 | }; 318 | 319 | // Construct authorization 320 | 321 | var result = { 322 | id: credentials.id, 323 | ts: artifacts.ts, 324 | nonce: artifacts.nonce, 325 | hash: artifacts.hash, 326 | mac: hawk.crypto.calculateMac('message', credentials, artifacts) 327 | }; 328 | 329 | return result; 330 | }, 331 | 332 | authenticateTimestamp: function authenticateTimestamp(message, credentials, updateClock) { 333 | // updateClock defaults to true 334 | 335 | var tsm = hawk.crypto.calculateTsMac(message.ts, credentials); 336 | if (tsm !== message.tsm) { 337 | return false; 338 | } 339 | 340 | if (updateClock !== false) { 341 | hawk.utils.setNtpSecOffset(message.ts - Math.floor(Date.now() / 1000)); // Keep offset at 1 second precision 342 | } 343 | 344 | return true; 345 | } 346 | }; 347 | 348 | hawk.crypto = { 349 | 350 | headerVersion: '1', 351 | 352 | algorithms: ['sha1', 'sha256'], 353 | 354 | calculateMac: function calculateMac(type, credentials, options) { 355 | 356 | var normalized = hawk.crypto.generateNormalizedString(type, options); 357 | 358 | var hmac = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()](normalized, credentials.key); 359 | return hmac.toString(CryptoJS.enc.Base64); 360 | }, 361 | 362 | generateNormalizedString: function generateNormalizedString(type, options) { 363 | 364 | var normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' + options.ts + '\n' + options.nonce + '\n' + (options.method || '').toUpperCase() + '\n' + (options.resource || '') + '\n' + options.host.toLowerCase() + '\n' + options.port + '\n' + (options.hash || '') + '\n'; 365 | 366 | if (options.ext) { 367 | normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n'); 368 | } 369 | 370 | normalized += '\n'; 371 | 372 | if (options.app) { 373 | normalized += options.app + '\n' + (options.dlg || '') + '\n'; 374 | } 375 | 376 | return normalized; 377 | }, 378 | 379 | calculatePayloadHash: function calculatePayloadHash(payload, algorithm, contentType) { 380 | 381 | var hash = CryptoJS.algo[algorithm.toUpperCase()].create(); 382 | hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n'); 383 | hash.update(hawk.utils.parseContentType(contentType) + '\n'); 384 | hash.update(payload); 385 | hash.update('\n'); 386 | return hash.finalize().toString(CryptoJS.enc.Base64); 387 | }, 388 | 389 | calculateTsMac: function calculateTsMac(ts, credentials) { 390 | 391 | var hash = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()]('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n', credentials.key); 392 | return hash.toString(CryptoJS.enc.Base64); 393 | } 394 | }; 395 | 396 | // localStorage compatible interface 397 | 398 | hawk.internals.LocalStorage = function () { 399 | 400 | this._cache = {}; 401 | this.length = 0; 402 | 403 | this.getItem = function (key) { 404 | 405 | return this._cache.hasOwnProperty(key) ? String(this._cache[key]) : null; 406 | }; 407 | 408 | this.setItem = function (key, value) { 409 | 410 | this._cache[key] = String(value); 411 | this.length = Object.keys(this._cache).length; 412 | }; 413 | 414 | this.removeItem = function (key) { 415 | 416 | delete this._cache[key]; 417 | this.length = Object.keys(this._cache).length; 418 | }; 419 | 420 | this.clear = function () { 421 | 422 | this._cache = {}; 423 | this.length = 0; 424 | }; 425 | 426 | this.key = function (i) { 427 | 428 | return Object.keys(this._cache)[i || 0]; 429 | }; 430 | }; 431 | 432 | hawk.utils = { 433 | 434 | storage: new hawk.internals.LocalStorage(), 435 | 436 | setStorage: function setStorage(storage) { 437 | 438 | var ntpOffset = hawk.utils.storage.getItem('hawk_ntp_offset'); 439 | hawk.utils.storage = storage; 440 | if (ntpOffset) { 441 | hawk.utils.setNtpSecOffset(ntpOffset); 442 | } 443 | }, 444 | 445 | setNtpSecOffset: function setNtpSecOffset(offset) { 446 | 447 | try { 448 | hawk.utils.storage.setItem('hawk_ntp_offset', offset); 449 | } catch (err) { 450 | console.error('[hawk] could not write to storage.'); 451 | console.error(err); 452 | } 453 | }, 454 | 455 | getNtpSecOffset: function getNtpSecOffset() { 456 | 457 | var offset = hawk.utils.storage.getItem('hawk_ntp_offset'); 458 | if (!offset) { 459 | return 0; 460 | } 461 | 462 | return parseInt(offset, 10); 463 | }, 464 | 465 | now: function now(localtimeOffsetMsec) { 466 | 467 | return Date.now() + (localtimeOffsetMsec || 0) + hawk.utils.getNtpSecOffset() * 1000; 468 | }, 469 | 470 | nowSec: function nowSec(localtimeOffsetMsec) { 471 | 472 | return Math.floor(hawk.utils.now(localtimeOffsetMsec) / 1000); 473 | }, 474 | 475 | escapeHeaderAttribute: function escapeHeaderAttribute(attribute) { 476 | 477 | return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); 478 | }, 479 | 480 | parseContentType: function parseContentType(header) { 481 | 482 | if (!header) { 483 | return ''; 484 | } 485 | 486 | return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase(); 487 | }, 488 | 489 | parseAuthorizationHeader: function parseAuthorizationHeader(header, keys) { 490 | 491 | if (!header) { 492 | return null; 493 | } 494 | 495 | var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something] 496 | if (!headerParts) { 497 | return null; 498 | } 499 | 500 | var scheme = headerParts[1]; 501 | if (scheme.toLowerCase() !== 'hawk') { 502 | return null; 503 | } 504 | 505 | var attributesString = headerParts[2]; 506 | if (!attributesString) { 507 | return null; 508 | } 509 | 510 | var attributes = {}; 511 | var verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) { 512 | 513 | // Check valid attribute names 514 | 515 | if (keys.indexOf($1) === -1) { 516 | return; 517 | } 518 | 519 | // Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9 520 | 521 | if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) { 522 | return; 523 | } 524 | 525 | // Check for duplicates 526 | 527 | if (attributes.hasOwnProperty($1)) { 528 | return; 529 | } 530 | 531 | attributes[$1] = $2; 532 | return ''; 533 | }); 534 | 535 | if (verify !== '') { 536 | return null; 537 | } 538 | 539 | return attributes; 540 | }, 541 | 542 | randomString: function randomString(size) { 543 | 544 | var randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 545 | var len = randomSource.length; 546 | 547 | var result = []; 548 | for (var i = 0; i < size; ++i) { 549 | result[i] = randomSource[Math.floor(Math.random() * len)]; 550 | } 551 | 552 | return result.join(''); 553 | }, 554 | 555 | // 1 2 3 4 556 | uriRegex: /^([^:]+)\:\/\/(?:[^@/]*@)?([^\/:]+)(?:\:(\d+))?([^#]*)(?:#.*)?$/, // scheme://credentials@host:port/resource#fragment 557 | parseUri: function parseUri(input) { 558 | 559 | var parts = input.match(hawk.utils.uriRegex); 560 | if (!parts) { 561 | return { host: '', port: '', resource: '' }; 562 | } 563 | 564 | var scheme = parts[1].toLowerCase(); 565 | var uri = { 566 | host: parts[2], 567 | port: parts[3] || (scheme === 'http' ? '80' : scheme === 'https' ? '443' : ''), 568 | resource: parts[4] 569 | }; 570 | 571 | return uri; 572 | }, 573 | 574 | base64urlEncode: function base64urlEncode(value) { 575 | 576 | var wordArray = CryptoJS.enc.Utf8.parse(value); 577 | var encoded = CryptoJS.enc.Base64.stringify(wordArray); 578 | return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, ''); 579 | } 580 | }; 581 | 582 | // $lab:coverage:off$ 583 | /* eslint-disable */ 584 | 585 | // Based on: Crypto-JS v3.1.2 586 | // Copyright (c) 2009-2013, Jeff Mott. All rights reserved. 587 | // http://code.google.com/p/crypto-js/ 588 | // http://code.google.com/p/crypto-js/wiki/License 589 | 590 | var CryptoJS = CryptoJS || function (h, r) { 591 | var k = {}, 592 | l = k.lib = {}, 593 | n = function n() {}, 594 | f = l.Base = { extend: function extend(a) { 595 | n.prototype = this;var b = new n();a && b.mixIn(a);b.hasOwnProperty("init") || (b.init = function () { 596 | b.$super.init.apply(this, arguments); 597 | });b.init.prototype = b;b.$super = this;return b; 598 | }, create: function create() { 599 | var a = this.extend();a.init.apply(a, arguments);return a; 600 | }, init: function init() {}, mixIn: function mixIn(a) { 601 | for (var _b in a) { 602 | a.hasOwnProperty(_b) && (this[_b] = a[_b]); 603 | }a.hasOwnProperty("toString") && (this.toString = a.toString); 604 | }, clone: function clone() { 605 | return this.init.prototype.extend(this); 606 | } }, 607 | j = l.WordArray = f.extend({ init: function init(a, b) { 608 | a = this.words = a || [];this.sigBytes = b != r ? b : 4 * a.length; 609 | }, toString: function toString(a) { 610 | return (a || s).stringify(this); 611 | }, concat: function concat(a) { 612 | var b = this.words, 613 | d = a.words, 614 | c = this.sigBytes;a = a.sigBytes;this.clamp();if (c % 4) for (var e = 0; e < a; e++) { 615 | b[c + e >>> 2] |= (d[e >>> 2] >>> 24 - 8 * (e % 4) & 255) << 24 - 8 * ((c + e) % 4); 616 | } else if (65535 < d.length) for (var _e = 0; _e < a; _e += 4) { 617 | b[c + _e >>> 2] = d[_e >>> 2]; 618 | } else b.push.apply(b, d);this.sigBytes += a;return this; 619 | }, clamp: function clamp() { 620 | var a = this.words, 621 | b = this.sigBytes;a[b >>> 2] &= 4294967295 << 32 - 8 * (b % 4);a.length = h.ceil(b / 4); 622 | }, clone: function clone() { 623 | var a = f.clone.call(this);a.words = this.words.slice(0);return a; 624 | }, random: function random(a) { 625 | for (var _b2 = [], d = 0; d < a; d += 4) { 626 | _b2.push(4294967296 * h.random() | 0); 627 | }return new j.init(b, a); 628 | } }), 629 | m = k.enc = {}, 630 | s = m.Hex = { stringify: function stringify(a) { 631 | var b = a.words;a = a.sigBytes;for (var d = [], c = 0; c < a; c++) { 632 | var e = b[c >>> 2] >>> 24 - 8 * (c % 4) & 255;d.push((e >>> 4).toString(16));d.push((e & 15).toString(16)); 633 | }return d.join(""); 634 | }, parse: function parse(a) { 635 | for (var b = a.length, d = [], c = 0; c < b; c += 2) { 636 | d[c >>> 3] |= parseInt(a.substr(c, 2), 16) << 24 - 4 * (c % 8); 637 | }return new j.init(d, b / 2); 638 | } }, 639 | p = m.Latin1 = { stringify: function stringify(a) { 640 | var b = a.words;a = a.sigBytes;for (var d = [], c = 0; c < a; c++) { 641 | d.push(String.fromCharCode(b[c >>> 2] >>> 24 - 8 * (c % 4) & 255)); 642 | }return d.join(""); 643 | }, parse: function parse(a) { 644 | for (var b = a.length, d = [], c = 0; c < b; c++) { 645 | d[c >>> 2] |= (a.charCodeAt(c) & 255) << 24 - 8 * (c % 4); 646 | }return new j.init(d, b); 647 | } }, 648 | t = m.Utf8 = { stringify: function stringify(a) { 649 | try { 650 | return decodeURIComponent(escape(p.stringify(a))); 651 | } catch (b) { 652 | throw Error("Malformed UTF-8 data"); 653 | } 654 | }, parse: function parse(a) { 655 | return p.parse(unescape(encodeURIComponent(a))); 656 | } }, 657 | q = l.BufferedBlockAlgorithm = f.extend({ reset: function reset() { 658 | this._data = new j.init();this._nDataBytes = 0; 659 | }, _append: function _append(a) { 660 | "string" == typeof a && (a = t.parse(a));this._data.concat(a);this._nDataBytes += a.sigBytes; 661 | }, _process: function _process(a) { 662 | var b = this._data, 663 | d = b.words, 664 | c = b.sigBytes, 665 | e = this.blockSize, 666 | f = c / (4 * e), 667 | f = a ? h.ceil(f) : h.max((f | 0) - this._minBufferSize, 0);a = f * e;c = h.min(4 * a, c);if (a) { 668 | for (var g = 0; g < a; g += e) { 669 | this._doProcessBlock(d, g); 670 | }g = d.splice(0, a);b.sigBytes -= c; 671 | }return new j.init(g, c); 672 | }, clone: function clone() { 673 | var a = f.clone.call(this);a._data = this._data.clone();return a; 674 | }, _minBufferSize: 0 });l.Hasher = q.extend({ cfg: f.extend(), init: function init(a) { 675 | this.cfg = this.cfg.extend(a);this.reset(); 676 | }, reset: function reset() { 677 | q.reset.call(this);this._doReset(); 678 | }, update: function update(a) { 679 | this._append(a);this._process();return this; 680 | }, finalize: function finalize(a) { 681 | a && this._append(a);return this._doFinalize(); 682 | }, blockSize: 16, _createHelper: function _createHelper(a) { 683 | return function (b, d) { 684 | return new a.init(d).finalize(b); 685 | }; 686 | }, _createHmacHelper: function _createHmacHelper(a) { 687 | return function (b, d) { 688 | return new u.HMAC.init(a, d).finalize(b); 689 | }; 690 | } });var u = k.algo = {};return k; 691 | }(Math); 692 | (function () { 693 | var k = CryptoJS, 694 | b = k.lib, 695 | m = b.WordArray, 696 | l = b.Hasher, 697 | d = [], 698 | b = k.algo.SHA1 = l.extend({ _doReset: function _doReset() { 699 | this._hash = new m.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]); 700 | }, _doProcessBlock: function _doProcessBlock(n, p) { 701 | for (var a = this._hash.words, e = a[0], f = a[1], h = a[2], j = a[3], b = a[4], c = 0; 80 > c; c++) { 702 | if (16 > c) d[c] = n[p + c] | 0;else { 703 | var g = d[c - 3] ^ d[c - 8] ^ d[c - 14] ^ d[c - 16];d[c] = g << 1 | g >>> 31; 704 | }g = (e << 5 | e >>> 27) + b + d[c];g = 20 > c ? g + ((f & h | ~f & j) + 1518500249) : 40 > c ? g + ((f ^ h ^ j) + 1859775393) : 60 > c ? g + ((f & h | f & j | h & j) - 1894007588) : g + ((f ^ h ^ j) - 899497514);b = j;j = h;h = f << 30 | f >>> 2;f = e;e = g; 705 | }a[0] = a[0] + e | 0;a[1] = a[1] + f | 0;a[2] = a[2] + h | 0;a[3] = a[3] + j | 0;a[4] = a[4] + b | 0; 706 | }, _doFinalize: function _doFinalize() { 707 | var b = this._data, 708 | d = b.words, 709 | a = 8 * this._nDataBytes, 710 | e = 8 * b.sigBytes;d[e >>> 5] |= 128 << 24 - e % 32;d[(e + 64 >>> 9 << 4) + 14] = Math.floor(a / 4294967296);d[(e + 64 >>> 9 << 4) + 15] = a;b.sigBytes = 4 * d.length;this._process();return this._hash; 711 | }, clone: function clone() { 712 | var b = l.clone.call(this);b._hash = this._hash.clone();return b; 713 | } });k.SHA1 = l._createHelper(b);k.HmacSHA1 = l._createHmacHelper(b); 714 | })(); 715 | (function (k) { 716 | for (var g = CryptoJS, h = g.lib, v = h.WordArray, j = h.Hasher, h = g.algo, s = [], t = [], u = function u(q) { 717 | return 4294967296 * (q - (q | 0)) | 0; 718 | }, l = 2, b = 0; 64 > b;) { 719 | var d;a: { 720 | d = l;for (var w = k.sqrt(d), r = 2; r <= w; r++) { 721 | if (!(d % r)) { 722 | d = !1;break a; 723 | } 724 | }d = !0; 725 | }d && (8 > b && (s[b] = u(k.pow(l, 0.5))), t[b] = u(k.pow(l, 1 / 3)), b++);l++; 726 | }var n = [], 727 | h = h.SHA256 = j.extend({ _doReset: function _doReset() { 728 | this._hash = new v.init(s.slice(0)); 729 | }, _doProcessBlock: function _doProcessBlock(q, h) { 730 | for (var a = this._hash.words, c = a[0], d = a[1], b = a[2], k = a[3], f = a[4], g = a[5], j = a[6], l = a[7], e = 0; 64 > e; e++) { 731 | if (16 > e) n[e] = q[h + e] | 0;else { 732 | var m = n[e - 15], 733 | p = n[e - 2];n[e] = ((m << 25 | m >>> 7) ^ (m << 14 | m >>> 18) ^ m >>> 3) + n[e - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + n[e - 16]; 734 | }m = l + ((f << 26 | f >>> 6) ^ (f << 21 | f >>> 11) ^ (f << 7 | f >>> 25)) + (f & g ^ ~f & j) + t[e] + n[e];p = ((c << 30 | c >>> 2) ^ (c << 19 | c >>> 13) ^ (c << 10 | c >>> 22)) + (c & d ^ c & b ^ d & b);l = j;j = g;g = f;f = k + m | 0;k = b;b = d;d = c;c = m + p | 0; 735 | }a[0] = a[0] + c | 0;a[1] = a[1] + d | 0;a[2] = a[2] + b | 0;a[3] = a[3] + k | 0;a[4] = a[4] + f | 0;a[5] = a[5] + g | 0;a[6] = a[6] + j | 0;a[7] = a[7] + l | 0; 736 | }, _doFinalize: function _doFinalize() { 737 | var d = this._data, 738 | b = d.words, 739 | a = 8 * this._nDataBytes, 740 | c = 8 * d.sigBytes;b[c >>> 5] |= 128 << 24 - c % 32;b[(c + 64 >>> 9 << 4) + 14] = k.floor(a / 4294967296);b[(c + 64 >>> 9 << 4) + 15] = a;d.sigBytes = 4 * b.length;this._process();return this._hash; 741 | }, clone: function clone() { 742 | var b = j.clone.call(this);b._hash = this._hash.clone();return b; 743 | } });g.SHA256 = j._createHelper(h);g.HmacSHA256 = j._createHmacHelper(h); 744 | })(Math); 745 | (function () { 746 | var c = CryptoJS, 747 | k = c.enc.Utf8;c.algo.HMAC = c.lib.Base.extend({ init: function init(a, b) { 748 | a = this._hasher = new a.init();"string" == typeof b && (b = k.parse(b));var c = a.blockSize, 749 | e = 4 * c;b.sigBytes > e && (b = a.finalize(b));b.clamp();for (var f = this._oKey = b.clone(), g = this._iKey = b.clone(), h = f.words, j = g.words, d = 0; d < c; d++) { 750 | h[d] ^= 1549556828, j[d] ^= 909522486; 751 | }f.sigBytes = g.sigBytes = e;this.reset(); 752 | }, reset: function reset() { 753 | var a = this._hasher;a.reset();a.update(this._iKey); 754 | }, update: function update(a) { 755 | this._hasher.update(a);return this; 756 | }, finalize: function finalize(a) { 757 | var b = this._hasher;a = b.finalize(a);b.reset();return b.finalize(this._oKey.clone().concat(a)); 758 | } }); 759 | })(); 760 | (function () { 761 | var h = CryptoJS, 762 | j = h.lib.WordArray;h.enc.Base64 = { stringify: function stringify(b) { 763 | var e = b.words, 764 | f = b.sigBytes, 765 | c = this._map;b.clamp();b = [];for (var a = 0; a < f; a += 3) { 766 | for (var d = (e[a >>> 2] >>> 24 - 8 * (a % 4) & 255) << 16 | (e[a + 1 >>> 2] >>> 24 - 8 * ((a + 1) % 4) & 255) << 8 | e[a + 2 >>> 2] >>> 24 - 8 * ((a + 2) % 4) & 255, g = 0; 4 > g && a + 0.75 * g < f; g++) { 767 | b.push(c.charAt(d >>> 6 * (3 - g) & 63)); 768 | } 769 | }if (e = c.charAt(64)) for (; b.length % 4;) { 770 | b.push(e); 771 | }return b.join(""); 772 | }, parse: function parse(b) { 773 | var e = b.length, 774 | f = this._map, 775 | c = f.charAt(64);c && (c = b.indexOf(c), -1 != c && (e = c));for (var c = [], a = 0, d = 0; d < e; d++) { 776 | if (d % 4) { 777 | var g = f.indexOf(b.charAt(d - 1)) << 2 * (d % 4), 778 | h = f.indexOf(b.charAt(d)) >>> 6 - 2 * (d % 4);c[a >>> 2] |= (g | h) << 24 - 8 * (a % 4);a++; 779 | } 780 | }return j.create(c, a); 781 | }, _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" }; 782 | })(); 783 | 784 | hawk.crypto.utils = CryptoJS; 785 | 786 | // Export if used as a module 787 | 788 | if (typeof module !== 'undefined' && module.exports) { 789 | module.exports = hawk; 790 | } 791 | 792 | /* eslint-enable */ 793 | // $lab:coverage:on$ 794 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![hawk Logo](https://raw.github.com/hueniverse/hawk/master/images/hawk.png) 2 | 3 | **Hawk** is an HTTP authentication scheme using a message authentication code (MAC) algorithm to provide partial 4 | HTTP request cryptographic verification. For more complex use cases such as access delegation, see [Oz](https://github.com/hueniverse/oz). 5 | 6 | Current version: **6.x** 7 | 8 | Note: 6.x, 5.x, 4.x, 3.x, and 2.x are the same exact protocol as 1.1. The version increments reflect changes in the node API. 9 | 10 | [![Build Status](https://travis-ci.org/hueniverse/hawk.svg?branch=master)](https://travis-ci.org/hueniverse/hawk) 11 | 12 | # Table of Content 13 | 14 | - [**Introduction**](#introduction) 15 | - [Replay Protection](#replay-protection) 16 | - [Usage Example](#usage-example) 17 | - [Protocol Example](#protocol-example) 18 | - [Payload Validation](#payload-validation) 19 | - [Response Payload Validation](#response-payload-validation) 20 | - [Browser Support and Considerations](#browser-support-and-considerations) 21 | - [**Single URI Authorization**](#single-uri-authorization) 22 | - [Usage Example](#bewit-usage-example) 23 | - [**Security Considerations**](#security-considerations) 24 | - [MAC Keys Transmission](#mac-keys-transmission) 25 | - [Confidentiality of Requests](#confidentiality-of-requests) 26 | - [Spoofing by Counterfeit Servers](#spoofing-by-counterfeit-servers) 27 | - [Plaintext Storage of Credentials](#plaintext-storage-of-credentials) 28 | - [Entropy of Keys](#entropy-of-keys) 29 | - [Coverage Limitations](#coverage-limitations) 30 | - [Future Time Manipulation](#future-time-manipulation) 31 | - [Client Clock Poisoning](#client-clock-poisoning) 32 | - [Bewit Limitations](#bewit-limitations) 33 | - [Host Header Forgery](#host-header-forgery) 34 | - [**Frequently Asked Questions**](#frequently-asked-questions) 35 | - [**Implementations**](#implementations) 36 | - [**Acknowledgements**](#acknowledgements) 37 | 38 | # Introduction 39 | 40 | **Hawk** is an HTTP authentication scheme providing mechanisms for making authenticated HTTP requests with 41 | partial cryptographic verification of the request and response, covering the HTTP method, request URI, host, 42 | and optionally the request payload. 43 | 44 | Similar to the HTTP [Digest access authentication schemes](http://www.ietf.org/rfc/rfc2617.txt), **Hawk** uses a set of 45 | client credentials which include an identifier (e.g. username) and key (e.g. password). Likewise, just as with the Digest scheme, 46 | the key is never included in authenticated requests. Instead, it is used to calculate a request MAC value which is 47 | included in its place. 48 | 49 | However, **Hawk** has several differences from Digest. In particular, while both use a nonce to limit the possibility of 50 | replay attacks, in **Hawk** the client generates the nonce and uses it in combination with a timestamp, leading to less 51 | "chattiness" (interaction with the server). 52 | 53 | Also unlike Digest, this scheme is not intended to protect the key itself (the password in Digest) because 54 | the client and server must both have access to the key material in the clear. 55 | 56 | The primary design goals of this scheme are to: 57 | * simplify and improve HTTP authentication for services that are unwilling or unable to deploy TLS for all resources, 58 | * secure credentials against leakage (e.g., when the client uses some form of dynamic configuration to determine where 59 | to send an authenticated request), and 60 | * avoid the exposure of credentials sent to a malicious server over an unauthenticated secure channel due to client 61 | failure to validate the server's identity as part of its TLS handshake. 62 | 63 | In addition, **Hawk** supports a method for granting third-parties temporary access to individual resources using 64 | a query parameter called _bewit_ (in falconry, a leather strap used to attach a tracking device to the leg of a hawk). 65 | 66 | The **Hawk** scheme requires the establishment of a shared symmetric key between the client and the server, 67 | which is beyond the scope of this module. Typically, the shared credentials are established via an initial 68 | TLS-protected phase or derived from some other shared confidential information available to both the client 69 | and the server. 70 | 71 | 72 | ## Replay Protection 73 | 74 | Without replay protection, an attacker can use a compromised (but otherwise valid and authenticated) request more 75 | than once, gaining access to a protected resource. To mitigate this, clients include both a nonce and a timestamp when 76 | making requests. This gives the server enough information to prevent replay attacks. 77 | 78 | The nonce is generated by the client, and is a string unique across all requests with the same timestamp and 79 | key identifier combination. 80 | 81 | The timestamp enables the server to restrict the validity period of the credentials where requests occurring afterwards 82 | are rejected. It also removes the need for the server to retain an unbounded number of nonce values for future checks. 83 | By default, **Hawk** uses a time window of 1 minute to allow for time skew between the client and server (which in 84 | practice translates to a maximum of 2 minutes as the skew can be positive or negative). 85 | 86 | Using a timestamp requires the client's clock to be in sync with the server's clock. **Hawk** requires both the client 87 | clock and the server clock to use NTP to ensure synchronization. However, given the limitations of some client types 88 | (e.g. browsers) to deploy NTP, the server provides the client with its current time (in seconds precision) in response 89 | to a bad timestamp. 90 | 91 | There is no expectation that the client will adjust its system clock to match the server (in fact, this would be a 92 | potential attack vector). Instead, the client only uses the server's time to calculate an offset used only 93 | for communications with that particular server. The protocol rewards clients with synchronized clocks by reducing 94 | the number of round trips required to authenticate the first request. 95 | 96 | 97 | ## Usage Example 98 | 99 | Server code: 100 | 101 | ```javascript 102 | const Http = require('http'); 103 | const Hawk = require('hawk'); 104 | 105 | 106 | // Credentials lookup function 107 | 108 | const credentialsFunc = function (id, callback) { 109 | 110 | const credentials = { 111 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 112 | algorithm: 'sha256', 113 | user: 'Steve' 114 | }; 115 | 116 | return callback(null, credentials); 117 | }; 118 | 119 | // Create HTTP server 120 | 121 | const handler = function (req, res) { 122 | 123 | // Authenticate incoming request 124 | 125 | Hawk.server.authenticate(req, credentialsFunc, {}, (err, credentials, artifacts) => { 126 | 127 | // Prepare response 128 | 129 | const payload = (!err ? `Hello ${credentials.user} ${artifacts.ext}` : 'Shoosh!'); 130 | const headers = { 'Content-Type': 'text/plain' }; 131 | 132 | // Generate Server-Authorization response header 133 | 134 | const header = Hawk.server.header(credentials, artifacts, { payload, contentType: headers['Content-Type'] }); 135 | headers['Server-Authorization'] = header; 136 | 137 | // Send the response back 138 | 139 | res.writeHead(!err ? 200 : 401, headers); 140 | res.end(payload); 141 | }); 142 | }; 143 | 144 | // Start server 145 | 146 | Http.createServer(handler).listen(8000, 'example.com'); 147 | ``` 148 | 149 | Client code: 150 | 151 | ```javascript 152 | const Request = require('request'); 153 | const Hawk = require('hawk'); 154 | 155 | 156 | // Client credentials 157 | 158 | const credentials = { 159 | id: 'dh37fgj492je', 160 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 161 | algorithm: 'sha256' 162 | } 163 | 164 | // Request options 165 | 166 | const requestOptions = { 167 | uri: 'http://example.com:8000/resource/1?b=1&a=2', 168 | method: 'GET', 169 | headers: {} 170 | }; 171 | 172 | // Generate Authorization request header 173 | 174 | const header = Hawk.client.header('http://example.com:8000/resource/1?b=1&a=2', 'GET', { credentials: credentials, ext: 'some-app-data' }); 175 | requestOptions.headers.Authorization = header.field; 176 | 177 | // Send authenticated request 178 | 179 | Request(requestOptions, function (error, response, body) { 180 | 181 | // Authenticate the server's response 182 | 183 | const isValid = Hawk.client.authenticate(response, credentials, header.artifacts, { payload: body }); 184 | 185 | // Output results 186 | 187 | console.log(`${response.statusCode}: ${body}` + (isValid ? ' (valid)' : ' (invalid)')); 188 | }); 189 | ``` 190 | 191 | **Hawk** utilized the [**SNTP**](https://github.com/hueniverse/sntp) module for time sync management. By default, the local 192 | machine time is used. To automatically retrieve and synchronize the clock within the application, use the SNTP 'start()' method. 193 | 194 | ```javascript 195 | Hawk.sntp.start(); 196 | ``` 197 | 198 | 199 | ## Protocol Example 200 | 201 | The client attempts to access a protected resource without authentication, sending the following HTTP request to 202 | the resource server: 203 | 204 | ``` 205 | GET /resource/1?b=1&a=2 HTTP/1.1 206 | Host: example.com:8000 207 | ``` 208 | 209 | The resource server returns an authentication challenge. 210 | 211 | ``` 212 | HTTP/1.1 401 Unauthorized 213 | WWW-Authenticate: Hawk 214 | ``` 215 | 216 | The client has previously obtained a set of **Hawk** credentials for accessing resources on the "`http://example.com/`" 217 | server. The **Hawk** credentials issued to the client include the following attributes: 218 | 219 | * Key identifier: `dh37fgj492je` 220 | * Key: `werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn` 221 | * Algorithm: `hmac sha256` 222 | * Hash: `6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=` 223 | 224 | The client generates the authentication header by calculating a timestamp (e.g. the number of seconds since January 1, 225 | 1970 00:00:00 GMT), generating a nonce, and constructing the normalized request string (each value followed by a newline 226 | character): 227 | 228 | ``` 229 | hawk.1.header 230 | 1353832234 231 | j4h3g2 232 | GET 233 | /resource/1?b=1&a=2 234 | example.com 235 | 8000 236 | 237 | some-app-ext-data 238 | 239 | ``` 240 | 241 | The request MAC is calculated using HMAC with the specified hash algorithm "`sha256`" and the key over the normalized request string. 242 | The result is base64-encoded to produce the request MAC: 243 | 244 | ``` 245 | 6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE= 246 | ``` 247 | 248 | The client includes the **Hawk** key identifier, timestamp, nonce, application specific data, and request MAC with the request using 249 | the HTTP `Authorization` request header field: 250 | 251 | ``` 252 | GET /resource/1?b=1&a=2 HTTP/1.1 253 | Host: example.com:8000 254 | Authorization: Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=" 255 | ``` 256 | 257 | The server validates the request by calculating the request MAC again based on the request received and verifies the validity 258 | and scope of the **Hawk** credentials. If valid, the server responds with the requested resource. 259 | 260 | 261 | ### Payload Validation 262 | 263 | **Hawk** provides optional payload validation. When generating the authentication header, the client calculates a payload hash 264 | using the specified hash algorithm. The hash is calculated over the concatenated value of (each followed by a newline character): 265 | * `hawk.1.payload` 266 | * the content-type in lowercase, without any parameters (e.g. `application/json`) 267 | * the request payload prior to any content encoding (the exact representation requirements should be specified by the server for payloads other than simple single-part ascii to ensure interoperability) 268 | 269 | For example: 270 | 271 | * Payload: `Thank you for flying Hawk` 272 | * Content Type: `text/plain` 273 | * Algorithm: `sha256` 274 | * Hash: `Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=` 275 | 276 | Results in the following input to the payload hash function (newline terminated values): 277 | 278 | ``` 279 | hawk.1.payload 280 | text/plain 281 | Thank you for flying Hawk 282 | 283 | ``` 284 | 285 | Which produces the following hash value: 286 | 287 | ``` 288 | Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY= 289 | ``` 290 | 291 | The client constructs the normalized request string (newline terminated values): 292 | 293 | ``` 294 | hawk.1.header 295 | 1353832234 296 | j4h3g2 297 | POST 298 | /resource/1?a=1&b=2 299 | example.com 300 | 8000 301 | Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY= 302 | some-app-ext-data 303 | 304 | ``` 305 | 306 | Then calculates the request MAC and includes the **Hawk** key identifier, timestamp, nonce, payload hash, application specific data, 307 | and request MAC, with the request using the HTTP `Authorization` request header field: 308 | 309 | ``` 310 | POST /resource/1?a=1&b=2 HTTP/1.1 311 | Host: example.com:8000 312 | Authorization: Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", hash="Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=", ext="some-app-ext-data", mac="aSe1DERmZuRl3pI36/9BdZmnErTw3sNzOOAUlfeKjVw=" 313 | ``` 314 | 315 | It is up to the server if and when it validates the payload for any given request, based solely on its security policy 316 | and the nature of the data included. 317 | 318 | If the payload is available at the time of authentication, the server uses the hash value provided by the client to construct 319 | the normalized string and validates the MAC. If the MAC is valid, the server calculates the payload hash and compares the value 320 | with the provided payload hash in the header. In many cases, checking the MAC first is faster than calculating the payload hash. 321 | 322 | However, if the payload is not available at authentication time (e.g. too large to fit in memory, streamed elsewhere, or processed 323 | at a different stage in the application), the server may choose to defer payload validation for later by retaining the hash value 324 | provided by the client after validating the MAC. 325 | 326 | It is important to note that MAC validation does not mean the hash value provided by the client is valid, only that the value 327 | included in the header was not modified. Without calculating the payload hash on the server and comparing it to the value provided 328 | by the client, the payload may be modified by an attacker. 329 | 330 | 331 | ## Response Payload Validation 332 | 333 | **Hawk** provides partial response payload validation. The server includes the `Server-Authorization` response header which enables the 334 | client to authenticate the response and ensure it is talking to the right server. **Hawk** defines the HTTP `Server-Authorization` header 335 | as a response header using the exact same syntax as the `Authorization` request header field. 336 | 337 | The header is constructed using the same process as the client's request header. The server uses the same credentials and other 338 | artifacts provided by the client to constructs the normalized request string. The `ext` and `hash` values are replaced with 339 | new values based on the server response. The rest as identical to those used by the client. 340 | 341 | The result MAC digest is included with the optional `hash` and `ext` values: 342 | 343 | ``` 344 | Server-Authorization: Hawk mac="XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific" 345 | ``` 346 | 347 | 348 | ## Browser Support and Considerations 349 | 350 | A browser script is provided for including using a `