diff --git a/.travis.yml b/.travis.yml index a420ca9..fe9f8dd 100755 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,11 @@ sudo: false install: - "npm install" + - "npm install hapi@$HAPI_VERSION" + +env: + - HAPI_VERSION="17" + - HAPI_VERSION="18" os: - "linux" diff --git a/README.md b/README.md index 14b7f16..41c8238 100755 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -![hawk Logo](https://raw.github.com/hueniverse/hawk/master/images/hawk.png) + + +# hawk **Hawk** is an HTTP authentication scheme using a message authentication code (MAC) algorithm to provide partial -HTTP request cryptographic verification. For more complex use cases such as access delegation, see [Oz](https://github.com/hueniverse/oz). - -Current version: **7.x** +HTTP request cryptographic verification. Note: the protocol has not changed since version 1.1. The version increments reflect changes in the node API. -[![Build Status](https://travis-ci.org/hueniverse/hawk.svg?branch=master)](https://travis-ci.org/hueniverse/hawk) +[![Build Status](https://travis-ci.org/hapi/hawk.svg?branch=master)](https://travis-ci.org/hapi/hawk) # Table of Content @@ -18,6 +18,7 @@ Note: the protocol has not changed since version 1.1. The version increments ref - [Payload Validation](#payload-validation) - [Response Payload Validation](#response-payload-validation) - [Browser Support and Considerations](#browser-support-and-considerations) + - [hapi Plugin](#hapi-plugin) - [**Single URI Authorization**](#single-uri-authorization) - [Usage Example](#bewit-usage-example) - [**Security Considerations**](#security-considerations) @@ -98,9 +99,9 @@ the number of round trips required to authenticate the first request. Server code: -```javascript +```js const Http = require('http'); -const Hawk = require('hawk'); +const Hawk = require('@hapi/hawk'); // Credentials lookup function @@ -155,9 +156,9 @@ Http.createServer(handler).listen(8000, 'example.com'); Client code: -```javascript +```js const Request = require('request'); -const Hawk = require('hawk'); +const Hawk = require('@hapi/hawk'); // Client credentials @@ -198,7 +199,7 @@ Request(requestOptions, function (error, response, body) { **Hawk** utilized the [**SNTP**](https://github.com/hueniverse/sntp) module for time sync management. By default, the local machine time is used. To automatically retrieve and synchronize the clock within the application, use the SNTP 'start()' method. -```javascript +```js Hawk.sntp.start(); ``` @@ -366,6 +367,150 @@ Hawk client from authenticating the requests.You can read more about the why and [article](http://www.html5rocks.com/en/tutorials/cors/#toc-adding-cors-support-to-the-server) +## hapi Plugin + +**hawk** includes an authentication plugin for **hapi** which registers two authentication schemes. + +### hawk Strategy + +The scheme supports payload authentication. The scheme requires the following options: + +- `getCredentialsFunc` - credential lookup function with the signature `[async] function(id)` where: + - `id` - the Hawk credentials identifier. + - _throws_ an internal error. + - _returns_ `{ credentials }` object where: + - `credentials` a credentials object passed back to the application in `request.auth.credentials`. Set to be `null` or `undefined` to + indicate unknown credentials (which is not considered an error state). +- `hawk` - optional protocol options passed to `Hawk.server.authenticate()`. + +```js +const Hapi = require('@hapi/hapi'); +const Hawk = require('@hapi/hawk'); + +const credentials = { + d74s3nz2873n: { + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256' + } +}; + +const getCredentialsFunc = function (id) { + + return credentials[id]; +}; + +const start = async () => { + + const server = Hapi.server({ port: 4000 }); + + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.auth.default('default'); + + server.route({ + method: 'GET', + path: '/', + handler: function (request, h) { + + return 'welcome'; + } + }); + + await server.start(); + + console.log('Server started listening on %s', server.info.uri); +}; + +start(); + +// Ensure process exits on unhandled rejection + +process.on('unhandledRejection', (err) => { + + throw err; +}); + +``` + +### bewit Strategy + +The scheme can only be used with 'GET' requests and requires the following options: + +- `getCredentialsFunc` - credential lookup function with the signature `async function(id)` where: + - `id` - the Hawk credentials identifier. + - _throws_ an internal error. + - _returns_ `{ credentials }` object where: + - `credentials` a credentials object passed back to the application in `request.auth.credentials`. Set to be `null` or `undefined` to + indicate unknown credentials (which is not considered an error state). +- `hawk` - optional protocol options passed to `Hawk.server.authenticateBewit()`. + +```js +const Hapi = require('@hapi/hapi'); +const Hawk = require('@hapi/hawk'); + +const credentials = { + d74s3nz2873n: { + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256' + } +}; + +const getCredentialsFunc = function (id) { + + return credentials[id]; +}; + +const start = async () => { + + const server = Hapi.server({ port: 4000 }); + + await server.register(Hawk); + + server.auth.strategy('default', 'bewit', { getCredentialsFunc }); + server.auth.default('default'); + + server.route({ + method: 'GET', + path: '/', + handler: function (request, h) { + + return 'welcome'; + } + }); + + await server.start(); + + console.log('Server started listening on %s', server.info.uri); +}; + +start(); + +// Ensure process exits on unhandled rejection + +process.on('unhandledRejection', (err) => { + + throw err; +}); +``` + +To send an authenticated Bewit request, the URI must contain the `'bewit'` query parameter which can be generated using the Hawk module: + +```js +const Hawk = require('@hapi/hawk'); + +const credentials = { + id: 'd74s3nz2873n', + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256' +}; + +let uri = 'http://example.com:8080/endpoint'; +const bewit = Hawk.client.getBewit(uri, { credentials: credentials, ttlSec: 60 }); +uri += '?bewit=' + bewit; +``` + + # Single URI Authorization There are cases in which limited and short-term access to a protected resource is granted to a third party which does not @@ -385,9 +530,9 @@ the granted access timeframe. Server code: -```javascript +```js const Http = require('http'); -const Hawk = require('hawk'); +const Hawk = require('@hapi/hawk'); // Credentials lookup function @@ -422,9 +567,8 @@ Http.createServer(handler).listen(8000, 'example.com'); Bewit code generation: -```javascript -const Request = require('request'); -const Hawk = require('hawk'); +```js +const Hawk = require('@hapi/hawk'); // Client credentials diff --git a/lib/index.js b/lib/index.js index b40024a..685788b 100755 --- a/lib/index.js +++ b/lib/index.js @@ -11,6 +11,7 @@ exports.crypto = require('./crypto'); exports.utils = require('./utils'); +exports.plugin = require('./plugin'); exports.uri = { authenticate: exports.server.authenticateBewit, diff --git a/lib/plugin.js b/lib/plugin.js new file mode 100755 index 0000000..11a8ebf --- /dev/null +++ b/lib/plugin.js @@ -0,0 +1,134 @@ +'use strict'; + +const Boom = require('@hapi/boom'); +const Hoek = require('@hapi/hoek'); + +const Crypto = require('./crypto'); +const Server = require('./server'); + + +const internals = {}; + + +exports.plugin = { + pkg: require('../package.json'), + requirements: { + hapi: '>=17.7.0' + }, + register: function (server) { + + server.auth.scheme('hawk', internals.hawk); + server.auth.scheme('bewit', internals.bewit); + } +}; + + +internals.hawk = function (server, options) { + + Hoek.assert(options, 'Invalid hawk scheme options'); + Hoek.assert(options.getCredentialsFunc, 'Missing required getCredentialsFunc method in hawk scheme configuration'); + + const settings = Hoek.clone(options); + settings.hawk = settings.hawk || {}; + + const scheme = { + authenticate: async function (request, h) { + + try { + var result = await Server.authenticate(request.raw.req, settings.getCredentialsFunc, settings.hawk); + } + catch (err) { + const { credentials, artifacts } = err; + return h.unauthenticated(err, credentials ? { credentials, artifacts } : undefined); + } + + if (request.route.settings.auth.payload) { + request.events.once('peek', (chunk) => { + + const payloadHash = Crypto.initializePayloadHash(request.auth.credentials.algorithm, request.headers['content-type']); + payloadHash.update(chunk); + + request.events.on('peek', (chunk2) => payloadHash.update(chunk2)); + + request.events.once('finish', () => { + + request.plugins['hapi-auth-hawk'] = { payloadHash: Crypto.finalizePayloadHash(payloadHash) }; + }); + }); + } + + return h.authenticated(result); + }, + payload: function (request, h) { + + if (!request.auth.artifacts.hash) { + throw Boom.unauthorized(null, 'Hawk'); // Missing + } + + const plugin = request.plugins['hapi-auth-hawk']; + + if (!plugin) { + throw Boom.unauthorized('Payload is invalid'); + } + + try { + Server.authenticatePayloadHash(plugin.payloadHash, request.auth.artifacts); + return h.continue; + } + catch (err) { + throw Boom.unauthorized('Payload is invalid'); + } + }, + response: function (request, h) { + + const response = request.response; + const payloadHash = Crypto.initializePayloadHash(request.auth.credentials.algorithm, response.headers['content-type']); + + response.header('trailer', 'server-authorization'); + response.header('transfer-encoding', 'chunked'); + + 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) + + response.events.on('peek', (chunk) => { + + payloadHash.update(chunk); + }); + + response.events.once('finish', () => { + + const header = Server.header(request.auth.credentials, request.auth.artifacts, { hash: Crypto.finalizePayloadHash(payloadHash) }); + request.raw.res.addTrailers({ 'server-authorization': header }); + }); + + return h.continue; + } + }; + + return scheme; +}; + + +internals.bewit = function (server, options) { + + Hoek.assert(options, 'Invalid bewit scheme options'); + Hoek.assert(options.getCredentialsFunc, 'Missing required getCredentialsFunc method in bewit scheme configuration'); + + const settings = Hoek.clone(options); + settings.hawk = settings.hawk || {}; + + const scheme = { + authenticate: async function (request, h) { + + try { + const { credentials, attributes } = await Server.authenticateBewit(request.raw.req, settings.getCredentialsFunc, settings.hawk); + return h.authenticated({ credentials, attributes }); + } + catch (err) { + const { credentials, attributes } = err; + return h.unauthenticated(err, credentials ? { credentials, attributes } : undefined); + } + } + }; + + return scheme; +}; diff --git a/package.json b/package.json index d0e459a..84e14ce 100755 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@hapi/code": "5.x.x", + "@hapi/hapi": "18.x.x", "@hapi/lab": "18.x.x" }, "scripts": { diff --git a/test/plugin.js b/test/plugin.js new file mode 100755 index 0000000..a0213b6 --- /dev/null +++ b/test/plugin.js @@ -0,0 +1,869 @@ +'use strict'; + +const Stream = require('stream'); + +const Boom = require('@hapi/boom'); +const Code = require('@hapi/code'); +const Hapi = require('@hapi/hapi'); +const Hawk = require('..'); +const Lab = require('@hapi/lab'); + + +const internals = {}; + + +const { it, describe, before } = exports.lab = Lab.script(); +const expect = Code.expect; + + +describe('Plugin', () => { + + describe('hawk', () => { + + const credentials = { + john: { + cred: { + id: 'john', + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256' + } + }, + jane: { + err: Boom.internal('boom') + }, + joan: { + cred: { + id: 'joan', + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256' + } + } + }; + + const getCredentialsFunc = function (id) { + + if (credentials[id]) { + if (credentials[id].err) { + throw credentials[id].err; + } + + return credentials[id].cred; + } + }; + + const hawkHeader = function (id, path) { + + if (credentials[id] && credentials[id].cred) { + return Hawk.client.header('http://example.com:8080' + path, 'POST', { credentials: credentials[id].cred }); + } + + return ''; + }; + + it('calls through to handler on successful auth', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawk', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: 'default' } + }); + + const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: hawkHeader('john', '/hawk').header } }; + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('Success'); + }); + + it('calls through to handler on failed optional auth', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkOptional', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'optional', strategy: 'default' } } + }); + + const request = { method: 'POST', url: 'http://example.com:8080/hawkOptional' }; + const res = await server.inject(request); + + expect(res.result).to.equal('Success'); + }); + + it('includes authorization header in response when the response is a stream', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkStream', + handler: function (request, h) { + + const TestStream = class extends Stream.Readable { + + _read(size) { + + if (this.isDone) { + return; + } + + this.isDone = true; + + setTimeout(() => this.push('hi'), 2); + setTimeout(() => this.push(null), 5); + } + }; + + const stream = new TestStream(); + return stream; + }, + options: { auth: 'default' } + }); + + const authHeader = hawkHeader('john', '/hawkStream'); + const request = { method: 'POST', url: 'http://example.com:8080/hawkStream', headers: { authorization: authHeader.header } }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.trailers['server-authorization']).to.contain('Hawk'); + + const options = { + payload: res.payload, + contentType: res.headers['content-type'] + }; + + const cred = getCredentialsFunc('john'); + + const header = Hawk.server.header(cred, authHeader.artifacts, options); + expect(header).to.equal(res.trailers['server-authorization']); + }); + + it('includes valid authorization header in response when the response is text', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawk', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: 'default' } + }); + + const authHeader = hawkHeader('john', '/hawk'); + const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: authHeader.header } }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.trailers['server-authorization']).to.contain('Hawk'); + + const options = { + payload: res.payload, + contentType: res.headers['content-type'] + }; + + const cred = getCredentialsFunc('john'); + + const header = Hawk.server.header(cred, authHeader.artifacts, options); + expect(header).to.equal(res.trailers['server-authorization']); + }); + + it('removes the content-length header when switching to chunked transfer encoding', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawk', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: 'default' } + }); + + const authHeader = hawkHeader('john', '/hawk'); + const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: authHeader.header } }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.headers['transfer-encoding']).to.equal('chunked'); + expect(res.headers['content-length']).to.not.exist(); + }); + + it('includes valid authorization header in response when the request fails validation', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkValidate', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: 'default', validate: { query: {} } } + }); + + const authHeader = hawkHeader('john', '/hawkValidate?a=1'); + const request = { method: 'POST', url: 'http://example.com:8080/hawkValidate?a=1', headers: { authorization: authHeader.header } }; + const res = await server.inject(request); + + expect(res.trailers['server-authorization']).to.exist(); + expect(res.trailers['server-authorization']).to.contain('Hawk'); + expect(res.statusCode).to.equal(400); + + const options = { + payload: res.payload, + contentType: res.headers['content-type'] + }; + + const cred = getCredentialsFunc('john'); + + authHeader.artifacts.credentials = cred; + const header = Hawk.server.header(cred, authHeader.artifacts, options); + expect(header).to.equal(res.trailers['server-authorization']); + }); + + it('does not include authorization header in response when the response is an error', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkError', + handler: function (request, h) { + + return new Error(); + }, + options: { auth: 'default' } + }); + + const request = { method: 'POST', url: 'http://example.com:8080/hawkError', headers: { authorization: hawkHeader('john', '/hawkError').header } }; + const res = await server.inject(request); + + expect(res.statusCode).to.equal(500); + expect(res.headers.authorization).to.not.exist(); + }); + + it('returns an error on bad auth header', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawk', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: 'default' } + }); + + const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: hawkHeader('john', 'abcd').header } }; + const res = await server.inject(request); + + expect(res.result).to.exist(); + expect(res.statusCode).to.equal(401); + }); + + it('returns an error on bad header format', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawk', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: 'default' } + }); + + const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: 'junk' } }; + const res = await server.inject(request); + + expect(res.result).to.exist(); + expect(res.statusCode).to.equal(401); + }); + + it('returns an error on bad scheme', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawk', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: 'default' } + }); + + const request = { method: 'POST', url: 'http://example.com:8080/hawk', headers: { authorization: 'junk something' } }; + const res = await server.inject(request); + + expect(res.result).to.exist(); + expect(res.statusCode).to.equal(401); + }); + + it('returns an error on insufficient scope', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkScope', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { scope: 'x', strategy: 'default' } } + }); + + const request = { method: 'POST', url: 'http://example.com:8080/hawkScope', payload: '{}', headers: { authorization: hawkHeader('john', '/hawkScope').header } }; + const res = await server.inject(request); + + expect(res.statusCode).to.equal(403); + }); + + it('returns a reply on successful auth when using a custom host header key', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { + getCredentialsFunc, + hawk: { + hostHeaderName: 'custom' + } + }); + + server.route({ + method: 'POST', + path: '/hawk', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: 'default' } + }); + + const request = { method: 'POST', url: '/hawk', headers: { authorization: hawkHeader('john', '/hawk').header, custom: 'example.com:8080' } }; + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('Success'); + }); + + it('returns a reply on successful auth and payload validation', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayload', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + const payload = 'application text formatted payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayload', 'POST', { credentials: credentials.john.cred, payload, contentType: 'text/plain' }); + const request = { + method: 'POST', + url: 'http://example.com:8080/hawkPayload', + headers: { authorization: authHeader.header, 'content-type': 'text/plain' }, + payload, + simulate: { split: true } + }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('Success'); + }); + + it('returns an error with payload validation when the payload is tampered with', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayload', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + let payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayload', 'POST', { credentials: credentials.john.cred, payload }); + payload += 'HACKED'; + const request = { method: 'POST', url: 'http://example.com:8080/hawkPayload', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(401); + expect(res.result.message).to.equal('Payload is invalid'); + }); + + it('returns an error with payload validation when the payload is absent', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayload', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + let payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayload', 'POST', { credentials: credentials.john.cred, payload }); + payload = ''; + const request = { method: 'POST', url: 'http://example.com:8080/hawkPayload', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(401); + expect(res.result.message).to.equal('Payload is invalid'); + }); + + it('returns an error with payload validation when the payload is tampered with and the route has optional validation', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayloadOptional', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: 'optional', strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + let payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadOptional', 'POST', { credentials: credentials.john.cred, payload }); + payload += 'HACKED'; + const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadOptional', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(401); + expect(res.result.message).to.equal('Payload is invalid'); + }); + + it('returns a reply on successful auth and payload validation when validation is optional', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayloadOptional', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: 'optional', strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + const payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadOptional', 'POST', { credentials: credentials.john.cred, payload }); + const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadOptional', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.result).to.exist(); + expect(res.result).to.equal('Success'); + }); + + it('returns a reply on successful auth when payload validation is optional and no payload hash exists', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayloadOptional', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: 'optional', strategy: 'default' }, payload: { override: 'text/plain' } } + } + ); + + const payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadOptional', 'POST', { credentials: credentials.john.cred }); + const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadOptional', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.result).to.exist(); + expect(res.result).to.equal('Success'); + }); + + it('returns a reply on successful auth and when payload validation is disabled', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayloadNone', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: false, strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + const payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadNone', 'POST', { credentials: credentials.john.cred, payload }); + const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadNone', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('Success'); + }); + + it('returns a reply on successful auth when the payload is tampered with and the route has disabled validation', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayloadNone', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: false, strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + let payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayloadNone', 'POST', { credentials: credentials.john.cred, payload }); + payload += 'HACKED'; + const request = { method: 'POST', url: 'http://example.com:8080/hawkPayloadNone', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('Success'); + }); + + it('returns a reply on successful auth when auth is optional and when payload validation is required', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkOptionalPayload', + config: { + handler: (request, h) => 'Success', + auth: { mode: 'optional', payload: 'required', strategy: 'default' }, + payload: { override: 'text/plain' } + } + }); + + const payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkOptionalPayload', 'POST', { credentials: credentials.john.cred, payload }); + const request = { method: 'POST', url: 'http://example.com:8080/hawkOptionalPayload', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('Success'); + }); + + it('returns an error with payload validation when the payload is tampered with and the route has optional auth', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkOptionalPayload', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'optional', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + let payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkOptionalPayload', 'POST', { credentials: credentials.john.cred, payload }); + payload += 'HACKED'; + const request = { method: 'POST', url: 'http://example.com:8080/hawkOptionalPayload', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(401); + expect(res.result.message).to.equal('Payload is invalid'); + }); + + it('returns an error with payload validation when the payload hash is not included and payload validation is required', async () => { + + const server = Hapi.server(); + await server.register(Hawk); + + server.auth.strategy('default', 'hawk', { getCredentialsFunc }); + server.route({ + method: 'POST', + path: '/hawkPayload', + handler: function (request, h) { + + return 'Success'; + }, + options: { auth: { mode: 'required', payload: 'required', strategy: 'default' }, payload: { override: 'text/plain' } } + }); + + const payload = 'Here is my payload'; + const authHeader = Hawk.client.header('http://example.com:8080/hawkPayload', 'POST', { credentials: credentials.john.cred }); + const request = { method: 'POST', url: 'http://example.com:8080/hawkPayload', headers: { authorization: authHeader.header }, payload }; + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(401); + expect(res.result.message).to.equal('Missing payload authentication'); + }); + }); + + describe('bewit', () => { + + const credentials = { + john: { + cred: { + id: 'john', + key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', + algorithm: 'sha256' + } + }, + jane: { + err: Boom.internal('boom') + } + }; + + const getCredentialsFunc = function (id) { + + if (credentials[id]) { + if (credentials[id].err) { + throw credentials[id].err; + } + + return credentials[id].cred; + } + }; + + const getBewit = function (id, path) { + + if (credentials[id] && credentials[id].cred) { + return Hawk.uri.getBewit('http://example.com:8080' + path, { credentials: credentials[id].cred, ttlSec: 60 }); + } + + return ''; + }; + + const bewitHandler = function (request, h) { + + return 'Success'; + }; + + let server = Hapi.server(); + + before(async () => { + + await server.register(Hawk); + + server.auth.strategy('default', 'bewit', { getCredentialsFunc }); + + server.route([ + { method: 'GET', path: '/bewit', handler: bewitHandler, options: { auth: 'default' } }, + { method: 'GET', path: '/bewitOptional', handler: bewitHandler, options: { auth: { mode: 'optional', strategy: 'default' } } }, + { method: 'GET', path: '/bewitScope', handler: bewitHandler, options: { auth: { scope: 'x', strategy: 'default' } } } + ]); + }); + + it('returns a reply on successful auth', async () => { + + const bewit = getBewit('john', '/bewit'); + const res = await server.inject('http://example.com:8080/bewit?bewit=' + bewit); + + expect(res.result).to.equal('Success'); + }); + + it('returns an error reply on failed optional auth', async () => { + + const bewit = getBewit('john', '/abc'); + const res = await server.inject('http://example.com:8080/bewitOptional?bewit=' + bewit); + + expect(res.statusCode).to.equal(401); + }); + + it('returns an error on bad bewit', async () => { + + const bewit = getBewit('john', '/abc'); + const res = await server.inject('http://example.com:8080/bewit?bewit=' + bewit); + + expect(res.statusCode).to.equal(401); + }); + + it('returns an error on bad bewit format', async () => { + + const res = await server.inject('http://example.com:8080/bewit?bewit=junk'); + + expect(res.statusCode).to.equal(400); + }); + + it('returns an error on insufficient scope', async () => { + + const bewit = getBewit('john', '/bewitScope'); + const res = await server.inject('http://example.com:8080/bewitScope?bewit=' + bewit); + + expect(res.statusCode).to.equal(403); + }); + + it('returns a reply on successful auth when using a custom host header key', async () => { + + const bewit = getBewit('john', '/bewit'); + const request = { method: 'GET', url: '/bewit?bewit=' + bewit, headers: { custom: 'example.com:8080' } }; + + server = new Hapi.Server(); + await server.register(Hawk); + + server.auth.strategy('default', 'bewit', { + getCredentialsFunc, + hawk: { + hostHeaderName: 'custom' + } + }); + + server.route({ method: 'GET', path: '/bewit', handler: bewitHandler, options: { auth: 'default' } }); + + const res = await server.inject(request); + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('Success'); + }); + + it('cannot add a route that has payload validation required', () => { + + const fn = function () { + + server.route({ + method: 'POST', + path: '/bewitPayload', + handler: bewitHandler, + options: { + auth: { mode: 'required', strategy: 'default', payload: 'required' }, + payload: { output: 'stream', parse: false } + } + }); + }; + + expect(fn).to.throw('Payload validation can only be required when all strategies support it in /bewitPayload'); + }); + + it('cannot add a route that has payload validation as optional', () => { + + const fn = function () { + + server.route({ + method: 'POST', + path: '/bewitPayload', + handler: bewitHandler, + options: { + auth: { mode: 'required', strategy: 'default', payload: 'optional' }, + payload: { output: 'stream', parse: false } + } + }); + }; + + expect(fn).to.throw('Payload authentication requires at least one strategy with payload support in /bewitPayload'); + }); + + it('can add a route that has payload validation as none', () => { + + const fn = function () { + + server.route({ + method: 'POST', + path: '/bewitPayload', + handler: bewitHandler, + options: { + auth: { mode: 'required', strategy: 'default', payload: false }, + payload: { output: 'stream', parse: false } + } + }); + }; + + expect(fn).to.not.throw(); + }); + }); +});