Skip to content

Commit

Permalink
Types and enable passing secret in decorator (#32)
Browse files Browse the repository at this point in the history
* types

* updated decorator to accept secret as arg

* update node version
  • Loading branch information
eadmundo authored Mar 8, 2024
1 parent 5c482de commit 1e43d8f
Show file tree
Hide file tree
Showing 7 changed files with 5,969 additions and 3,803 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ jobs:

strategy:
matrix:
node-version: [12.x, 14.x]
node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
Expand All @@ -25,11 +25,11 @@ jobs:
runs-on: ubuntu-latest
needs: validate
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 12.x
node-version: 20.x
registry-url: https://registry.npmjs.org/
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ jobs:

strategy:
matrix:
node-version: [12.x, 14.x]
node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
Expand Down
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.10.0
6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module '@autotelic/fastify-hmac' {

function fastifyHMAC(fastify: any, options: any, done: any): void;

export = fastifyHMAC;
}
28 changes: 20 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const fp = require('fastify-plugin')
const { Unauthorized } = require('http-errors')
const { Unauthorized, BadRequest } = require('http-errors')
const {
constructSignatureString,
extractSignature,
Expand All @@ -21,26 +21,38 @@ function fastifyHMAC (fastify, options, next) {
getAlgorithm,
getDigest,
getSignatureEncoding: () => DEFAULT_ENCODING,
sharedSecret: null,
validateRequests: true,
sharedSecret: null,
verificationError: () => new Unauthorized(DEFAULT_ERROR_MESSAGE),
...options
}

if (pluginOptions.sharedSecret === null) {
next(new Error('missing shared secret'))
if (pluginOptions.sharedSecret === null && pluginOptions.validateRequests) {
next(new Error('Must provide shared secret in plugin options when validateRequests hook enabled'))
}

function validateHMAC (callback) {
function validateHMAC (argSharedSecret = null, callback) {
const request = this
const {
extractSignature,
constructSignatureString,
verificationError
verificationError,
sharedSecret: configSharedSecret
} = pluginOptions

if (configSharedSecret === null && argSharedSecret === null) {
throw BadRequest('No shared secret provided')
}

// argSharedSecret takes precedence over configSharedSecret
const options = {
...pluginOptions,
...(configSharedSecret !== null ? { sharedSecret: configSharedSecret } : {}),
...(argSharedSecret !== null ? { sharedSecret: argSharedSecret } : {})
}

try {
if (extractSignature(request, pluginOptions) === constructSignatureString(request, pluginOptions)) {
if (extractSignature(request, pluginOptions) === constructSignatureString(request, options)) {
return callback ? callback() : true
}
throw Error(DEFAULT_ERROR_MESSAGE)
Expand All @@ -54,7 +66,7 @@ function fastifyHMAC (fastify, options, next) {

if (pluginOptions.validateRequests) {
fastify.addHook('preValidation', function (request, reply, next) {
request.validateHMAC(next)
request.validateHMAC(pluginOptions.sharedSecret, next)
})
}

Expand Down
126 changes: 114 additions & 12 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const defaultOptions = {
}
}

test('fastifyHMAC plugin - request decorator, without hook', async ({ is, same, teardown }) => {
test('request decorator, without hook', async ({ equal, same, teardown }) => {
teardown(async () => fastify.close())
const fastify = require('fastify')()

Expand All @@ -30,12 +30,12 @@ test('fastifyHMAC plugin - request decorator, without hook', async ({ is, same,
url: '/test'
})

is(fastify.hasRequestDecorator('validateHMAC'), true)
is(response.statusCode, 200)
equal(fastify.hasRequestDecorator('validateHMAC'), true)
equal(response.statusCode, 200)
same(response.json(), { hmacVerified: { message: 'Signature verification failed' } })
})

test('fastifyHMAC plugin - hook enabled, request authorized', async ({ is, same, teardown }) => {
test('hook enabled, request authorized', async ({ equal, same, teardown }) => {
teardown(async () => fastify.close())
const fastify = require('fastify')()

Expand All @@ -55,11 +55,11 @@ test('fastifyHMAC plugin - hook enabled, request authorized', async ({ is, same,
}
})

is(response.statusCode, 200)
equal(response.statusCode, 200)
same(response.json(), { hmacVerified: true })
})

test('fastifyHMAC plugin - hook enabled, request unauthorized', async ({ is, teardown }) => {
test('hook enabled, request unauthorized', async ({ equal, teardown }) => {
teardown(async () => fastify.close())
const fastify = require('fastify')()

Expand All @@ -79,16 +79,118 @@ test('fastifyHMAC plugin - hook enabled, request unauthorized', async ({ is, tea
}
})

is(response.statusCode, 401)
is(response.json().error, 'Unauthorized')
is(response.json().message, 'Signature verification failed')
equal(response.statusCode, 401)
equal(response.json().error, 'Unauthorized')
equal(response.json().message, 'Signature verification failed')
})

test('fastifyHMAC plugin - no sharedSecret', async ({ rejects, teardown }) => {
test('fastifyHMAC plugin - no sharedSecret, hook enabled', async ({ rejects, teardown }) => {
teardown(async () => fastify.close())
const fastify = require('fastify')()

fastify.register(fastifyHMAC, {})
const options = {
...defaultOptions
}
delete options.sharedSecret

fastify.register(fastifyHMAC, options)

await rejects(fastify.ready())
})

test('no sharedSecret in options, hook not enabled, secret not passed in decorator', async ({ equal, has, teardown }) => {
teardown(async () => fastify.close())
const fastify = require('fastify')()

const options = {
...defaultOptions,
validateRequests: false
}
delete options.sharedSecret

fastify.register(fastifyHMAC, options)

fastify.get('/test', (request, reply) => {
const validated = request.validateHMAC()
reply.send({ hmacVerified: validated })
})

await fastify.ready()

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

equal(fastify.hasRequestDecorator('validateHMAC'), true)
equal(response.statusCode, 400)
has(response.json(), { message: 'No shared secret provided' })
})

test('no sharedSecret in options, hook not enabled, secret passed in decorator, authorized', async ({ equal, same, teardown }) => {
teardown(async () => fastify.close())
const fastify = require('fastify')()

const options = {
...defaultOptions,
validateRequests: false
}
delete options.sharedSecret

fastify.register(fastifyHMAC, options)

fastify.get('/foo', (request, reply) => {
const validated = request.validateHMAC('test')
reply.send({ hmacVerified: validated })
})

await fastify.ready()

const response = await fastify.inject({
method: 'GET',
url: '/foo',
headers: {
signature: 'keyId="test-key-a", algorithm="hs2019", headers="(request-target)", signature="8zJ7k7Cp4Gqfwfe2SYl6u3uqdUfa6PgZZ7Z0+e5+gV/3/UNUyQowMcbEb9Ni0MfGrDGZ9a04RJrIVcZNa0easA=="'
}
})

equal(fastify.hasRequestDecorator('validateHMAC'), true)
equal(response.statusCode, 200)
same(response.json(), { hmacVerified: true })
})

await rejects(fastify.ready(), Error('missing shared secret'))
test('no sharedSecret in options, hook not enabled, secret passed in decorator, not authorized', async ({ equal, same, teardown }) => {
teardown(async () => fastify.close())
const fastify = require('fastify')()

const options = {
...defaultOptions,
validateRequests: false
}
delete options.sharedSecret

fastify.register(fastifyHMAC, options)

fastify.get('/foo', (request, reply) => {
const validated = request.validateHMAC('test')
reply.send({ hmacVerified: validated })
})

await fastify.ready()

const response = await fastify.inject({
method: 'GET',
url: '/foo',
headers: {
signature: 'keyId="test-key-a", algorithm="hs2019", headers="(request-target)", signature="vivwFqXDfZ/SlUImUIyHuA2kMZAdsNoTQZTAZvjCHFEMBk4Gq7LIWL50Dfr3StO0+UUofTc2bFS9R68JDK+UfQ=="'
}
})

equal(fastify.hasRequestDecorator('validateHMAC'), true)
equal(response.statusCode, 200)
same(response.json(), {
hmacVerified: {
message: 'Signature verification failed'
}
})
})
Loading

0 comments on commit 1e43d8f

Please sign in to comment.