Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: load token from url query parameter #50

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,11 @@ validate: (artifacts, request, h) => {

##### cookieName

- `cookieName` - Tells the jwt plugin to read the token from the cookie specified. Note that the plugin does not allow you to read from cookie and header at the same time, either read from a header or from a cookie. If you want to read from cookie and header you must use multiple strategies with in which one will have `headerName` config and other will have `cookieName` config. Defaults to `undefined`.
- `cookieName` - Tells the jwt plugin to read the token from the cookie specified. Note that the plugin does not allow you to read from cookie, header and url query parameter at the same time, either read from a header or from a cookie or from a query parameter. If you want to read from multiple sources you must use multiple strategies in which one will have `headerName` config and other will have `cookieName` config and so on. Defaults to `undefined`.

##### urlQueryParamName

- `urlQueryParamName` - Tells the jwt plugin to read the token from the url query parameter specified. When the url query parameter is specified multiple times (`https://example.com?token=token1&token=token2`) the value of the last one is used. Note that the plugin does not allow you to read from cookie, header and url query parameter at the same time, either read from a header or from a cookie or from a query parameter. Defaults to `undefined`.


## token
Expand Down
55 changes: 42 additions & 13 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,16 @@ internals.schema.strategy = Joi.object({
cookieName: Utils.validHttpTokenSchema
.optional()
.messages({
'string.pattern.base':
'Cookie name cannot start or end with special characters. Valid characters in cookie name are _, -, numbers and alphabets'
'string.pattern.base': 'cookieName must be a valid http header name following https://tools.ietf.org/html/rfc7230#section-3.2.6'
}),

headerName: Joi.any().when('cookieName', {
is: Joi.exist(),
then: Joi.string().forbidden().messages({ 'any.unknown': 'headerName not allowed when cookieName is specified' }),
otherwise: Utils.validHttpTokenSchema.optional()
.default('authorization')
.messages({
'string.pattern.base': 'Header name must be a valid header name following https://tools.ietf.org/html/rfc7230#section-3.2.6'
})
}),
headerName: Utils.validHttpTokenSchema
.optional()
.messages({
'string.pattern.base': 'headerName must be a valid http header name following https://tools.ietf.org/html/rfc7230#section-3.2.6'
}),

urlQueryParamName: Joi.string().optional(),

headless: [Joi.string(), Joi.object({ alg: Joi.string().valid(...Keys.supportedAlgorithms).required(), typ: Joi.valid('JWT') }).unknown()],

Expand Down Expand Up @@ -133,7 +130,25 @@ internals.schema.strategy = Joi.object({
})
.when('.validate', { is: Joi.not(false), then: Joi.allow(false) })
.required()
});
}).when(
Joi.object({
headerName: Joi.any(),
cookieName: Joi.any(),
urlQueryParamName: Joi.any()
})
.unknown()
.or('cookieName', 'headerName', 'urlQueryParamName'),
{
then: Joi.object().xor('cookieName', 'headerName', 'urlQueryParamName')
.messages({
'object.xor': 'cookieName, headerName and urlQueryParam cannot be specified at the same time'
}),
otherwise: Joi.object({
headerName: Utils.validHttpTokenSchema.default('authorization')
})
}
);



