Skip to content

Commit

Permalink
feat: add specCompliance option, option to set case insensitive for b…
Browse files Browse the repository at this point in the history
…earerType (#172)

* feat: add bearerTypeCaseSensitive option

* feat: add bearerTypeCaseSensitive option

* set correct specCompliance value in readme.md

* Update Readme.md

Co-authored-by: KaKa <[email protected]>
Signed-off-by: Aras Abbasi <[email protected]>

---------

Signed-off-by: Aras Abbasi <[email protected]>
Co-authored-by: KaKa <[email protected]>
  • Loading branch information
Uzlopak and climba03003 authored Dec 31, 2023
1 parent c27b75a commit bf525cb
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 14 deletions.
8 changes: 8 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ sent to the client (optional)
* `contentType`: If the content to be sent is anything other than
`application/json`, then the `contentType` property must be set (optional)
* `bearerType`: string specifying the Bearer string (optional)
* `specCompliance`:
Configure how this plugin follow the spec. Accept either
[`rfc6749`](https://datatracker.ietf.org/doc/html/rfc6749) or
[`rfc6750`](https://datatracker.ietf.org/doc/html/rfc6750).
Default is set to `rfc6750`.
* `rfc6749` is about the generic OAuth2.0 protocol which allows token type to be case-insensitive.
* `rfc6750` is about the Bearer Token Usage which forces the token type to be exact match.
* `function auth (key, req) {}` : this function will test if `key` is a valid token.
The function must return a literal `true` if the key is accepted or a literal
`false` if rejected. The function may also return a promise that resolves to
Expand All @@ -68,6 +75,7 @@ The default configuration object is:
keys: new Set(),
contentType: undefined,
bearerType: 'Bearer',
specCompliance: 'rfc6750',
errorResponse: (err) => {
return {error: err.message}
},
Expand Down
17 changes: 10 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ function fastifyBearerAuth (fastify, options, done) {
done(new FST_BEARER_AUTH_INVALID_LOG_LEVEL(options.verifyErrorLogLevel))
}

if (options.addHook === true) {
fastify.addHook('onRequest', verifyBearerAuthFactory(options))
} else {
fastify.decorate('verifyBearerAuthFactory', verifyBearerAuthFactory)
fastify.decorate('verifyBearerAuth', verifyBearerAuthFactory(options))
try {
if (options.addHook === true) {
fastify.addHook('onRequest', verifyBearerAuthFactory(options))
} else {
fastify.decorate('verifyBearerAuthFactory', verifyBearerAuthFactory)
fastify.decorate('verifyBearerAuth', verifyBearerAuthFactory(options))
}
done()
} catch (err) {
done(err)
}

done()
}

module.exports = fp(fastifyBearerAuth, {
Expand Down
4 changes: 3 additions & 1 deletion lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ const { createError } = require('@fastify/error')
const FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE = createError('FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE', 'options.keys has to be an Array or a Set')
const FST_BEARER_AUTH_INVALID_LOG_LEVEL = createError('FST_BEARER_AUTH_INVALID_LOG_LEVEL', 'fastify.log does not have level \'%s\'')
const FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE = createError('FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE', 'options.keys has to contain only string entries')
const FST_BEARER_AUTH_INVALID_SPEC = createError('FST_BEARER_AUTH_INVALID_SPEC', 'options.specCompliance has to be set to \'rfc6750\' or \'rfc6749\'')

module.exports = {
FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE,
FST_BEARER_AUTH_INVALID_LOG_LEVEL,
FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE
FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE,
FST_BEARER_AUTH_INVALID_SPEC
}
33 changes: 27 additions & 6 deletions lib/verify-bearer-auth-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@
const authenticate = require('./authenticate')
const {
FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE,
FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE
FST_BEARER_AUTH_KEYS_OPTION_INVALID_KEY_TYPE,
FST_BEARER_AUTH_INVALID_SPEC
} = require('./errors')

const validSpecs = new Set([
'rfc6749',
'rfc6750'
])

const defaultOptions = {
keys: [],
auth: undefined,
errorResponse (err) {
return { error: err.message }
},
contentType: undefined,
bearerType: 'Bearer'
bearerType: 'Bearer',
specCompliance: 'rfc6750'
}

module.exports = function verifyBearerAuthFactory (options) {
module.exports = function verifyBearerAuthFactory (options, done) {
const _options = Object.assign({}, defaultOptions, options)
if (_options.keys instanceof Set) {
_options.keys = Array.from(_options.keys)
Expand All @@ -27,7 +34,11 @@ module.exports = function verifyBearerAuthFactory (options) {
throw new FST_BEARER_AUTH_INVALID_KEYS_OPTION_TYPE()
}

const { keys, errorResponse, contentType, bearerType, auth, addHook = true, verifyErrorLogLevel = 'error' } = _options
const { keys, errorResponse, contentType, bearerType, specCompliance, auth, addHook = true, verifyErrorLogLevel = 'error' } = _options

if (validSpecs.has(specCompliance) === false) {
throw new FST_BEARER_AUTH_INVALID_SPEC()
}

for (let i = 0, il = keys.length; i < il; ++i) {
if (typeof keys[i] !== 'string') {
Expand All @@ -36,8 +47,18 @@ module.exports = function verifyBearerAuthFactory (options) {
keys[i] = Buffer.from(keys[i])
}

const bearerTypePrefix = bearerType + ' '
const bearerTypePrefixLength = bearerType.length + 1
const bearerTypePrefix = specCompliance === 'rfc6750'
? bearerType + ' '
: bearerType.toLowerCase() + ' '

const verifyBearerType = specCompliance === 'rfc6750'
? function (authorizationHeader) {
return authorizationHeader.substring(0, bearerTypePrefixLength) !== bearerTypePrefix
}
: function (authorizationHeader) {
return authorizationHeader.substring(0, bearerTypePrefixLength).toLowerCase() !== bearerTypePrefix
}

function handleUnauthorized (request, reply, done, message) {
const noHeaderError = Error(message)
Expand All @@ -57,7 +78,7 @@ module.exports = function verifyBearerAuthFactory (options) {
return handleUnauthorized(request, reply, done, 'missing authorization header')
}

if (authorizationHeader.substring(0, bearerTypePrefixLength) !== bearerTypePrefix) {
if (verifyBearerType(authorizationHeader)) {
return handleUnauthorized(request, reply, done, 'invalid authorization header')
}

Expand Down
15 changes: 15 additions & 0 deletions test/spec-compliance-invalid.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict'

const tap = require('tap')
const test = tap.test
const Fastify = require('fastify')
const plugin = require('../')
const { FST_BEARER_AUTH_INVALID_SPEC } = require('../lib/errors')

test('throws FST_BEARER_AUTH_INVALID_SPEC when invalid value for specCompliance was used', async (t) => {
t.plan(1)

const fastify = Fastify()

t.rejects(() => fastify.register(plugin, { keys: new Set(['123456']), specCompliance: 'invalid' }), new FST_BEARER_AUTH_INVALID_SPEC())
})
85 changes: 85 additions & 0 deletions test/spec-compliance-rfc-6749.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict'

const tap = require('tap')
const test = tap.test
const fastify = require('fastify')()
const plugin = require('../')

fastify.register(plugin, { keys: new Set(['123456']), specCompliance: 'rfc6749' })

fastify.get('/test', (req, res) => {
res.send({ hello: 'world' })
})

test('bearerType starting with capital letter', async (t) => {
t.plan(2)

const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'Bearer 123456'
}
})

t.equal(response.statusCode, 200)
t.same(JSON.parse(response.body), { hello: 'world' })
})

test('bearerType all lowercase', async (t) => {
t.plan(2)

const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'bearer 123456'
}
})

t.equal(response.statusCode, 200)
t.same(JSON.parse(response.body), { hello: 'world' })
})

test('bearerType all uppercase', async (t) => {
t.plan(2)

const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'Bearer 123456'
}
})

t.equal(response.statusCode, 200)
t.same(JSON.parse(response.body), { hello: 'world' })
})

test('invalid key route fails correctly', async (t) => {
t.plan(2)
const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'bearer 987654'
}
})

t.equal(response.statusCode, 401)
t.match(JSON.parse(response.body).error, /invalid authorization header/)
})

