diff --git a/API.md b/API.md index 94e3818..1059654 100644 --- a/API.md +++ b/API.md @@ -329,150 +329,6 @@ 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 diff --git a/lib/index.js b/lib/index.js index 685788b..e61925c 100755 --- a/lib/index.js +++ b/lib/index.js @@ -11,8 +11,6 @@ exports.crypto = require('./crypto'); exports.utils = require('./utils'); -exports.plugin = require('./plugin'); - exports.uri = { authenticate: exports.server.authenticateBewit, getBewit: exports.client.getBewit diff --git a/lib/plugin.js b/lib/plugin.js deleted file mode 100755 index 44420c8..0000000 --- a/lib/plugin.js +++ /dev/null @@ -1,134 +0,0 @@ -'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: '>=18.4.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 (server.auth.lookup(request.route).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 68483a2..394ad6a 100755 --- a/package.json +++ b/package.json @@ -14,15 +14,14 @@ "hawk" ], "dependencies": { - "@hapi/hoek": "9.x.x", "@hapi/b64": "5.x.x", "@hapi/boom": "9.x.x", "@hapi/cryptiles": "5.x.x", + "@hapi/hoek": "9.x.x", "@hapi/sntp": "4.x.x" }, "devDependencies": { "@hapi/code": "8.x.x", - "@hapi/hapi": "19.x.x", "@hapi/lab": "22.x.x" }, "scripts": { diff --git a/test/plugin.js b/test/plugin.js deleted file mode 100755 index ef97a3e..0000000 --- a/test/plugin.js +++ /dev/null @@ -1,892 +0,0 @@ -'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('handles 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('handles 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('handles successful auth (default strategy)', async () => { - - const server = Hapi.server(); - await server.register(Hawk); - - server.auth.strategy('default', 'hawk', { getCredentialsFunc }); - server.auth.default('default'); - server.route({ - method: 'POST', - path: '/hawk', - handler: function (request, h) { - - return 'Success'; - } - }); - - 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('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: false } } - }); - - 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(); - }); - }); -});