internals.implementation = function (server, options) {
Expand Down Expand Up @@ -252,9 +267,23 @@ internals.token = function (request, settings, missing, unauthorized) {
if (settings.headerName) {
authorization = request.headers[settings.headerName];
}
else {
else if (settings.cookieName) {
authorization = request.state[settings.cookieName];
}
else {
const isQueryParamArray = Array.isArray(request.query[settings.urlQueryParamName]);

// We have to check this because there can be multiple query parameters passed which are same
// which will be kept as an array

if (isQueryParamArray) {
authorization =
request.query[settings.urlQueryParamName][request.query[settings.urlQueryParamName].length - 1];
}
else {
authorization = request.query[settings.urlQueryParamName];
}
}

if (!authorization) {
throw missing;
Expand Down
191 changes: 183 additions & 8 deletions test/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -528,13 +528,42 @@ describe('Plugin', () => {
await jwks.server.stop();
});

it('uses authorization as headerName when cookieName or headerName is not specified in config', async () => {
it('does not allow to specify multiple token sources at the same time', async () => {

const secret = 'some_shared_secret';

const server = Hapi.server();

server.register(Jwt);
await server.register(Jwt);

expect(
() => {

server.auth.strategy('jwt', 'jwt', {
keys: secret,
verify: {
aud: 'urn:audience:test',
iss: 'urn:issuer:test',
sub: false
},
validate: (artifacts, request, h) => {

return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
},
headerName: 'test',
urlQueryParamName: 'test'
});
}
).to.throw('cookieName, headerName and urlQueryParam cannot be specified at the same time');
});

it('uses authorization as headerName when cookieName or headerName or urlQueryParamName is not specified in config', async () => {

const secret = 'some_shared_secret';

const server = Hapi.server();

await server.register(Jwt);

server.auth.strategy('jwt', 'jwt', {
keys: secret,
Expand Down Expand Up @@ -626,7 +655,7 @@ describe('Plugin', () => {
expect(res.result).to.equal('steve');
});

it('errors when token is not present at headerName or cookieName', async () => {
it('errors when token is not present at headerName or cookieName or urlQueryParamName specified', async () => {

const secret = 'some_shared_secret';

Expand All @@ -636,6 +665,7 @@ describe('Plugin', () => {

const headerName = 'random-header';
const cookieName = 'random-cookie';
const urlQueryParamName = 'random-url-param';

server.auth.strategy('jwt-header', 'jwt', {
keys: secret,
Expand Down Expand Up @@ -665,23 +695,168 @@ describe('Plugin', () => {
cookieName
});

server.auth.strategy('jwt-url-query-param', 'jwt', {
keys: secret,
verify: {
aud: 'urn:audience:test',
iss: 'urn:issuer:test',
sub: false
},
validate: (artifacts, request, h) => {

return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
},
urlQueryParamName
});

const handler = (request) => request.auth.credentials.user;

server.route({
path: '/header-strategy',
method: 'GET',
options: {
auth: {
strategy: 'jwt-header'
}
},
handler
});

server.route({
path: '/cookie-strategy',
method: 'GET',
options: {
auth: {
strategy: 'jwt-cookie'
}
},
handler
});

server.route({
path: '/',
path: '/url-strategy',
method: 'GET',
options: {
auth: {
strategies: ['jwt-header', 'jwt-cookie']
strategy: 'jwt-url-query-param'
}
},
handler: (request) => request.auth.credentials.user
handler
});

const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });

const resWithHeader = await server.inject({ url: '/', headers: { 'another-header-name': `Bearer ${token}` } });
const resWithHeader = await server.inject({ url: '/header-strategy', headers: { 'another-header-name': `Bearer ${token}` } });
expect(resWithHeader.statusCode).to.equal(401);

const resWithCookie = await server.inject({ url: '/', headers: { 'cookie': `another-cookie=${token}` } });
const resWithCookie = await server.inject({ url: '/cookie-strategy', headers: { 'cookie': `another-cookie=${token}` } });
expect(resWithCookie.statusCode).to.equal(401);

const resWithUrl = await server.inject({ url: '/url-strategy?another-query-param=test-token' });
expect(resWithUrl.statusCode).to.equal(401);
});

it('reads token from url search param specified in urlQueryParamName and authenticates', async () => {

const secret = 'some_shared_secret';

const server = Hapi.server();

server.register(Jwt);

const urlQueryParamName = 'random-url-search-param';

server.auth.strategy('jwt', 'jwt', {
keys: secret,
verify: {
aud: 'urn:audience:test',
iss: 'urn:issuer:test',
sub: false
},
validate: (artifacts, request, h) => {

return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
},
urlQueryParamName
});

server.auth.default('jwt');

server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });

const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });
const res = await server.inject({ url: `/?${urlQueryParamName}=${token}` });

expect(res.result).to.equal('steve');
});

it('reads token from url query param specified in urlQueryParamName and authenticates', async () => {

const secret = 'some_shared_secret';

const server = Hapi.server();

server.register(Jwt);

const urlQueryParamName = 'random-url-search-param';

server.auth.strategy('jwt', 'jwt', {
keys: secret,
verify: {
aud: 'urn:audience:test',
iss: 'urn:issuer:test',
sub: false
},
validate: (artifacts, request, h) => {

return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
},
urlQueryParamName
});

server.auth.default('jwt');

server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });

const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });
const res = await server.inject({ url: `/?${urlQueryParamName}=${token}` });

expect(res.result).to.equal('steve');
});

it('reads token from last url query param value is an array', async () => {

const secret = 'some_shared_secret';

const server = Hapi.server();

server.register(Jwt);

const urlQueryParamName = 'random-url-search-param';

server.auth.strategy('jwt', 'jwt', {
keys: secret,
verify: {
aud: 'urn:audience:test',
iss: 'urn:issuer:test',
sub: false
},
validate: (artifacts, request, h) => {

return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
},
urlQueryParamName
});

server.auth.default('jwt');

server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });

const token1 = Jwt.token.generate({ user: 'steve1', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });
const token2 = Jwt.token.generate({ user: 'steve2', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });

const res = await server.inject({ url: `/?${urlQueryParamName}=${token1}&${urlQueryParamName}=${token2}` });

expect(res.result).to.equal('steve2');
});
});