Skip to content

Commit

Permalink
Merge pull request #103 from hollandben/add-support-for-custom-header
Browse files Browse the repository at this point in the history
Add an option to specify the CSRF header name
  • Loading branch information
geek committed Apr 23, 2018
2 parents d0d028b + 63466ff commit 98a1d50
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ The following options are available when registering the plugin.
* `autoGenerate` - whether to automatically generate a new crumb for requests. Defaults to `true`.
* `addToViewContext` - whether to automatically add the crumb to view contexts as the given key. Defaults to `true`.
* `cookieOptions` - storage options for the cookie containing the crumb, see the [server.state](http://hapijs.com/api#serverstatename-options) documentation of hapi for more information. Default to `cookieOptions.path=/`
* `headerName` - specify the name of the custom CSRF header. Defaults to `X-CSRF-Token`.
* `restful` - RESTful mode that validates crumb tokens from *"X-CSRF-Token"* request header for **POST**, **PUT**, **PATCH** and **DELETE** server routes. Disables payload/query crumb validation. Defaults to `false`.
* `skip` - a function with the signature of `function (request, h) {}`, which when provided, is called for every request. If the provided function returns true, validation and generation of crumb is skipped. Defaults to `false`.
* `logUnauthorized` - whether to add to the request log with tag 'crumb' and data 'validation failed' (defaults to false)
Expand Down
8 changes: 5 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internals.schema = Joi.object().keys({
autoGenerate: Joi.boolean().optional(),
addToViewContext: Joi.boolean().optional(),
cookieOptions: Joi.object().keys(null),
headerName: Joi.string().optional(),
restful: Joi.boolean().optional(),
skip: Joi.func().optional(),
logUnauthorized: Joi.boolean().optional()
Expand All @@ -34,8 +35,9 @@ internals.defaults = {
cookieOptions: { // Cookie options (i.e. hapi server.state)
path: '/'
},
restful: false, // Set to true for X-CSRF-Token header crumb validation. Disables payload/query validation
skip: false, // Set to a function which returns true when to skip crumb generation and validation
headerName: 'X-CSRF-Token', // Specify the name of the custom CSRF header
restful: false, // Set to true for custom header crumb validation. Disables payload/query validation
skip: false, // Set to a function which returns true when to skip crumb generation and validation,
logUnauthorized: false // Set to true for crumb to write an event to the request log
};

Expand Down Expand Up @@ -126,7 +128,7 @@ const register = (server, options) => {
return h.continue;
}

const header = request.headers['x-csrf-token'];
const header = request.headers[settings.headerName.toLowerCase()];

if (!header) {
unauthorizedLogger();
Expand Down
232 changes: 232 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -927,4 +927,236 @@ describe('Crumb', () => {

expect(res12.statusCode).to.equal(200);
});

it('validates crumb with a custom header name', async () => {

const server = new Hapi.Server();

server.route([
{
method: 'GET',
path: '/1',
handler: (request, h) => {

expect(request.plugins.crumb).to.exist();
expect(request.server.plugins.crumb.generate).to.exist();

return h.view('index', {
title: 'test',
message: 'hi'
});
}
},
{
method: 'POST',
path: '/2',
handler: (request, h) => {

expect(request.payload).to.equal({ key: 'value' });
return 'valid';
}
},
{
method: 'POST',
path: '/3',
options: { payload: { output: 'stream' } },
handler: (request, h) => 'never'
},
{
method: 'PUT',
path: '/4',
handler: (request, h) => {

expect(request.payload).to.equal({ key: 'value' });
return 'valid';
}
},
{
method: 'PATCH',
path: '/5',
handler: (request, h) => {

expect(request.payload).to.equal({ key: 'value' });
return 'valid';
}
},
{
method: 'DELETE',
path: '/6',
handler: (request, h) => 'valid'
},
{
method: 'POST',
path: '/7',
options: {
plugins: {
crumb: false
}
},
handler: (request, h) => {

expect(request.payload).to.equal({ key: 'value' });
return 'valid';
}
},
{
method: 'POST',
path: '/8',
options: {
plugins: {
crumb: {
restful: false,
source: 'payload'
}
}
},
handler: (request, h) => {

expect(request.payload).to.equal({ key: 'value' });
return 'valid';
}
}

]);

await server.register([
Vision,
{
plugin: Crumb,
options: {
restful: true,
cookieOptions: {
isSecure: true
},
headerName: 'X-CUSTOM-TOKEN'
}
}
]);

server.views(internals.viewOptions);

const res = await server.inject({
method: 'GET',
url: '/1'
});

const header = res.headers['set-cookie'];
expect(header.length).to.equal(1);
expect(header[0]).to.contain('Secure');

const cookie = header[0].match(/crumb=([^\x00-\x20\"\,\;\\\x7F]*)/);

const validHeader = {
cookie: 'crumb=' + cookie[1],
'x-custom-token': cookie[1]
};

const invalidHeader = {
cookie: 'crumb=' + cookie[1],
'x-custom-token': 'x' + cookie[1]
};

expect(res.result).to.equal(Views.viewWithCrumb(cookie[1]));

const res2 = await server.inject({
method: 'POST',
url: '/2',
payload: '{ "key": "value" }',
headers: validHeader
});

expect(res2.result).to.equal('valid');

const res3 = await server.inject({
method: 'POST',
url: '/2',
payload: '{ "key": "value" }',
headers: invalidHeader
});

expect(res3.statusCode).to.equal(403);

const res4 = await server.inject({
method: 'POST',
url: '/3',
headers: {
cookie: 'crumb=' + cookie[1]
}
});

expect(res4.statusCode).to.equal(403);

const res5 = await server.inject({
method: 'PUT',
url: '/4',
payload: '{ "key": "value" }',
headers: validHeader
});

expect(res5.result).to.equal('valid');

const res6 = await server.inject({
method: 'PUT',
url: '/4',
payload: '{ "key": "value" }',
headers: invalidHeader
});

expect(res6.statusCode).to.equal(403);

const res7 = await server.inject({
method: 'PATCH',
url: '/5',
payload: '{ "key": "value" }',
headers: validHeader
});

expect(res7.result).to.equal('valid');

const res8 = await server.inject({
method: 'PATCH',
url: '/5',
payload: '{ "key": "value" }',
headers: invalidHeader
});

expect(res8.statusCode).to.equal(403);

const res9 = await server.inject({
method: 'DELETE',
url: '/6',
headers: validHeader
});

expect(res9.result).to.equal('valid');

const res10 = await server.inject({
method: 'DELETE',
url: '/6',
headers: invalidHeader
});

expect(res10.statusCode).to.equal(403);

const res11 = await server.inject({
method: 'POST',
url: '/7',
payload: '{ "key": "value" }'
});

expect(res11.result).to.equal('valid');

const payload = { key: 'value', crumb: cookie[1] };

delete validHeader['x-custom-token'];

const res12 = await server.inject({
method: 'POST',
url: '/8',
payload: JSON.stringify(payload),
headers: validHeader
});

expect(res12.statusCode).to.equal(200);
});
});

0 comments on commit 98a1d50

Please sign in to comment.