-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3a34c48
Showing
19 changed files
with
2,238 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
github: devinstewart |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
version: 2 | ||
updates: | ||
- package-ecosystem: "npm" | ||
directory: "/" # Location of package manifests | ||
schedule: | ||
interval: "daily" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
name: ci | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
|
||
jobs: | ||
test: | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
os: [ubuntu, windows, macos] | ||
node: ['20', '18'] | ||
|
||
runs-on: ${{ matrix.os }}-latest | ||
name: ${{ matrix.os }} node@${{ matrix.node }} | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-node@v3 | ||
with: | ||
node-version: ${{ matrix.node }} | ||
- name: install | ||
run: npm install | ||
- name: lint | ||
run: npm run lint | ||
- name: test | ||
run: npm test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
**/node_modules | ||
**/package-lock.json | ||
|
||
coverage.* | ||
|
||
**/.DS_Store | ||
**/._* | ||
|
||
**/.vs | ||
**/.vscode | ||
**/.idea | ||
|
||
.dccache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
**v1.0.0 - Initial Release** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Contributing | ||
First of all, thanks for your interest in contributing to this project. | ||
|
||
I am open to contributions, and I will be happy to accept any pull request that meets the needs of this module. You can also open an issue, and I will be happy to help you out, or even add a new feature. | ||
|
||
### Why does this project exist? | ||
While maintining [sns-payload-validator](https://www.npmjs.com/package/sns-payload-validator), I was asked on a few occasions if the module could support Cloudflare's worker platform. Due to Cloudflare not including `crypto` in their worker platform, I was unable to support it. | ||
|
||
So I decided to create a new module that used the WebCrypto API, which is supported by Cloudflare workers. | ||
mostly use it for the Intellisense). | ||
|
||
### Coding Style | ||
I have adapted the coding style guide of [hapijs](https://hapi.dev/policies/styleguide/), as I do work with the fine folks in that project. | ||
|
||
### Dependencies | ||
As a DevSecOps engineer, I love modules without a lot of dependencies. If there is a feature that you would like to add that requires a dependency, please open an issue. We will come to one of three decisions: | ||
- We add the dependency. | ||
- We include the funtionality needed in the module. | ||
- We create a separate module maintained here that includes the functionality needed. | ||
|
||
This module only has a dependency on [lru-cache](https://www.npmjs.com/package/lru-cache) to cache the certificate keys from AWS. This is a perfect example of using an external dependency, as lru-cache is a long proven and well maintained module. There is no reason for us to recreate the wheel of caching. | ||
|
||
By contrast, I have adapted code from [node-forge](https://www.npmjs.com/package/node-forge) to extract the public key from the x509 certificate. I did this because node-forge has a lot of functionality, making it large, and I only needed a small portion of the module to complete this task. So I adapted the code to fit my needs keeping the module small. | ||
|
||
### Conclusion | ||
I hope that this module is useful to you, and I hope that you will contribute to the project. -- Devin Stewart |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
Copyright (c) 2023 Devin Stewart | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# AWS SNS Cloudflare Validator | ||
A package for Cloudflare Workers that validates AWS SNS requests. The request body is parsed and the signature is verified. If the signature is valid, the payload is returned. If the signature is invalid, an error is thrown. | ||
|
||
## Installation | ||
```bash | ||
npm install sns-cloudflare-validator | ||
``` | ||
**Please note:** This package is intended to be used with [Cloudflare Workers](https://workers.cloudflare.com/). To validate AWS SNS requests in Node.js, please use [sns-payload-validator](https://github.com/devinstewart/sns-payload-validator) | ||
|
||
## Usage | ||
```javascript | ||
import { Validator } from 'sns-cloudflare-validator'; | ||
|
||
export default { | ||
async fetch(request) { | ||
try { | ||
const validator = new Validator(); | ||
const payload = await validator.validate(request); | ||
return new Response(JSON.stringify(payload), { | ||
status: 200, | ||
headers: { | ||
'Content-Type': 'application/json' | ||
} | ||
}); | ||
} | ||
catch (error) { | ||
return new Response(error.message, { | ||
status: 400, | ||
headers: { | ||
'Content-Type': 'text/plain' | ||
} | ||
}); | ||
} | ||
} | ||
}; | ||
``` | ||
|
||
## Settings | ||
There are four setting that can be passed to the constructor: | ||
- `autoSubscribe` - A message type of `SubscriptionConfirmation` automatically subscribes the endpoint to the topic after validation, default `true`. | ||
- `autoResubscribe` - A message type of `UnsubscribeConfirmation` automatically resubscribes the endpoint to the topic after validation, default `true`. | ||
- `useCache` - The plugin uses a cache to store the certificate for each topic. This is enabled by default, but can be disabled if you don't want to use the cache. If disabled, the certificate will be fetched from the SNS service for each request. | ||
- `maxCerts` - The maximum number of certificates to store in the cache. This is only used if `useCache` is enabled. The default is `5000`. | ||
|
||
All settings can be passed to the constructor as an object: | ||
```javascript | ||
const validator = new Validator({ | ||
autoSubscribe: false, | ||
autoResubscribe: false, | ||
useCache: false, | ||
maxCerts: 100 | ||
}); | ||
``` | ||
|
||
## Additional Information | ||
The returned payload will have the following properties: | ||
- `Type` - The message type: `Notification`, `SubscriptionConfirmation` or `UnsubscribeConfirmation`. | ||
- `MessageId` - A uuid provided by the SNS service for each message. | ||
- `Token` - The token that must be passed to the `SubscribeURL` to confirm the subscription when the message type is `SubscriptionConfirmation` or `UnsubscribeConfirmation`. | ||
- `TopicArn` - The ARN of the topic the message was sent from. | ||
- `Subject` - The subject of the message when the message type is `Notification`. This is not present if a Subject was not provided when the message was published. | ||
- `Message` - The message body when the message type is `Notification`. | ||
- `Timestamp` - The time the message was sent. | ||
- `SignatureVersion` - The version of the signature algorithm used to sign the message. Defaults to `1`, can also be `2`. | ||
- `Signature` - The signature of the message used to verify the message integrity. | ||
- `SigningCertURL` - The URL of the certificate used to sign the message. | ||
- `SubscribeURL` - The URL used to subscribe the route when the message type is `SubscriptionConfirmation` or `UnsubscribeConfirmation`. | ||
- `UnsubscribeURL` - The URL used to unsubscribe the route when the message type is `Notification`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { describe, beforeAll, afterAll, it, expect } from 'vitest'; | ||
import { unstable_dev } from 'wrangler'; | ||
import { payloads } from './mocks/payloads'; | ||
|
||
describe('test validate()', () => { | ||
|
||
let worker; | ||
beforeAll(async () => { | ||
|
||
worker = await unstable_dev('./__tests__/worker.js', { | ||
|
||
experimental: { disableExperimentalWarning: true } | ||
}); | ||
}); | ||
|
||
afterAll(async () => { | ||
|
||
await worker.stop(); | ||
}); | ||
|
||
it('succussfully validates Notification SignatureVersion 1', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.validNotificationSv1) | ||
}); | ||
const json = await resp.json(); | ||
|
||
expect(json).toStrictEqual(payloads.validNotificationSv1); | ||
expect(resp.status).toStrictEqual(200); | ||
}); | ||
|
||
it('succussfully validates Notification SignatureVersion 2', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.validNotificationSv2) | ||
}); | ||
const json = await resp.json(); | ||
expect(json).toStrictEqual(payloads.validNotificationSv2); | ||
expect(resp.status).toStrictEqual(200); | ||
}); | ||
|
||
it('succussfully validates Notification with Subject', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.validNotificationWithSubject) | ||
}); | ||
const json = await resp.json(); | ||
expect(json).toStrictEqual(payloads.validNotificationWithSubject); | ||
expect(resp.status).toStrictEqual(200); | ||
}); | ||
|
||
it('succussfully validates SubscriptionConfirmation', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.validSubscriptionConfirmation) | ||
}); | ||
const json = await resp.json(); | ||
expect(json).toStrictEqual(payloads.validSubscriptionConfirmation); | ||
expect(resp.status).toStrictEqual(200); | ||
}); | ||
|
||
it('succussfully validates UnsubscribeConfirmation', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.validUnsubscribeConfirmation) | ||
}); | ||
const json = await resp.json(); | ||
expect(json).toStrictEqual(payloads.validUnsubscribeConfirmation); | ||
expect(resp.status).toStrictEqual(200); | ||
}); | ||
|
||
it('fails on invalid JSON', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: 'invalid' | ||
}); | ||
const text = await resp.text(); | ||
expect(text).toStrictEqual('Invalid JSON'); | ||
expect(resp.status).toStrictEqual(400); | ||
}); | ||
|
||
it('fails on unsupported Type', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.invalidType) | ||
}); | ||
const text = await resp.text(); | ||
expect(text).toStrictEqual('Invalid Type'); | ||
expect(resp.status).toStrictEqual(400); | ||
}); | ||
|
||
it('fails on invalid SignatureVersion', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.invalidSignatureVersion) | ||
}); | ||
const text = await resp.text(); | ||
expect(text).toStrictEqual('Invalid SignatureVersion'); | ||
expect(resp.status).toStrictEqual(400); | ||
}); | ||
|
||
it('fails on invalid SigningCertURL', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.invalidSigningCertURL) | ||
}); | ||
const text = await resp.text(); | ||
expect(text).toStrictEqual('Invalid SigningCertURL'); | ||
expect(resp.status).toStrictEqual(400); | ||
}); | ||
|
||
it('fails on invalid Signature', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.invalidSignature) | ||
}); | ||
const text = await resp.text(); | ||
expect(text).toStrictEqual('Invalid Signature'); | ||
expect(resp.status).toStrictEqual(400); | ||
}); | ||
|
||
it('fails on null Signature', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'POST', | ||
body: JSON.stringify(payloads.invalidSignatureNull) | ||
}); | ||
const text = await resp.text(); | ||
expect(text).toStrictEqual('Invalid Signature'); | ||
expect(resp.status).toStrictEqual(400); | ||
}); | ||
|
||
it('fails on a GET', async () => { | ||
|
||
const resp = await worker.fetch('https://127.0.0.1', { | ||
method: 'GET' | ||
}); | ||
const text = await resp.text(); | ||
expect(text).toStrictEqual('Method must be POST'); | ||
expect(resp.status).toStrictEqual(400); | ||
}); | ||
}); |
Oops, something went wrong.