test('missing space between bearerType and key fails correctly', async (t) => {
t.plan(2)

const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'bearer123456'
}
})
t.equal(response.statusCode, 401)
t.match(JSON.parse(response.body).error, /invalid authorization header/)
})
85 changes: 85 additions & 0 deletions test/spec-compliance-rfc-6750.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict'

const tap = require('tap')
const test = tap.test
const fastify = require('fastify')()
const plugin = require('../')

fastify.register(plugin, { keys: new Set(['123456']), specCompliance: 'rfc6750' })

fastify.get('/test', (req, res) => {
res.send({ hello: 'world' })
})

test('bearerType starting with capital letter', async (t) => {
t.plan(2)

const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'Bearer 123456'
}
})

t.equal(response.statusCode, 200)
t.same(JSON.parse(response.body), { hello: 'world' })
})

test('bearerType all lowercase', async (t) => {
t.plan(2)

const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'bearer 123456'
}
})

t.equal(response.statusCode, 401)
t.match(JSON.parse(response.body).error, /invalid authorization header/)
})

test('bearerType all uppercase', async (t) => {
t.plan(2)

const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'Bearer 123456'
}
})

t.equal(response.statusCode, 200)
t.same(JSON.parse(response.body), { hello: 'world' })
})

test('invalid key route fails correctly', async (t) => {
t.plan(2)
const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'bearer 987654'
}
})

t.equal(response.statusCode, 401)
t.match(JSON.parse(response.body).error, /invalid authorization header/)
})

test('missing space between bearerType and key fails correctly', async (t) => {
t.plan(2)

const response = await fastify.inject({
method: 'GET',
url: '/test',
headers: {
authorization: 'bearer123456'
}
})
t.equal(response.statusCode, 401)
t.match(JSON.parse(response.body).error, /invalid authorization header/)
})
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare namespace fastifyBearerAuth {
errorResponse?: (err: Error) => { error: string };
contentType?: string;
bearerType?: string;
specCompliance?: 'rfc6749' | 'rfc6750';
addHook?: boolean;
verifyErrorLogLevel?: string;
}
Expand Down
1 change: 1 addition & 0 deletions types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ expectAssignable<{
errorResponse?: (err: Error) => { error: string };
contentType?: string;
bearerType?: string;
specCompliance?: 'rfc6749' | 'rfc6750';
verifyErrorLogLevel? : string;
}>(pluginOptionsAuthPromise)

Expand Down

0 comments on commit bf525cb

Please sign in to comment.