Skip to content

Commit

Permalink
Feature/add JWS and request Variables (#122)
Browse files Browse the repository at this point in the history
* Added atob function to javascript scripting engine

* Added requestVaribles param in JS scripting and JWS related functions

* Fixed failed unit tests

* Added unit tests

* Bumped up the version and resolved audits

* Added validateCallbackProtectedHeaders in javascript scripting

* Added documentation
  • Loading branch information
vijayg10 authored Dec 18, 2020
1 parent 05b9878 commit 251f9c9
Show file tree
Hide file tree
Showing 10 changed files with 16,425 additions and 16,082 deletions.
32,034 changes: 16,017 additions & 16,017 deletions audit-resolve.json

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions documents/User-Guide-Mojaloop-Testing-Toolkit.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,10 @@ You can write scripts in two formats.
Functions supported:
- _websocket.connect_ - To connect to a websocket URL and listen for messsages
- _websocket.getMessage_ - To get the message arrived. This function can also wait for the message some time. The session will be disconnected automatically after returning the message
- _websocket.disconnect_ - To disconnect a particular session
- _websocket.disconnectAll_ - To disconnect all the sessions
- _**websocket.connect**_ - To connect to a websocket URL and listen for messsages
- _**websocket.getMessage**_ - To get the message arrived. This function can also wait for the message some time. The session will be disconnected automatically after returning the message
- _**websocket.disconnect**_ - To disconnect a particular session
- _**websocket.disconnectAll**_ - To disconnect all the sessions
This will be used to assert on the payee side data from the sdk-scheme-adapter in tests cases. You may need to enable websocket capabilities in the sdk-scheme-adapter.
Expand All @@ -360,6 +360,16 @@ You can write scripts in two formats.
```
environment.payeeRequest.headers['content-type']
```
- **custom.jws** - library
With custom.jws library, you can sign and validate an FSPIOP request using JWS
Functions supported:
- _**custom.jws.signRequest**(<PRIVATE_KEY>)_ - To sign the outgoing request using the private key
- _**custom.jws.validateCallback**(<callback.headers>, <callback.body>, <PUBLIC_CERTIFICATE>)_ - To validate the incoming callback using public certificate. This will validate protected headers too.
- _**custom.jws.validateCallbackProtectedHeaders**(<callback.headers>)_ - To validate only protected headers in the FSPIOP-Signature header
![Sample Pre Request and Post Request Scripts](/assets/images/test-case-editor-scripts.png)
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ml-testing-toolkit",
"description": "Testing Toolkit for Mojaloop implementations",
"version": "11.7.3",
"version": "11.7.4",
"license": "Apache-2.0",
"author": "Vijaya Kumar Guthi, ModusBox Inc.",
"contributors": [
Expand Down
123 changes: 75 additions & 48 deletions src/lib/jws/JwsSigning.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
const Config = require('../config')
const { Jws } = require('@mojaloop/sdk-standard-components')
const ConnectionProvider = require('../configuration-providers/mb-connection-manager')
const atob = require('atob')

const validate = async (req) => {
const userConfig = await Config.getUserConfig()
Expand All @@ -37,26 +38,45 @@ const validate = async (req) => {
if (req.method === 'put' && req.path.startsWith('/parties/') && !userConfig.VALIDATE_INBOUND_PUT_PARTIES_JWS) {
return false
}
const reqOpts = {
method: req.method,
headers: req.headers,
body: req.payload,
resolveWithFullResponse: true,
simple: false
}
const keys = {}
keys[req.headers['fspiop-source']] = await ConnectionProvider.getUserDfspJWSCerts(req.headers['fspiop-source'])
const jwsValidator = new Jws.validator({ // eslint-disable-line
validationKeys: keys
})

try {
jwsValidator.validate(reqOpts)
return true
} catch (err) {
throw new Error(err.toString())
}
const certificate = await ConnectionProvider.getUserDfspJWSCerts(req.headers['fspiop-source'])
return validateWithCert(req.headers, req.payload, certificate)
}
}

const validateWithCert = (headers, body, certificate) => {
const reqOpts = {
headers,
body,
resolveWithFullResponse: true,
simple: false
}
const keys = {}
keys[headers['fspiop-source']] = certificate

const jwsValidator = new Jws.validator({ // eslint-disable-line
validationKeys: keys
})

try {
jwsValidator.validate(reqOpts)
return true
} catch (err) {
throw new Error(err.toString())
}
}

const validateProtectedHeaders = (headers) => {
if (!headers['fspiop-signature']) {
throw new Error('fspiop-signature is missing in the headers')
}
const { protectedHeader } = JSON.parse(headers['fspiop-signature'])
const decodedProtectedHeader = JSON.parse(atob(protectedHeader))
const jwsValidator = new Jws.validator({ // eslint-disable-line
validationKeys: []
})
jwsValidator._validateProtectedHeader(headers, decodedProtectedHeader)
return true
}

const sign = async (req) => {
Expand All @@ -70,40 +90,47 @@ const sign = async (req) => {
return false
}
const jwsSigningKey = await ConnectionProvider.getTestingToolkitDfspJWSPrivateKey()
const jwsSigner = new Jws.signer({ // eslint-disable-line
signingKey: jwsSigningKey
})
const reqOpts = {
method: req.method,
uri: req.url,
headers: req.headers,
body: JSON.stringify(req.data),
agent: 'testingtoolkit',
resolveWithFullResponse: true,
simple: false
}
if (reqOpts.headers['FSPIOP-Source']) {
reqOpts.headers['fspiop-source'] = reqOpts.headers['FSPIOP-Source']
delete reqOpts.headers['FSPIOP-Source']
}
if (reqOpts.headers['FSPIOP-Destination']) {
reqOpts.headers['fspiop-destination'] = reqOpts.headers['FSPIOP-Destination']
delete reqOpts.headers['FSPIOP-Destination']
}
delete reqOpts.headers['FSPIOP-Signature']
delete reqOpts.headers['FSPIOP-URI']
delete reqOpts.headers['FSPIOP-HTTP-Method']
return signWithKey(req, jwsSigningKey)
}
}

try {
jwsSigner.sign(reqOpts)
return true
} catch (err) {
throw new Error(err.toString())
}
const signWithKey = (req, jwsSigningKey) => {
const jwsSigner = new Jws.signer({ // eslint-disable-line
signingKey: jwsSigningKey
})
const reqOpts = {
method: req.method,
uri: req.url,
headers: req.headers,
body: JSON.stringify(req.data),
agent: 'testingtoolkit',
resolveWithFullResponse: true,
simple: false
}
if (reqOpts.headers['FSPIOP-Source']) {
reqOpts.headers['fspiop-source'] = reqOpts.headers['FSPIOP-Source']
delete reqOpts.headers['FSPIOP-Source']
}
if (reqOpts.headers['FSPIOP-Destination']) {
reqOpts.headers['fspiop-destination'] = reqOpts.headers['FSPIOP-Destination']
delete reqOpts.headers['FSPIOP-Destination']
}
delete reqOpts.headers['FSPIOP-Signature']
delete reqOpts.headers['FSPIOP-URI']
delete reqOpts.headers['FSPIOP-HTTP-Method']

try {
jwsSigner.sign(reqOpts)
return true
} catch (err) {
throw new Error(err.toString())
}
}

module.exports = {
validate,
sign
validateWithCert,
validateProtectedHeaders,
sign,
signWithKey
}
38 changes: 38 additions & 0 deletions src/lib/scripting-engines/vm-javascript-sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@

const Sandbox = require('vm')
const axios = require('axios').default
const atob = require('atob')
const WebSocketClientManager = require('../webSocketClient/WebSocketClientManager').WebSocketClientManager
const JwsSigning = require('../jws/JwsSigning')

const consoleWrapperFn = (consoleOutObj) => {
return {
Expand All @@ -34,6 +36,32 @@ const consoleWrapperFn = (consoleOutObj) => {
}
}

const customWrapperFn = (requestVariables) => {
return {
jws: {
signRequest: function (key) {
requestVariables.TTK_JWS_SIGN_KEY = key
},
validateCallback: function (headers, body, certificate) {
try {
JwsSigning.validateWithCert(headers, body, certificate)
return 'VALID'
} catch (err) {
return err.message
}
},
validateCallbackProtectedHeaders: function (headers) {
try {
JwsSigning.validateProtectedHeaders(headers)
return 'VALID'
} catch (err) {
return err.message
}
}
}
}
}

const clearConsole = (consoleOutObj) => {
consoleOutObj.stdOut = []
}
Expand All @@ -51,19 +79,25 @@ const generateContextObj = async (environmentObj = {}) => {
const consoleOutObj = {
stdOut: []
}
const requestVariables = {}
const consoleFn = consoleWrapperFn(consoleOutObj)
const customFn = customWrapperFn(requestVariables)
const websocket = new WebSocketClientManager(consoleFn)

const contextObj = {
ctx: {
dispose: () => {}
},
environment: { ...environmentObj },
requestVariables,
axios,
atob,
consoleWrapperFn,
customWrapperFn,
executeAsync,
websocket,
console: consoleFn,
custom: customFn,
consoleOutObj
}
return contextObj
Expand All @@ -73,6 +107,10 @@ const executeAsync = async (script, data, contextObj) => {
const fullScript = preScript + script.join('\n') + postScript
let consoleLog = []

if (data.context.request) {
contextObj.request = data.context.request
}

if (data.context.response) {
contextObj.response = data.context.response
}
Expand Down
37 changes: 27 additions & 10 deletions src/lib/test-outbound/outbound-initiator.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ const processTestCase = async (testCase, traceID, inputValues, variableData, dfs
if (request.delay) {
await new Promise(resolve => setTimeout(resolve, request.delay))
}
const resp = await sendRequest(convertedRequest.url, convertedRequest.method, convertedRequest.path, convertedRequest.queryParams, convertedRequest.headers, convertedRequest.body, successCallbackUrl, errorCallbackUrl, convertedRequest.ignoreCallbacks, dfspId)
const resp = await sendRequest(convertedRequest.url, convertedRequest.method, convertedRequest.path, convertedRequest.queryParams, convertedRequest.headers, convertedRequest.body, successCallbackUrl, errorCallbackUrl, convertedRequest.ignoreCallbacks, dfspId, contextObj)
await setResponse(convertedRequest, resp, variableData, request, 'SUCCESS', tracing, testCase, scriptsExecution, contextObj, globalConfig)
} catch (err) {
let resp
Expand Down Expand Up @@ -302,7 +302,7 @@ const setResponse = async (convertedRequest, resp, variableData, request, status

let testResult = null
if (globalConfig.testsExecution) {
testResult = await handleTests(convertedRequest, resp.syncResponse, resp.callback, variableData.environment, backgroundData)
testResult = await handleTests(convertedRequest, resp.syncResponse, resp.callback, variableData.environment, backgroundData, contextObj.requestVariables)
}
request.appended = {
status: status,
Expand Down Expand Up @@ -338,7 +338,15 @@ const executePreRequestScript = async (convertedRequest, scriptsExecution, conte
if (convertedRequest.scriptingEngine && convertedRequest.scriptingEngine === 'javascript') {
context = javascriptContext
}
scriptsExecution.preRequest = await context.executeAsync(convertedRequest.scripts.preRequest.exec, { context: { request: convertedRequest }, id: uuid.v4() }, contextObj)
const requestToPass = {
url: convertedRequest.url,
method: convertedRequest.method,
path: convertedRequest.path,
queryParams: convertedRequest.queryParams,
headers: convertedRequest.headers,
body: convertedRequest.body
}
scriptsExecution.preRequest = await context.executeAsync(convertedRequest.scripts.preRequest.exec, { context: { request: requestToPass }, id: uuid.v4() }, contextObj)
variableData.environment = scriptsExecution.preRequest.environment
}
}
Expand Down Expand Up @@ -380,7 +388,7 @@ const executePostRequestScript = async (convertedRequest, resp, scriptsExecution
}
}

const handleTests = async (request, response = null, callback = null, environment = {}, backgroundData = {}) => {
const handleTests = async (request, response = null, callback = null, environment = {}, backgroundData = {}, requestVariables = {}) => {
try {
const results = {}
let passedCount = 0
Expand Down Expand Up @@ -420,7 +428,7 @@ const getUrlPrefix = (baseUrl) => {
return returnUrl
}

const sendRequest = (baseUrl, method, path, queryParams, headers, body, successCallbackUrl, errorCallbackUrl, ignoreCallbacks, dfspId) => {
const sendRequest = (baseUrl, method, path, queryParams, headers, body, successCallbackUrl, errorCallbackUrl, ignoreCallbacks, dfspId, contextObj = {}) => {
return new Promise((resolve, reject) => {
(async () => {
const httpsProps = {}
Expand Down Expand Up @@ -474,11 +482,20 @@ const sendRequest = (baseUrl, method, path, queryParams, headers, body, successC
},
...httpsProps
}
try {
await JwsSigning.sign(reqOpts)
customLogger.logOutboundRequest('info', 'JWS signed', { uniqueId, request: reqOpts })
} catch (err) {
customLogger.logMessage('error', err.message, { additionalData: err })

if (contextObj.requestVariables && contextObj.requestVariables.TTK_JWS_SIGN_KEY) {
try {
await JwsSigning.signWithKey(reqOpts, contextObj.requestVariables.TTK_JWS_SIGN_KEY)
} catch (err) {
customLogger.logMessage('error', err.message, { additionalData: err })
}
} else {
try {
await JwsSigning.sign(reqOpts)
customLogger.logOutboundRequest('info', 'JWS signed', { uniqueId, request: reqOpts })
} catch (err) {
customLogger.logMessage('error', err.message, { additionalData: err })
}
}

var syncResponse = {}
Expand Down
14 changes: 14 additions & 0 deletions test/unit/lib/jws/JwsSigning.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@ describe('JwsSigning', () => {
// Validate with JWS
await expect(JwsSigning.validate(reqOpts)).resolves.toBeDefined();
})
it('Validate only protected headers', () => {
// Validate with JWS
expect(JwsSigning.validateProtectedHeaders(reqOpts.headers)).toEqual(true)
})
it('Validate only protected headers negative', () => {
// Validate with JWS
let result
try {
result = JwsSigning.validateProtectedHeaders({ ...reqOpts.headers, 'fspiop-signature': null })
} catch(err) {
result = false
}
expect(result).toEqual(false)
})
})

describe('Validation request should fail when', () => {
Expand Down
Loading

0 comments on commit 251f9c9

Please sign in to comment.