├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lib └── index.js ├── package.json └── test ├── bewit.js └── hawk.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/package-lock.json 3 | 4 | coverage.* 5 | 6 | **/.DS_Store 7 | **/._* 8 | 9 | **/*.pem 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "10" 6 | - "11" 7 | - "node" 8 | 9 | sudo: false 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Breaking changes are documented using GitHub issues, see [issues labeled "release notes"](https://github.com/hapijs/hapi-auth-hawk/issues?q=is%3Aissue+label%3A%22release+notes%22). 2 | 3 | If you want changes of a specific minor or patch release, you can browse the [GitHub milestones](https://github.com/hapijs/hapi-auth-hawk/milestones?state=closed&direction=asc&sort=due_date). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please view our [hapijs contributing guide](https://github.com/hapijs/hapi/blob/master/CONTRIBUTING.md). 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2018, Project contributors 2 | Copyright (c) 2012-2014, Walmart 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * The names of any contributors may not be used to endorse or promote 13 | products derived from this software without specific prior written 14 | permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### hapi-auth-hawk 2 | 3 | This plugin was merged with @hapi/hawk. 4 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Boom = require('boom'); 4 | const Hawk = require('hawk'); 5 | const Hoek = require('hoek'); 6 | 7 | 8 | const internals = {}; 9 | 10 | 11 | exports.plugin = { 12 | pkg: require('../package.json'), 13 | requirements: { 14 | hapi: '>=17.7.0' 15 | }, 16 | register: function (server) { 17 | 18 | server.auth.scheme('hawk', internals.hawk); 19 | server.auth.scheme('bewit', internals.bewit); 20 | } 21 | }; 22 | 23 | 24 | internals.hawk = function (server, options) { 25 | 26 | Hoek.assert(options, 'Invalid hawk scheme options'); 27 | Hoek.assert(options.getCredentialsFunc, 'Missing required getCredentialsFunc method in hawk scheme configuration'); 28 | 29 | const settings = Hoek.clone(options); 30 | settings.hawk = settings.hawk || {}; 31 | 32 | const scheme = { 33 | authenticate: async function (request, h) { 34 | 35 | try { 36 | var result = await Hawk.server.authenticate(request.raw.req, settings.getCredentialsFunc, settings.hawk); 37 | } 38 | catch (err) { 39 | const { credentials, artifacts } = err; 40 | return h.unauthenticated(err, credentials ? { credentials, artifacts } : undefined); 41 | } 42 | 43 | if (request.route.settings.auth.payload) { 44 | request.events.once('peek', (chunk) => { 45 | 46 | const payloadHash = Hawk.crypto.initializePayloadHash(request.auth.credentials.algorithm, request.headers['content-type']); 47 | payloadHash.update(chunk); 48 | 49 | request.events.on('peek', (chunk2) => payloadHash.update(chunk2)); 50 | 51 | request.events.once('finish', () => { 52 | 53 | request.plugins['hapi-auth-hawk'] = { payloadHash: Hawk.crypto.finalizePayloadHash(payloadHash) }; 54 | }); 55 | }); 56 | } 57 | 58 | return h.authenticated(result); 59 | }, 60 | payload: function (request, h) { 61 | 62 | if (!request.auth.artifacts.hash) { 63 | throw Boom.unauthorized(null, 'Hawk'); // Missing 64 | } 65 | 66 | const plugin = request.plugins['hapi-auth-hawk']; 67 | 68 | if (!plugin) { 69 | throw Boom.unauthorized('Payload is invalid'); 70 | } 71 | 72 | try { 73 | Hawk.server.authenticatePayloadHash(plugin.payloadHash, request.auth.artifacts); 74 | return h.continue; 75 | } 76 | catch (err) { 77 | throw Boom.unauthorized('Payload is invalid'); 78 | } 79 | }, 80 | response: function (request, h) { 81 | 82 | const response = request.response; 83 | const payloadHash = Hawk.crypto.initializePayloadHash(request.auth.credentials.algorithm, response.headers['content-type']); 84 | 85 | response.header('trailer', 'server-authorization'); 86 | response.header('transfer-encoding', 'chunked'); 87 | 88 | delete response.headers['content-length']; // Cannot not send a content-length header alongside transfer-encoding (https://tools.ietf.org/html/rfc7230#section-3.3.3) 89 | 90 | response.events.on('peek', (chunk) => { 91 | 92 | payloadHash.update(chunk); 93 | }); 94 | 95 | response.events.once('finish', () => { 96 | 97 | const header = Hawk.server.header(request.auth.credentials, request.auth.artifacts, { hash: Hawk.crypto.finalizePayloadHash(payloadHash) }); 98 | request.raw.res.addTrailers({ 'server-authorization': header }); 99 | }); 100 | 101 | return h.continue; 102 | } 103 | }; 104 | 105 | return scheme; 106 | }; 107 | 108 | 109 | internals.bewit = function (server, options) { 110 | 111 | Hoek.assert(options, 'Invalid bewit scheme options'); 112 | Hoek.assert(options.getCredentialsFunc, 'Missing required getCredentialsFunc method in bewit scheme configuration'); 113 | 114 | const settings = Hoek.clone(options); 115 | settings.hawk = settings.hawk || {}; 116 | 117 | const scheme = { 118 | authenticate: async function (request, h) { 119 | 120 | try { 121 | const { credentials, attributes } = await Hawk.server.authenticateBewit(request.raw.req, settings.getCredentialsFunc, settings.hawk); 122 | return h.authenticated({ credentials, attributes }); 123 | } 124 | catch (err) { 125 | const { credentials, attributes } = err; 126 | return h.unauthenticated(err, credentials ? { credentials, attributes } : undefined); 127 | } 128 | } 129 | }; 130 | 131 | return scheme; 132 | }; 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-auth-hawk", 3 | "description": "Hawk authentication plugin", 4 | "version": "4.0.1", 5 | "repository": "git://github.com/hapijs/hapi-auth-hawk", 6 | "main": "lib/index.js", 7 | "keywords": [ 8 | "hapi", 9 | "plugin", 10 | "auth", 11 | "hawk", 12 | "bewit" 13 | ], 14 | "dependencies": { 15 | "boom": "7.x.x", 16 | "hoek": "6.x.x", 17 | "hawk": "7.x.x" 18 | }, 19 | "peerDependencies": { 20 | "hapi": ">=17.x.x" 21 | }, 22 | "devDependencies": { 23 | "code": "5.x.x", 24 | "hapi": "17.x.x", 25 | "lab": "17.x.x" 26 | }, 27 | "scripts": { 28 | "test": "lab -r console -t 100 -a code -L", 29 | "test-cov-html": "lab -r html -o coverage.html -a code -L" 30 | }, 31 | "license": "BSD-3-Clause" 32 | } 33 | -------------------------------------------------------------------------------- /test/bewit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Boom = require('boom'); 4 | const Code = require('code'); 5 | const Hapi = require('hapi'); 6 | const Hawk = require('hawk'); 7 | const Lab = require('lab'); 8 | 9 | 10 | const internals = {}; 11 | 12 | 13 | const { it, describe, before } = exports.lab = Lab.script(); 14 | const expect = Code.expect; 15 | 16 | 17 | describe('bewit scheme', () => { 18 | 19 | const credentials = { 20 | john: { 21 | cred: { 22 | id: 'john', 23 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 24 | algorithm: 'sha256' 25 | } 26 | }, 27 | jane: { 28 | err: Boom.internal('boom') 29 | } 30 | }; 31 | 32 | const getCredentialsFunc = function (id) { 33 | 34 | if (credentials[id]) { 35 | if (credentials[id].err) { 36 | throw credentials[id].err; 37 | } 38 | 39 | return credentials[id].cred; 40 | } 41 | }; 42 | 43 | const getBewit = function (id, path) { 44 | 45 | if (credentials[id] && credentials[id].cred) { 46 | return Hawk.uri.getBewit('http://example.com:8080' + path, { credentials: credentials[id].cred, ttlSec: 60 }); 47 | } 48 | 49 | return ''; 50 | }; 51 | 52 | const bewitHandler = function (request, h) { 53 | 54 | return 'Success'; 55 | }; 56 | 57 | let server = Hapi.server(); 58 | 59 | before(async () => { 60 | 61 | await server.register(require('../')); 62 | 63 | server.auth.strategy('default', 'bewit', { getCredentialsFunc }); 64 | 65 | server.route([ 66 | { method: 'GET', path: '/bewit', handler: bewitHandler, options: { auth: 'default' } }, 67 | { method: 'GET', path: '/bewitOptional', handler: bewitHandler, options: { auth: { mode: 'optional', strategy: 'default' } } }, 68 | { method: 'GET', path: '/bewitScope', handler: bewitHandler, options: { auth: { scope: 'x', strategy: 'default' } } } 69 | ]); 70 | }); 71 | 72 | it('returns a reply on successful auth', async () => { 73 | 74 | const bewit = getBewit('john', '/bewit'); 75 | const res = await server.inject('http://example.com:8080/bewit?bewit=' + bewit); 76 | 77 | expect(res.result).to.equal('Success'); 78 | }); 79 | 80 | it('returns an error reply on failed optional auth', async () => { 81 | 82 | const bewit = getBewit('john', '/abc'); 83 | const res = await server.inject('http://example.com:8080/bewitOptional?bewit=' + bewit); 84 | 85 | expect(res.statusCode).to.equal(401); 86 | }); 87 | 88 | it('returns an error on bad bewit', async () => { 89 | 90 | const bewit = getBewit('john', '/abc'); 91 | const res = await server.inject('http://example.com:8080/bewit?bewit=' + bewit); 92 | 93 | expect(res.statusCode).to.equal(401); 94 | }); 95 | 96 | it('returns an error on bad bewit format', async () => { 97 | 98 | const res = await server.inject('http://example.com:8080/bewit?bewit=junk'); 99 | 100 | expect(res.statusCode).to.equal(400); 101 | }); 102 | 103 | it('returns an error on insufficient scope', async () => { 104 | 105 | const bewit = getBewit('john', '/bewitScope'); 106 | const res = await server.inject('http://example.com:8080/bewitScope?bewit=' + bewit); 107 | 108 | expect(res.statusCode).to.equal(403); 109 | }); 110 | 111 | it('returns a reply on successful auth when using a custom host header key', async () => { 112 | 113 | const bewit = getBewit('john', '/bewit'); 114 | const request = { method: 'GET', url: '/bewit?bewit=' + bewit, headers: { custom: 'example.com:8080' } }; 115 | 116 | server = new Hapi.Server(); 117 | await server.register(require('../')); 118 | 119 | server.auth.strategy('default', 'bewit', { 120 | getCredentialsFunc, 121 | hawk: { 122 | hostHeaderName: 'custom' 123 | } 124 | }); 125 | 126 | server.route({ method: 'GET', path: '/bewit', handler: bewitHandler, options: { auth: 'default' } }); 127 | 128 | const res = await server.inject(request); 129 | 130 | expect(res.statusCode).to.equal(200); 131 | expect(res.result).to.equal('Success'); 132 | }); 133 | 134 | it('cannot add a route that has payload validation required', () => { 135 | 136 | const fn = function () { 137 | 138 | server.route({ method: 'POST', 139 | path: '/bewitPayload', 140 | handler: bewitHandler, 141 | options: { auth: { mode: 'required', strategy: 'default', payload: 'required' }, 142 | payload: { output: 'stream', parse: false } } 143 | }); 144 | }; 145 | 146 | expect(fn).to.throw('Payload validation can only be required when all strategies support it in /bewitPayload'); 147 | }); 148 | 149 | it('cannot add a route that has payload validation as optional', () => { 150 | 151 | const fn = function () { 152 | 153 | server.route({ method: 'POST', 154 | path: '/bewitPayload', 155 | handler: bewitHandler, 156 | options: { auth: { mode: 'required', strategy: 'default', payload: 'optional' }, 157 | payload: { output: 'stream', parse: false } } 158 | }); 159 | }; 160 | 161 | expect(fn).to.throw('Payload authentication requires at least one strategy with payload support in /bewitPayload'); 162 | }); 163 | 164 | it('can add a route that has payload validation as none', () => { 165 | 166 | const fn = function () { 167 | 168 | server.route({ method: 'POST', 169 | path: '/bewitPayload', 170 | handler: bewitHandler, 171 | options: { auth: { mode: 'required', strategy: 'default', payload: false }, 172 | payload: { output: 'stream', parse: false } } 173 | }); 174 | }; 175 | 176 | expect(fn).to.not.throw(); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/hawk.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Stream = require('stream'); 4 | 5 | const Boom = require('boom'); 6 | const Code = require('code'); 7 | const Hapi = require('hapi'); 8 | const Hawk = require('hawk'); 9 | const Lab = require('lab'); 10 | 11 | 12 | const internals = {}; 13 | 14 | 15 | const { it, describe } = exports.lab = Lab.script(); 16 | const expect = Code.expect; 17 | 18 | 19 | describe('hawk scheme', () => { 20 | 21 | const credentials = { 22 | john: { 23 | cred: { 24 | id: 'john', 25 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 26 | algorithm: 'sha256' 27 | } 28 | }, 29 | jane: { 30 | err: Boom.internal('boom') 31 | }, 32 | joan: { 33 | cred: { 34 | id: 'joan', 35 | key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 36 | algorithm: 'sha256' 37 | } 38 | } 39 | }; 40 | 41 | const getCredentialsFunc = function (id) { 42 | 43 | if (credentials[id]) { 44 | if (credentials[id].err) { 45 | throw credentials[id].err; 46 | } 47 | 48 | return credentials[id].cred; 49 | } 50 | }; 51 | 52 | const hawkHeader = function (id, path) { 53 | 54 | if (credentials[id] && credentials[id].cred) { 55 | return Hawk.client.header('http://example.com:8080' + path, 'POST', { credentials: credentials[id].cred }); 56 | } 57 | 58 | return ''; 59 | }; 60 | 61 | it('calls through to handler on successful auth', async () => { 62 | 63 | const server = Hapi.server(); 64 | await server.register(require('../')); 65 | 66 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 67 | server.route({ 68 | method: 'POST', 69 | path: '/hawk', 70 | handler: function (request, h) { 71 | 72 | return 'Success'; 73 | }, 74 | options: { auth: 'default' } 75 | }); 76 | 77 | const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: hawkHeader('john', '/hawk').header } }; 78 | const res = await server.inject(request); 79 | 80 | expect(res.statusCode).to.equal(200); 81 | expect(res.result).to.equal('Success'); 82 | }); 83 | 84 | it('calls through to handler on failed optional auth', async () => { 85 | 86 | const server = Hapi.server(); 87 | await server.register(require('../')); 88 | 89 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 90 | server.route({ 91 | method: 'POST', 92 | path: '/hawkOptional', 93 | handler: function (request, h) { 94 | 95 | return 'Success'; 96 | }, 97 | options: { auth: { mode: 'optional', strategy: 'default' } } 98 | }); 99 | 100 | const request = { method: 'POST', url: 'http://example.com:8080/hawkOptional' }; 101 | const res = await server.inject(request); 102 | 103 | expect(res.result).to.equal('Success'); 104 | }); 105 | 106 | it('includes authorization header in response when the response is a stream', async () => { 107 | 108 | const hawkStreamHandler = function (request, h) { 109 | 110 | const TestStream = class extends Stream.Readable { 111 | 112 | _read(size) { 113 | 114 | const self = this; 115 | 116 | if (this.isDone) { 117 | return; 118 | } 119 | 120 | this.isDone = true; 121 | 122 | setTimeout(() => { 123 | 124 | self.push('hi'); 125 | }, 2); 126 | 127 | setTimeout(() => { 128 | 129 | self.push(null); 130 | }, 5); 131 | } 132 | }; 133 | 134 | const stream = new TestStream(); 135 | return stream; 136 | }; 137 | 138 | const server = Hapi.server(); 139 | await server.register(require('../')); 140 | 141 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 142 | server.route({ method: 'POST', path: '/hawkStream', handler: hawkStreamHandler, options: { auth: 'default' } }); 143 | 144 | const authHeader = hawkHeader('john', '/hawkStream'); 145 | const request = { method: 'POST', url: 'http://example.com:8080/hawkStream', headers: { authorization: authHeader.header } }; 146 | const res = await server.inject(request); 147 | 148 | expect(res.statusCode).to.equal(200); 149 | expect(res.trailers['server-authorization']).to.contain('Hawk'); 150 | 151 | const options = { 152 | payload: res.payload 153 | }; 154 | 155 | const cred = getCredentialsFunc('john'); 156 | 157 | const header = Hawk.server.header(cred, authHeader.artifacts, options); 158 | expect(header).to.equal(res.trailers['server-authorization']); 159 | }); 160 | 161 | it('includes valid authorization header in response when the response is text', async () => { 162 | 163 | const server = Hapi.server(); 164 | await server.register(require('../')); 165 | 166 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 167 | server.route({ 168 | method: 'POST', 169 | path: '/hawk', 170 | handler: function (request, h) { 171 | 172 | return 'Success'; 173 | }, 174 | options: { auth: 'default' } 175 | }); 176 | 177 | const authHeader = hawkHeader('john', '/hawk'); 178 | const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: authHeader.header } }; 179 | 180 | const res = await server.inject(request); 181 | 182 | expect(res.statusCode).to.equal(200); 183 | expect(res.trailers['server-authorization']).to.contain('Hawk'); 184 | 185 | const options = { 186 | payload: res.payload, 187 | contentType: res.headers['content-type'] 188 | }; 189 | 190 | const cred = getCredentialsFunc('john'); 191 | 192 | const header = Hawk.server.header(cred, authHeader.artifacts, options); 193 | expect(header).to.equal(res.trailers['server-authorization']); 194 | }); 195 | 196 | it('removes the content-length header when switching to chunked transfer encoding', async () => { 197 | 198 | const server = Hapi.server(); 199 | await server.register(require('../')); 200 | 201 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 202 | server.route({ 203 | method: 'POST', 204 | path: '/hawk', 205 | handler: function (request, h) { 206 | 207 | return 'Success'; 208 | }, 209 | options: { auth: 'default' } 210 | }); 211 | 212 | const authHeader = hawkHeader('john', '/hawk'); 213 | const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: authHeader.header } }; 214 | 215 | const res = await server.inject(request); 216 | 217 | expect(res.statusCode).to.equal(200); 218 | expect(res.headers['transfer-encoding']).to.equal('chunked'); 219 | expect(res.headers['content-length']).to.not.exist(); 220 | }); 221 | 222 | it('includes valid authorization header in response when the request fails validation', async () => { 223 | 224 | const server = Hapi.server(); 225 | await server.register(require('../')); 226 | 227 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 228 | server.route({ 229 | method: 'POST', 230 | path: '/hawkValidate', 231 | handler: function (request, h) { 232 | 233 | return 'Success'; 234 | }, 235 | options: { auth: 'default', validate: { query: {} } } 236 | }); 237 | 238 | const authHeader = hawkHeader('john', '/hawkValidate?a=1'); 239 | const request = { method: 'POST', url: 'http://example.com:8080/hawkValidate?a=1', headers: { authorization: authHeader.header } }; 240 | const res = await server.inject(request); 241 | 242 | expect(res.trailers['server-authorization']).to.exist(); 243 | expect(res.trailers['server-authorization']).to.contain('Hawk'); 244 | expect(res.statusCode).to.equal(400); 245 | 246 | const options = { 247 | payload: res.payload, 248 | contentType: res.headers['content-type'] 249 | }; 250 | 251 | const cred = getCredentialsFunc('john'); 252 | 253 | authHeader.artifacts.credentials = cred; 254 | const header = Hawk.server.header(cred, authHeader.artifacts, options); 255 | expect(header).to.equal(res.trailers['server-authorization']); 256 | }); 257 | 258 | it('does not include authorization header in response when the response is an error', async () => { 259 | 260 | const server = Hapi.server(); 261 | await server.register(require('../')); 262 | 263 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 264 | server.route({ 265 | method: 'POST', 266 | path: '/hawkError', 267 | handler: function (request, h) { 268 | 269 | return new Error(); 270 | }, 271 | options: { auth: 'default' } 272 | }); 273 | 274 | const request = { method: 'POST', url: 'http://example.com:8080/hawkError', headers: { authorization: hawkHeader('john', '/hawkError').header } }; 275 | const res = await server.inject(request); 276 | 277 | expect(res.statusCode).to.equal(500); 278 | expect(res.headers.authorization).to.not.exist(); 279 | }); 280 | 281 | it('returns an error on bad auth header', async () => { 282 | 283 | const server = Hapi.server(); 284 | await server.register(require('../')); 285 | 286 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 287 | server.route({ 288 | method: 'POST', 289 | path: '/hawk', 290 | handler: function (request, h) { 291 | 292 | return 'Success'; 293 | }, 294 | options: { auth: 'default' } 295 | }); 296 | 297 | const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: hawkHeader('john', 'abcd').header } }; 298 | const res = await server.inject(request); 299 | 300 | expect(res.result).to.exist(); 301 | expect(res.statusCode).to.equal(401); 302 | }); 303 | 304 | it('returns an error on bad header format', async () => { 305 | 306 | const server = Hapi.server(); 307 | await server.register(require('../')); 308 | 309 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 310 | server.route({ 311 | method: 'POST', 312 | path: '/hawk', 313 | handler: function (request, h) { 314 | 315 | return 'Success'; 316 | }, 317 | options: { auth: 'default' } 318 | }); 319 | 320 | const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: 'junk' } }; 321 | const res = await server.inject(request); 322 | 323 | expect(res.result).to.exist(); 324 | expect(res.statusCode).to.equal(401); 325 | }); 326 | 327 | it('returns an error on bad scheme', async () => { 328 | 329 | const server = Hapi.server(); 330 | await server.register(require('../')); 331 | 332 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 333 | server.route({ 334 | method: 'POST', 335 | path: '/hawk', 336 | handler: function (request, h) { 337 | 338 | return 'Success'; 339 | }, 340 | options: { auth: 'default' } 341 | }); 342 | 343 | const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: 'junk something' } }; 344 | const res = await server.inject(request); 345 | 346 | expect(res.result).to.exist(); 347 | expect(res.statusCode).to.equal(401); 348 | }); 349 | 350 | it('returns an error on insufficient scope', async () => { 351 | 352 | const server = Hapi.server(); 353 | await server.register(require('../')); 354 | 355 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 356 | server.route({ 357 | method: 'POST', 358 | path: '/hawkScope', 359 | handler: function (request, h) { 360 | 361 | return 'Success'; 362 | }, 363 | options: { auth: { scope: 'x', strategy: 'default' } } 364 | }); 365 | 366 | const request = { method: 'POST', url: 'http://example.com:8080/hawkScope', payload: '{}', headers: { authorization: hawkHeader('john', '/hawkScope').header } }; 367 | const res = await server.inject(request); 368 | 369 | expect(res.statusCode).to.equal(403); 370 | }); 371 | 372 | it('returns a reply on successful auth when using a custom host header key', async () => { 373 | 374 | const server = Hapi.server(); 375 | await server.register(require('../')); 376 | 377 | server.auth.strategy('default', 'hawk', { 378 | getCredentialsFunc, 379 | hawk: { 380 | hostHeaderName: 'custom' 381 | } 382 | }); 383 | 384 | server.route({ 385 | method: 'POST', 386 | path: '/hawk', 387 | handler: function (request, h) { 388 | 389 | return 'Success'; 390 | }, 391 | options: { auth: 'default' } 392 | }); 393 | 394 | const request = { method: 'POST', url: '/hawk', headers: { authorization: hawkHeader('john', '/hawk').header, custom: 'example.com:8080' } }; 395 | const res = await server.inject(request); 396 | 397 | expect(res.statusCode).to.equal(200); 398 | expect(res.result).to.equal('Success'); 399 | }); 400 | 401 | it('returns a reply on successful auth and payload validation', async () => { 402 | 403 | const server = Hapi.server(); 404 | await server.register(require('../')); 405 | 406 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 407 | server.route({ 408 | method: 'POST', 409 | path: '/hawkPayload', 410 | handler: function (request, h) { 411 | 412 | return 'Success'; 413 | }, 414 | options: { auth: { mode: 'required', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } 415 | }); 416 | 417 | const payload = 'application text formatted payload'; 418 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayload', 'POST', { credentials: credentials.john.cred, payload, contentType: 'text/plain' }); 419 | const request = { 420 | method: 'POST', 421 | url: 'http://example.com:8080/hawkPayload', 422 | headers: { authorization: authHeader.header, 'content-type': 'text/plain' }, 423 | payload, 424 | simulate: { split: true } 425 | }; 426 | 427 | const res = await server.inject(request); 428 | 429 | expect(res.statusCode).to.equal(200); 430 | expect(res.result).to.equal('Success'); 431 | }); 432 | 433 | it('returns an error with payload validation when the payload is tampered with', async () => { 434 | 435 | const server = Hapi.server(); 436 | await server.register(require('../')); 437 | 438 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 439 | server.route({ 440 | method: 'POST', 441 | path: '/hawkPayload', 442 | handler: function (request, h) { 443 | 444 | return 'Success'; 445 | }, 446 | options: { auth: { mode: 'required', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } 447 | }); 448 | 449 | let payload = 'Here is my payload'; 450 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayload', 'POST', { credentials: credentials.john.cred, payload }); 451 | payload += 'HACKED'; 452 | const request = { method: 'POST', url: 'http://example.com:8080/hawkPayload', headers: { authorization: authHeader.header }, payload }; 453 | 454 | const res = await server.inject(request); 455 | 456 | expect(res.statusCode).to.equal(401); 457 | expect(res.result.message).to.equal('Payload is invalid'); 458 | }); 459 | 460 | it('returns an error with payload validation when the payload is absent', async () => { 461 | 462 | const server = Hapi.server(); 463 | await server.register(require('../')); 464 | 465 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 466 | server.route({ 467 | method: 'POST', 468 | path: '/hawkPayload', 469 | handler: function (request, h) { 470 | 471 | return 'Success'; 472 | }, 473 | options: { auth: { mode: 'required', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } 474 | }); 475 | 476 | let payload = 'Here is my payload'; 477 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayload', 'POST', { credentials: credentials.john.cred, payload }); 478 | payload = ''; 479 | const request = { method: 'POST', url: 'http://example.com:8080/hawkPayload', headers: { authorization: authHeader.header }, payload }; 480 | 481 | const res = await server.inject(request); 482 | 483 | expect(res.statusCode).to.equal(401); 484 | expect(res.result.message).to.equal('Payload is invalid'); 485 | }); 486 | 487 | it('returns an error with payload validation when the payload is tampered with and the route has optional validation', async () => { 488 | 489 | const server = Hapi.server(); 490 | await server.register(require('../')); 491 | 492 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 493 | server.route({ 494 | method: 'POST', 495 | path: '/hawkPayloadOptional', 496 | handler: function (request, h) { 497 | 498 | return 'Success'; 499 | }, 500 | options: { auth: { mode: 'required', payload: 'optional', strategy: 'default' }, payload: { override: 'text/plain' } } 501 | }); 502 | 503 | let payload = 'Here is my payload'; 504 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadOptional', 'POST', { credentials: credentials.john.cred, payload }); 505 | payload += 'HACKED'; 506 | const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadOptional', headers: { authorization: authHeader.header }, payload }; 507 | 508 | const res = await server.inject(request); 509 | 510 | expect(res.statusCode).to.equal(401); 511 | expect(res.result.message).to.equal('Payload is invalid'); 512 | }); 513 | 514 | it('returns a reply on successful auth and payload validation when validation is optional', async () => { 515 | 516 | const server = Hapi.server(); 517 | await server.register(require('../')); 518 | 519 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 520 | server.route({ 521 | method: 'POST', 522 | path: '/hawkPayloadOptional', 523 | handler: function (request, h) { 524 | 525 | return 'Success'; 526 | }, 527 | options: { auth: { mode: 'required', payload: 'optional', strategy: 'default' }, payload: { override: 'text/plain' } } 528 | }); 529 | 530 | const payload = 'Here is my payload'; 531 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadOptional', 'POST', { credentials: credentials.john.cred, payload }); 532 | const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadOptional', headers: { authorization: authHeader.header }, payload }; 533 | 534 | const res = await server.inject(request); 535 | 536 | expect(res.result).to.exist(); 537 | expect(res.result).to.equal('Success'); 538 | }); 539 | 540 | it('returns a reply on successful auth when payload validation is optional and no payload hash exists', async () => { 541 | 542 | const server = Hapi.server(); 543 | await server.register(require('../')); 544 | 545 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 546 | server.route({ 547 | method: 'POST', 548 | path: '/hawkPayloadOptional', 549 | handler: function (request, h) { 550 | 551 | return 'Success'; 552 | }, 553 | options: { auth: { mode: 'required', payload: 'optional', strategy: 'default' }, payload: { override: 'text/plain' } } 554 | } 555 | ); 556 | 557 | const payload = 'Here is my payload'; 558 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadOptional', 'POST', { credentials: credentials.john.cred }); 559 | const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadOptional', headers: { authorization: authHeader.header }, payload }; 560 | 561 | const res = await server.inject(request); 562 | 563 | expect(res.result).to.exist(); 564 | expect(res.result).to.equal('Success'); 565 | }); 566 | 567 | it('returns a reply on successful auth and when payload validation is disabled', async () => { 568 | 569 | const server = Hapi.server(); 570 | await server.register(require('../')); 571 | 572 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 573 | server.route({ 574 | method: 'POST', 575 | path: '/hawkPayloadNone', 576 | handler: function (request, h) { 577 | 578 | return 'Success'; 579 | }, 580 | options: { auth: { mode: 'required', payload: false, strategy: 'default' }, payload: { override: 'text/plain' } } 581 | }); 582 | 583 | const payload = 'Here is my payload'; 584 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadNone', 'POST', { credentials: credentials.john.cred, payload }); 585 | const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadNone', headers: { authorization: authHeader.header }, payload }; 586 | 587 | const res = await server.inject(request); 588 | 589 | expect(res.statusCode).to.equal(200); 590 | expect(res.result).to.equal('Success'); 591 | }); 592 | 593 | it('returns a reply on successful auth when the payload is tampered with and the route has disabled validation', async () => { 594 | 595 | const server = Hapi.server(); 596 | await server.register(require('../')); 597 | 598 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 599 | server.route({ 600 | method: 'POST', 601 | path: '/hawkPayloadNone', 602 | handler: function (request, h) { 603 | 604 | return 'Success'; 605 | }, 606 | options: { auth: { mode: 'required', payload: false, strategy: 'default' }, payload: { override: 'text/plain' } } 607 | }); 608 | 609 | let payload = 'Here is my payload'; 610 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadNone', 'POST', { credentials: credentials.john.cred, payload }); 611 | payload += 'HACKED'; 612 | const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadNone', headers: { authorization: authHeader.header }, payload }; 613 | 614 | const res = await server.inject(request); 615 | 616 | expect(res.statusCode).to.equal(200); 617 | expect(res.result).to.equal('Success'); 618 | }); 619 | 620 | it('returns a reply on successful auth when auth is optional and when payload validation is required', async () => { 621 | 622 | const server = Hapi.server(); 623 | await server.register(require('../')); 624 | 625 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 626 | server.route({ 627 | method: 'POST', 628 | path: '/hawkOptionalPayload', 629 | config: { 630 | handler: (request, h) => 'Success', 631 | auth: { mode: 'optional', payload: 'required', strategy: 'default' }, 632 | payload: { override: 'text/plain' } 633 | } 634 | }); 635 | 636 | const payload = 'Here is my payload'; 637 | const authHeader = Hawk.client.header('http://example.com:8080/hawkOptionalPayload', 'POST', { credentials: credentials.john.cred, payload }); 638 | const request = { method: 'POST', url: 'http://example.com:8080/hawkOptionalPayload', headers: { authorization: authHeader.header }, payload }; 639 | 640 | const res = await server.inject(request); 641 | 642 | expect(res.statusCode).to.equal(200); 643 | expect(res.result).to.equal('Success'); 644 | }); 645 | 646 | it('returns an error with payload validation when the payload is tampered with and the route has optional auth', async () => { 647 | 648 | const server = Hapi.server(); 649 | await server.register(require('../')); 650 | 651 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 652 | server.route({ 653 | method: 'POST', 654 | path: '/hawkOptionalPayload', 655 | handler: function (request, h) { 656 | 657 | return 'Success'; 658 | }, 659 | options: { auth: { mode: 'optional', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } 660 | }); 661 | 662 | let payload = 'Here is my payload'; 663 | const authHeader = Hawk.client.header('http://example.com:8080/hawkOptionalPayload', 'POST', { credentials: credentials.john.cred, payload }); 664 | payload += 'HACKED'; 665 | const request = { method: 'POST', url: 'http://example.com:8080/hawkOptionalPayload', headers: { authorization: authHeader.header }, payload }; 666 | 667 | const res = await server.inject(request); 668 | 669 | expect(res.statusCode).to.equal(401); 670 | expect(res.result.message).to.equal('Payload is invalid'); 671 | }); 672 | 673 | it('returns an error with payload validation when the payload hash is not included and payload validation is required', async () => { 674 | 675 | const server = Hapi.server(); 676 | await server.register(require('../')); 677 | 678 | server.auth.strategy('default', 'hawk', { getCredentialsFunc }); 679 | server.route({ 680 | method: 'POST', 681 | path: '/hawkPayload', 682 | handler: function (request, h) { 683 | 684 | return 'Success'; 685 | }, 686 | options: { auth: { mode: 'required', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } 687 | }); 688 | 689 | const payload = 'Here is my payload'; 690 | const authHeader = Hawk.client.header('http://example.com:8080/hawkPayload', 'POST', { credentials: credentials.john.cred }); 691 | const request = { method: 'POST', url: 'http://example.com:8080/hawkPayload', headers: { authorization: authHeader.header }, payload }; 692 | 693 | const res = await server.inject(request); 694 | 695 | expect(res.statusCode).to.equal(401); 696 | expect(res.result.message).to.equal('Missing payload authentication'); 697 | }); 698 | }); 699 | --------------------------------------------------------------------------------