Skip to content

Add possibility to assign array of strings to token name #214

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

Open
wants to merge 2 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ This module creates a `'bearer-access-token'` scheme takes the following options
- `credentials` - a credentials object passed back to the application in `request.auth.credentials`. Note that due to underlying Hapi expectations, this value must be defined even if `isValid` is `false`. We recommend it be set to `{}` if `isValid` is `false` and you have no other value to provide.
- `artifacts` - optional [authentication](http://hapijs.com/tutorials/auth) related data that is not part of the user's credential.
- `options` - (optional)
- `accessTokenName` (Default: `'access_token'`) - Rename token key e.g. 'new_name' would rename the token query parameter to `/route1?new_name=1234`.
- `accessTokenName` (Default: `'access_token'`) - Rename token key e.g. 'new_name' would rename the token query parameter to `/route1?new_name=1234`. Alternatively can be assign as array of strings: `accessTokenName: [ 'access_token', 'custom_access_token' ]`

- `allowQueryToken` (Default: `false`) - Accept token via query parameter.
- `allowCookieToken` (Default: `false`) - Accept token via cookie.
- `allowMultipleHeaders` (Default: `false`) - Accept multiple authorization headers, e.g. `Authorization: FD AF6C74D1-BBB2-4171-8EE3-7BE9356EB018; Bearer 12345678`.
- `tokenType` (Default: `'Bearer'`) - Accept a custom token type e.g. `Authorization: Basic 12345678`.
- `tokenType` (Default: `'Bearer'`) - Accept a custom token type e.g. `Authorization: Basic 12345678`. Alternatively can be assign as array of strings: `accessTokenName: [ 'Bearer', 'Customkey' ]`
- `allowChaining` (Default: `false`) - Allow attempt of additional authentication strategies.
- `unauthorized` (Default: `Boom.unauthorized`) - A function to call when unauthorized with signature `function([message], [scheme], [attributes])`. [More details](https://github.com/hapijs/boom#boomunauthorizedmessage-scheme-attributes)

Expand Down
96 changes: 62 additions & 34 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,63 +20,91 @@ internals.defaults = {

internals.schema = Joi.object().keys({
validate: Joi.func().required(),
accessTokenName: Joi.string().required(),
accessTokenName: Joi.alternatives().try(
Joi.string().required(),
Joi.array().items(Joi.string().required())
).required(),
allowQueryToken: Joi.boolean(),
allowCookieToken: Joi.boolean(),
allowMultipleHeaders: Joi.boolean(),
allowChaining: Joi.boolean(),
tokenType: Joi.string().required(),
tokenType: Joi.alternatives().try(
Joi.string().required(),
Joi.array().items(Joi.string().required())
).required(),
unauthorized: Joi.func()
});

internals.implementation = (server, options) => {
// Look the first token in object
const getTokenFromObject = function (object, tokenTypes) {

Hoek.assert(options, 'Missing bearer auth strategy options');
for (const type of tokenTypes) {
if (object[ type ]) {
return { tokenType: type, token: object[ type ] };
}
}

const settings = Hoek.applyToDefaults(internals.defaults, options);
Joi.assert(settings, internals.schema);
return null;
};

const headerRegExp = new RegExp(settings.tokenType + '\\s+([^;$]+)','i');
const getToken = function (request, settings) {

const scheme = {
authenticate: async (request, h) => {
const tokenTypes = Array.isArray(settings.tokenType) ? settings.tokenType : [settings.tokenType];

let authorization = request.raw.req.headers.authorization;
const headerTokens = typeof (request.raw.req.headers.authorization) === 'string' ? request.raw.req.headers.authorization.split(';') : [];

if (settings.allowCookieToken
&& !authorization
&& request.state[settings.accessTokenName] ) {
const regExps = tokenTypes.map( (type) => {

authorization = `${settings.tokenType} ${request.state[settings.accessTokenName]}`;
}
return { type, regexp: new RegExp(type + '\\s+([^;$]+)','i') };
});

if (settings.allowQueryToken
&& !authorization
&& request.query[settings.accessTokenName] ) {
for (const possibleToken of headerTokens) {
for (const regxp of regExps) {
const test = possibleToken.match(regxp.regexp);

authorization = `${settings.tokenType} ${request.query[settings.accessTokenName]}`;
delete request.query[settings.accessTokenName];
if (test) {
return { tokenType: regxp.type, token: test[1] };
}
}
}

if (!authorization) {
return settings.unauthorized(null, settings.tokenType);
}
const accessTokenTypes = Array.isArray(settings.accessTokenName) ? settings.accessTokenName : [settings.accessTokenName];

if (settings.allowMultipleHeaders) {
const headers = authorization.match(headerRegExp);
if (headers !== null) {
authorization = headers[0];
}
}
if (settings.allowCookieToken) {
const token = getTokenFromObject(request.state, accessTokenTypes);
if (token) {
return token;
}
}

if (settings.allowQueryToken) {
const token = getTokenFromObject(request.query, accessTokenTypes);

if (token) {
return token;
}
}

return null;
};

internals.implementation = (server, options) => {

Hoek.assert(options, 'Missing bearer auth strategy options');

const settings = Hoek.applyToDefaults(internals.defaults, options);
Joi.assert(settings, internals.schema);

const scheme = {
authenticate: async (request, h) => {

const [tokenType, token] = authorization.split(/\s+/);
const accessToken = getToken(request, settings);

if (!token
|| tokenType.toLowerCase() !== settings.tokenType.toLowerCase()) {
if (!accessToken) {
throw settings.unauthorized(null, settings.tokenType);
}

const { isValid, credentials, artifacts } = await settings.validate(request, token, h);
const { isValid, credentials, artifacts } = await settings.validate(request, accessToken.token, h);

if (!isValid) {
let message = 'Bad token';
Expand All @@ -88,7 +116,7 @@ internals.implementation = (server, options) => {
}
}

return h.unauthenticated(settings.unauthorized(message, settings.tokenType), { credentials: credentials || {}, artifacts });
return h.unauthenticated(settings.unauthorized(message, accessToken.tokenType), { credentials: credentials || {}, artifacts });
}

if (!credentials
Expand Down
54 changes: 54 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,3 +631,57 @@ describe('default chain of strategies', () => {
expect(res.result).to.equal('success');
});
});

describe('tokenType can be string and array of strings', () => {

it(`Allow to be tokenType=['Bearer', 'Token', 'Customkey']`, async () => {

const server = new Hapi.Server({ debug: false });
await server.register(require('../'));

server.auth.strategy('default', 'bearer-access-token', {
tokenType: ['Bearer', 'Token', 'Customkey'],
validate: defaultValidateFunc
});
server.auth.default('default');

server.route([
{ method: 'POST', path: '/basic', handler: defaultHandler, options: { auth: 'default' } }
]);

const request = { method: 'POST', url: '/basic', headers: { authorization: 'Bearer 12345678' } };

const res = await server.inject(request);

//console.log('*** res:', res)

expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('success');
});

it(`Allow to be accessTokenName=['access_token', 'custom_token'] for query tokens`, async () => {

const server = new Hapi.Server({ debug: false });
await server.register(require('../'));

server.auth.strategy('query_token_enabled', 'bearer-access-token', {
accessTokenName: ['access_token', 'custom_token'],
validate: defaultValidateFunc,
allowQueryToken: true
});
server.auth.default('query_token_enabled');

server.route([
{ method: 'GET', path: '/query_token_enabled', handler: defaultHandler, options: { auth: 'query_token_enabled' } }
]);

const request = { method: 'GET', url: '/query_token_enabled?access_token=12345678' };

const res = await server.inject(request);

expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('success');
});


});