diff --git a/README.md b/README.md index 322e06d..6ba1b86 100755 --- a/README.md +++ b/README.md @@ -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) diff --git a/lib/index.js b/lib/index.js index a121ca2..871654a 100755 --- a/lib/index.js +++ b/lib/index.js @@ -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'; @@ -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 diff --git a/test/index.js b/test/index.js index 9638fee..09ff49d 100755 --- a/test/index.js +++ b/test/index.js @@ -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'); + }); + + +});