Skip to content

Commit

Permalink
Version 0.2.0 - Add signing functions, end-to-end testing (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
liangyuanruo authored Mar 2, 2020
1 parent 06c90ea commit 274b783
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 21 deletions.
18 changes: 18 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
sudo: required

language: node_js
node_js: '10'
cache: npm

notifications:
email:
recipients:
- [email protected]
on_success: always
on_failure: always

before_script:
- npm install

script:
- npm test
6 changes: 4 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ const webhooks = require('./src/webhooks.js')
* @param {Object} options
* @param {string} [options.mode] If set to 'staging' this will initialise
* the SDK for the FormSG staging environment
* @param {string} [options.webhookSecretKey] Optional base64 secret key for signing webhooks
*/
module.exports = function ({
mode='production'
mode='production',
webhookSecretKey,
} = {}) {
return {
webhooks: webhooks({ mode })
webhooks: webhooks({ mode, webhookSecretKey })
}
}
108 changes: 106 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "@opengovsg/formsg-sdk",
"version": "0.1.1",
"version": "0.2.0",
"description": "Node.js SDK for integrating with FormSG",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jasmine"
},
"keywords": [
"formsg",
Expand All @@ -16,5 +16,8 @@
"dependencies": {
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
},
"devDependencies": {
"jasmine": "^3.5.0"
}
}
14 changes: 14 additions & 0 deletions resource/webhook-keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
staging: {
// staging must never contain secret keys
publicKey: 'rjv41kYqZwcbe3r6ymMEEKQ+Vd+DPuogN+Gzq3lP2Og=',
},
production: {
// production must never contain secret keys
publicKey: '3Tt8VduXsjjd4IrpdCd7BAkdZl/vUCstu9UvTX84FWw=',
},
test: {
publicKey: 'KUY1XT30ar+XreVjsS1w/c3EpDs2oASbF6G3evvaUJM=',
secretKey: '/u+LP57Ib9y5Ytpud56FzuitSC9O6lJ4EOLOFHpsHlYpRjVdPfRqv5et5WOxLXD9zcSkOzagBJsXobd6+9pQkw==',
}
}
7 changes: 0 additions & 7 deletions resource/webhook-public-keys.js

This file was deleted.

5 changes: 5 additions & 0 deletions spec/init.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('FormSG SDK', () => {
it('should be able to initialise without arguments', () => {
expect(() => require('../index')()).not.toThrow()
})
})
11 changes: 11 additions & 0 deletions spec/support/jasmine.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": true
}
39 changes: 39 additions & 0 deletions spec/webhooks.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const webhookSecretKey = require('../resource/webhook-keys').test.secretKey
const webhook = require('../src/webhooks')({
mode: 'test',
webhookSecretKey
})

describe('Webhooks', () => {
const uri = 'https://some-endpoint.com/post'
const submissionId = 'someSubmissionId'
const formId = 'someFormId'

it('should be signing the signature and generating the X-FormSG-Signature header with the correct format', () => {
const epoch = 1583136171649
const signature = webhook.generateSignature({ uri, submissionId, formId, epoch })

expect(signature).toBe('KMirkrGJLPqu+Na+gdZLUxl9ZDgf2PnNGPnSoG1FuTMRUTiQ6o0jB/GTj1XFjn2s9JtsL5GiCmYROpjJhDyxCw==')

// X-FormSG-Signature
const header = webhook.constructHeader({ epoch, submissionId, formId, signature })

expect(header).toBe(`t=1583136171649,s=someSubmissionId,f=someFormId,v1=KMirkrGJLPqu+Na+gdZLUxl9ZDgf2PnNGPnSoG1FuTMRUTiQ6o0jB/GTj1XFjn2s9JtsL5GiCmYROpjJhDyxCw==`)
})

it('should authenticate a signature that was recently generated', () => {
const epoch = Date.now()
const signature = webhook.generateSignature({ uri, submissionId, formId, epoch })
const header = webhook.constructHeader({ epoch, submissionId, formId, signature })

webhook.authenticate(header, uri)
})

it('should reject signatures generated more than 5 minutes ago', () => {
const epoch = Date.now() - 5 * 60 * 1000 - 1
const signature = webhook.generateSignature({ uri, submissionId, formId, epoch })
const header = webhook.constructHeader({ epoch, submissionId, formId, signature })

expect(() => webhook.authenticate(header, uri)).toThrow()
})
})
1 change: 1 addition & 0 deletions src/util/stage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
staging: 'staging',
production: 'production',
test: 'test'
}
57 changes: 49 additions & 8 deletions src/webhooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

const url = require('url')

const { verify } = require('./util/signature')
const { sign, verify } = require('./util/signature')
const { parseSignatureHeader } = require('../src/util/parser')
const webhookPublicKeys = require('../resource/webhook-public-keys')
const WEBHOOK_KEYS = require('../resource/webhook-keys')
const STAGE = require('./util/stage')

/**
Expand Down Expand Up @@ -63,15 +63,56 @@ const authenticate = webhookPublicKey => (header, uri) => {
}

/**
* Provider function that accepts configuration
* Generates a signature based on the URI, submission ID and epoch timestamp.
* @param {String} webhookSecretKey The base64 secret key
* @param {String} uri Full URL of the request
* @param {Object} submissionId Submission Mongo ObjectId saved to the database
* @param {Number} epoch Number of milliseconds since Jan 1, 1970
*/
const generateSignature = (webhookSecretKey) => ({ uri, submissionId, formId, epoch }) => {
const baseString = `${url.parse(uri).href}.${submissionId}.${formId}.${epoch}`
return sign(baseString, webhookSecretKey)
}

/**
* Constructs the `X-FormSG-Signature` header
* @param {string} epoch Epoch timestamp
* @param {string} submissionId Mongo ObjectId
* @param {string} formId Mongo ObjectId
* @param {string} signature A signature generated by the generateSignature() function
*/
function constructHeader ({ epoch, submissionId, formId, signature }) {
return `t=${epoch},s=${submissionId},f=${formId},v1=${signature}`
}

/**
* Retrieves the appropriate webhook public key.
* Defaults to production.
* @param {string} [mode] One of 'staging' | 'production'
*/
function getWebhookPublicKey (mode) {
switch (mode) {
case STAGE.staging:
return WEBHOOK_KEYS.staging.publicKey
case STAGE.test:
return WEBHOOK_KEYS.test.publicKey
default:
return WEBHOOK_KEYS.production.publicKey
}
}

/**
* Provider that accepts configuration
* before returning the webhooks module
*/
module.exports = function ({ mode }) {
const webhookPublicKey = mode === STAGE.staging ?
webhookPublicKeys.staging :
webhookPublicKeys.production
module.exports = function ({ mode, webhookSecretKey }) {
const webhookPublicKey = getWebhookPublicKey(mode)

return {
authenticate: authenticate(webhookPublicKey)
/* Verification functions */
authenticate: authenticate(webhookPublicKey),
/* Signing functions */
generateSignature: webhookSecretKey ? generateSignature(webhookSecretKey) : undefined,
constructHeader: webhookSecretKey ? constructHeader : undefined,
}
}

0 comments on commit 274b783

Please sign in to comment.