зеркало из https://github.com/mozilla/hawk.git
Merge plugin. Closes #255
This commit is contained in:
Родитель
25ab0702a5
Коммит
84487d5a03
|
@ -10,6 +10,11 @@ sudo: false
|
|||
|
||||
install:
|
||||
- "npm install"
|
||||
- "npm install hapi@$HAPI_VERSION"
|
||||
|
||||
env:
|
||||
- HAPI_VERSION="17"
|
||||
- HAPI_VERSION="18"
|
||||
|
||||
os:
|
||||
- "linux"
|
||||
|
|
174
README.md
174
README.md
|
@ -1,13 +1,13 @@
|
|||
![hawk Logo](https://raw.github.com/hueniverse/hawk/master/images/hawk.png)
|
||||
<a href="http://hapijs.com"><img src="https://github.com/hapijs/assets/blob/master/images/family.svg" width="180px" align="right" /></a>
|
||||
|
||||
# hawk
|
||||
|
||||
<img align="right" src="https://raw.github.com/hueniverse/hawk/master/images/logo.png" /> **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
|
||||
|
|
|
@ -11,6 +11,7 @@ exports.crypto = require('./crypto');
|
|||
|
||||
exports.utils = require('./utils');
|
||||
|
||||
exports.plugin = require('./plugin');
|
||||
|
||||
exports.uri = {
|
||||
authenticate: exports.server.authenticateBewit,
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@hapi/code": "5.x.x",
|
||||
"@hapi/hapi": "18.x.x",
|
||||
"@hapi/lab": "18.x.x"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче