Skip to content

Commit 1bc4a2b

Browse files
panitaxxjsumners
authored andcommitted
add new validation function (#21)
* add new auth function * add new auth function that tests a bearer token against a function * wrote a test for wrong header * comment Promise.resolve block * Update Readme.md Co-Authored-By: Manuel Spigolon <[email protected]> * Update Readme.md Co-Authored-By: Manuel Spigolon <[email protected]> * add more info about auth option add info about what happens if auth is not a function or undefined * remove failSilent. add request parameter * remove failSilent option * made the auth function to strictly return a literal boolean * add request parameter to auth function
1 parent 3995f00 commit 1bc4a2b

File tree

3 files changed

+332
-10
lines changed

3 files changed

+332
-10
lines changed

Readme.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ sent to the client (optional)
4545
* `contentType`: If the content to be sent is anything other than
4646
`application/json`, then the `contentType` property must be set (optional)
4747
* `bearerType`: string specifying the Bearer string (optional)
48+
* `function auth (key, req) {}` : this function will test if `key` is a valid token.
49+
The function must return literal `true` if the key is accepted or literal `false`
50+
if rejected. The function may return also a promise that resolves to one of this
51+
values. If the function returns or resolves to another value, rejects or throws
52+
it will send an HTTP status 500. `req` will contain the request object. If `auth`
53+
is a function, `keys` will be ignored. If `auth` is not a function or undefined,
54+
`keys` will be used.
4855

4956
The default configuration object is:
5057

@@ -55,7 +62,8 @@ The default configuration object is:
5562
bearerType: 'Bearer',
5663
errorResponse: (err) => {
5764
return {error: err.message}
58-
}
65+
},
66+
auth: undefined
5967
}
6068
```
6169

plugin.js

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const compare = require('secure-compare')
66
function factory (options) {
77
const defaultOptions = {
88
keys: [],
9+
auth: undefined,
910
errorResponse (err) {
1011
return { error: err.message }
1112
},
@@ -14,7 +15,7 @@ function factory (options) {
1415
}
1516
const _options = Object.assign({}, defaultOptions, options || {})
1617
if (_options.keys instanceof Set) _options.keys = Array.from(_options.keys)
17-
const { keys, errorResponse, contentType, bearerType } = _options
18+
const { keys, errorResponse, contentType, bearerType, auth } = _options
1819

1920
function bearerAuthHook (fastifyReq, fastifyRes, next) {
2021
const header = fastifyReq.req.headers['authorization']
@@ -27,22 +28,56 @@ function factory (options) {
2728
}
2829

2930
const key = header.substring(bearerType.length).trim()
30-
if (authenticate(keys, key) === undefined) {
31-
const invalidKeyError = Error('invalid authorization header')
32-
fastifyReq.log.error('invalid authorization header: `%s`', header)
33-
if (contentType) fastifyRes.header('content-type', contentType)
34-
fastifyRes.code(401).send(errorResponse(invalidKeyError))
35-
return
31+
let retVal
32+
// check if auth function is defined
33+
if (auth && auth instanceof Function) {
34+
try {
35+
retVal = auth(key, fastifyReq)
36+
// catch any error from the user provided function
37+
} catch (err) {
38+
retVal = Promise.reject(err)
39+
}
40+
} else {
41+
// if auth is not defined use keys
42+
retVal = authenticate(keys, key)
3643
}
3744

38-
next()
45+
const invalidKeyError = Error('invalid authorization header')
46+
47+
// retVal contains the result of the auth function if defined or the
48+
// result of the key comparison.
49+
// retVal is enclosed in a Promise.resolve to allow auth to be a normal
50+
// function or an async funtion. If it returns a non-promise value it
51+
// will be converted to a resolving promise. If it returns a promise it
52+
// will be resolved.
53+
Promise.resolve(retVal).then((val) => {
54+
// if val is not truthy return 401
55+
if (val === false) {
56+
fastifyReq.log.error('invalid authorization header: `%s`', header)
57+
if (contentType) fastifyRes.header('content-type', contentType)
58+
fastifyRes.code(401).send(errorResponse(invalidKeyError))
59+
return
60+
}
61+
if (val === true) {
62+
// if it fails down stream return the proper error
63+
try {
64+
next()
65+
} catch (err) {
66+
next(err)
67+
}
68+
return
69+
}
70+
fastifyRes.code(500).send(errorResponse(new Error('internal server error')))
71+
}).catch((err) => {
72+
fastifyRes.code(500).send(errorResponse(err instanceof Error ? err : Error(String(err))))
73+
})
3974
}
4075

4176
return bearerAuthHook
4277
}
4378

4479
function authenticate (keys, key) {
45-
return keys.find((a) => compare(a, key))
80+
return keys.findIndex((a) => compare(a, key)) !== -1
4681
}
4782

4883
function plugin (fastify, options, next) {

test/hook.test.js

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,282 @@ test('hook accepts correct header with extra padding', (t) => {
146146
t.pass()
147147
})
148148
})
149+
150+
test('hook accepts correct header with auth function (promise)', (t) => {
151+
t.plan(2)
152+
const auth = function (val) {
153+
t.equal(val, key, 'wrong argument')
154+
return Promise.resolve(true)
155+
}
156+
const request = {
157+
log: { error: noop },
158+
req: {
159+
headers: { authorization: `bearer ${key}` }
160+
}
161+
}
162+
const response = {
163+
code: () => response,
164+
send: send
165+
}
166+
167+
function send (body) {
168+
t.fail('should not happen')
169+
}
170+
171+
const hook = plugin({ auth })
172+
hook(request, response, () => {
173+
t.pass()
174+
})
175+
})
176+
177+
test('hook accepts correct header with auth function (non-promise)', (t) => {
178+
t.plan(2)
179+
const auth = function (val) {
180+
t.equal(val, key, 'wrong argument')
181+
return true
182+
}
183+
const request = {
184+
log: { error: noop },
185+
req: {
186+
headers: { authorization: `bearer ${key}` }
187+
}
188+
}
189+
const response = {
190+
code: () => response,
191+
send: send
192+
}
193+
194+
function send (body) {
195+
t.fail('should not happen')
196+
}
197+
198+
const hook = plugin({ auth })
199+
hook(request, response, () => {
200+
t.pass()
201+
})
202+
})
203+
204+
test('hook rejects wrong token with keys', (t) => {
205+
t.plan(2)
206+
207+
const request = {
208+
log: { error: noop },
209+
req: {
210+
headers: { authorization: `bearer abcdedfg` }
211+
}
212+
}
213+
const response = {
214+
code: () => response,
215+
send: send
216+
}
217+
218+
function send (body) {
219+
t.ok(body.error)
220+
t.match(body.error, /invalid authorization header/)
221+
}
222+
223+
const hook = plugin(keys)
224+
hook(request, response, () => {
225+
t.fail('should not accept')
226+
})
227+
})
228+
229+
test('hook rejects wrong token with auth function', (t) => {
230+
t.plan(5)
231+
232+
const request = {
233+
log: { error: noop },
234+
req: {
235+
headers: { authorization: `bearer abcdefg` }
236+
}
237+
}
238+
239+
const auth = function (val, req) {
240+
t.equal(req, request)
241+
t.equal(val, 'abcdefg', 'wrong argument')
242+
return false
243+
}
244+
245+
const response = {
246+
code: (status) => {
247+
t.equal(401, status)
248+
return response
249+
},
250+
send: send
251+
}
252+
253+
function send (body) {
254+
t.ok(body.error)
255+
t.match(body.error, /invalid authorization header/)
256+
}
257+
258+
const hook = plugin({ auth })
259+
hook(request, response, () => {
260+
t.fail('should not accept')
261+
})
262+
})
263+
264+
test('hook rejects wrong token with function (resolved promise)', (t) => {
265+
t.plan(4)
266+
267+
const auth = function (val) {
268+
t.equal(val, 'abcdefg', 'wrong argument')
269+
return Promise.resolve(false)
270+
}
271+
272+
const request = {
273+
log: { error: noop },
274+
req: {
275+
headers: { authorization: `bearer abcdefg` }
276+
}
277+
}
278+
const response = {
279+
code: (status) => {
280+
t.equal(401, status)
281+
return response
282+
},
283+
send: send
284+
}
285+
286+
function send (body) {
287+
t.ok(body.error)
288+
t.match(body.error, /invalid authorization header/)
289+
}
290+
291+
const hook = plugin({ auth })
292+
hook(request, response, () => {
293+
t.fail('should not accept')
294+
})
295+
})
296+
297+
test('hook rejects with 500 when functions fails', (t) => {
298+
t.plan(4)
299+
300+
const auth = function (val) {
301+
t.equal(val, 'abcdefg', 'wrong argument')
302+
throw Error('failing')
303+
}
304+
305+
const request = {
306+
log: { error: noop },
307+
req: {
308+
headers: { authorization: `bearer abcdefg` }
309+
}
310+
}
311+
const response = {
312+
code: (status) => {
313+
t.equal(500, status)
314+
return response
315+
},
316+
send: send
317+
}
318+
319+
function send (body) {
320+
t.ok(body.error)
321+
t.match(body.error, /failing/)
322+
}
323+
324+
const hook = plugin({ auth })
325+
hook(request, response, () => {
326+
t.fail('should not accept')
327+
})
328+
})
329+
330+
test('hook rejects with 500 when promise rejects', (t) => {
331+
t.plan(4)
332+
333+
const auth = function (val) {
334+
t.equal(val, 'abcdefg', 'wrong argument')
335+
return Promise.reject(Error('failing'))
336+
}
337+
338+
const request = {
339+
log: { error: noop },
340+
req: {
341+
headers: { authorization: `bearer abcdefg` }
342+
}
343+
}
344+
const response = {
345+
code: (status) => {
346+
t.equal(500, status)
347+
return response
348+
},
349+
send: send
350+
}
351+
352+
function send (body) {
353+
t.ok(body.error)
354+
t.match(body.error, /failing/)
355+
}
356+
357+
const hook = plugin({ auth })
358+
hook(request, response, () => {
359+
t.fail('should not accept')
360+
})
361+
})
362+
363+
test('hook rejects with 500 when functions returns non-boolean', (t) => {
364+
t.plan(4)
365+
366+
const auth = function (val) {
367+
t.equal(val, 'abcdefg', 'wrong argument')
368+
return 'foobar'
369+
}
370+
371+
const request = {
372+
log: { error: noop },
373+
req: {
374+
headers: { authorization: `bearer abcdefg` }
375+
}
376+
}
377+
const response = {
378+
code: (status) => {
379+
t.equal(500, status)
380+
return response
381+
},
382+
send: send
383+
}
384+
385+
function send (body) {
386+
t.ok(body.error)
387+
t.match(body.error, /internal server error/)
388+
}
389+
390+
const hook = plugin({ auth })
391+
hook(request, response, () => {
392+
t.fail('should not accept')
393+
})
394+
})
395+
396+
test('hook rejects with 500 when promise resolves to non-boolean', (t) => {
397+
t.plan(4)
398+
399+
const auth = function (val) {
400+
t.equal(val, 'abcdefg', 'wrong argument')
401+
return Promise.resolve('abcde')
402+
}
403+
404+
const request = {
405+
log: { error: noop },
406+
req: {
407+
headers: { authorization: `bearer abcdefg` }
408+
}
409+
}
410+
const response = {
411+
code: (status) => {
412+
t.equal(500, status)
413+
return response
414+
},
415+
send: send
416+
}
417+
418+
function send (body) {
419+
t.ok(body.error)
420+
t.match(body.error, /internal server error/)
421+
}
422+
423+
const hook = plugin({ auth })
424+
hook(request, response, () => {
425+
t.fail('should not accept')
426+
})
427+
})

0 commit comments

Comments
 (0)