node-my-info-sg
Advanced tools
Comparing version 1.0.5 to 1.1.0
const _ = require('lodash'); | ||
const Promise = require('bluebird'); | ||
const crypto = require("crypto"); | ||
const querystring = require('querystring'); | ||
const restClient = require('superagent-bluebird-promise'); | ||
const crypto = require('crypto'); | ||
const superagent = require('superagent'); | ||
const securityHelper = require('./security'); | ||
@@ -10,5 +8,5 @@ | ||
constructor(options) { | ||
this._authApiUrl = options.baseUrl + '/com/v3/authorise'; | ||
this._personApiUrl = options.baseUrl + '/com/v3/person'; | ||
this._tokenApiUrl = options.baseUrl + '/com/v3/token'; | ||
this._authApiUrl = `${options.baseUrl}/com/v3/authorise`; | ||
this._personApiUrl = `${options.baseUrl}/com/v3/person`; | ||
this._tokenApiUrl = `${options.baseUrl}/com/v3/token`; | ||
this._authLevel = options.authLevel; | ||
@@ -21,232 +19,131 @@ this._clientId = options.clientId; | ||
} | ||
getAuthoriseUrl(purpose, attributes) { | ||
const state = crypto.randomBytes(16).toString("hex"); | ||
var authoriseUrl = this._authApiUrl + "?client_id=" + this._clientId | ||
+ "&attributes="+ attributes.join(',') | ||
+ "&purpose=" + purpose | ||
+ "&state=" + state | ||
+ "&redirect_uri=" + this._redirectUrl; | ||
return { authoriseUrl, state } ; | ||
const state = crypto.randomBytes(16).toString('hex'); | ||
const authoriseUrl = `${this._authApiUrl}\ | ||
?client_id=${this._clientId}\ | ||
&attributes=${attributes.join(',')}\ | ||
&purpose=${purpose}\ | ||
&state=${state}\ | ||
&redirect_uri=${this._redirectUrl}`; | ||
return { authoriseUrl, state }; | ||
} | ||
getToken(code) { | ||
var self = this; | ||
return new Promise(function (resolve, reject) { | ||
var _authLevel = self._authLevel; | ||
var _clientId = self._clientId; | ||
var _clientSecret = self._clientSecret; | ||
var _privateKeyPath = self._privateKeyPath; | ||
var _redirectUrl = self._redirectUrl; | ||
var _tokenApiUrl = self._tokenApiUrl; | ||
var cacheCtl = "no-cache"; | ||
var contentType = "application/x-www-form-urlencoded"; | ||
var method = "POST"; | ||
// assemble params for Token API | ||
var strParams = "grant_type=authorization_code" + | ||
"&code=" + code + | ||
"&redirect_uri=" + _redirectUrl + | ||
"&client_id=" + _clientId + | ||
"&client_secret=" + _clientSecret; | ||
var params = querystring.parse(strParams); | ||
async getToken(code) { | ||
const { | ||
_authLevel, _clientId, _clientSecret, _privateKeyPath, _redirectUrl, _tokenApiUrl, | ||
} = this; | ||
if (_authLevel !== 'L0' && _authLevel !== 'L2') throw new Error('UNKNOWN AUTH LEVEL'); | ||
// assemble headers for Token API | ||
var strHeaders = "Content-Type=" + contentType + "&Cache-Control=" + cacheCtl; | ||
var headers = querystring.parse(strHeaders); | ||
const params = { | ||
grant_type: 'authorization_code', | ||
redirect_uri: _redirectUrl, | ||
client_id: _clientId, | ||
client_secret: _clientSecret, | ||
code, | ||
}; | ||
// Add Authorisation headers for connecting to API Gateway | ||
var authHeaders = null; | ||
if (_authLevel == "L0") { | ||
// No headers | ||
} else if (_authLevel == "L2") { | ||
authHeaders = securityHelper.generateAuthorizationHeader( | ||
_tokenApiUrl, | ||
params, | ||
method, | ||
contentType, | ||
_authLevel, | ||
_clientId, | ||
_privateKeyPath, | ||
_clientSecret | ||
); | ||
} else { | ||
throw new Error("Unknown Auth Level"); | ||
} | ||
const headers = { | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
'Cache-Control': 'no-cache', | ||
}; | ||
if (!_.isEmpty(authHeaders)) { | ||
_.set(headers, "Authorization", authHeaders); | ||
} | ||
if (_authLevel === 'L2') { | ||
const authHeaders = securityHelper.generateAuthorizationHeader( | ||
_tokenApiUrl, | ||
params, | ||
'POST', | ||
'application/x-www-form-urlencoded', | ||
_authLevel, | ||
_clientId, | ||
_privateKeyPath, | ||
_clientSecret, | ||
); | ||
_.set(headers, 'Authorization', authHeaders); | ||
} | ||
var request = restClient.post(_tokenApiUrl); | ||
const response = await superagent.post(_tokenApiUrl) | ||
.set(headers) | ||
.send(params) | ||
.buffer(true); | ||
// Set headers | ||
if (!_.isUndefined(headers) && !_.isEmpty(headers)) | ||
request.set(headers); | ||
return { accessToken: _.get(response, 'body.access_token') }; | ||
} | ||
// Set Params | ||
if (!_.isUndefined(params) && !_.isEmpty(params)) | ||
request.send(params); | ||
async getPerson(accessToken, attributes) { | ||
const { | ||
_authLevel, _privateKeyPath, _publicCertPath, | ||
} = this; | ||
request | ||
.buffer(true) | ||
.end(function(callErr, callRes) { | ||
if (callErr) { | ||
// ERROR | ||
reject(callErr); | ||
} else { | ||
// SUCCESSFUL | ||
var data = { | ||
body: callRes.body, | ||
text: callRes.text | ||
}; | ||
if (_authLevel !== 'L0' && _authLevel !== 'L2') throw new Error('UNKNOWN AUTH LEVEL'); | ||
var accessToken = data.body.access_token; | ||
if (accessToken == undefined || accessToken == null) { | ||
reject(new Error('ACCESS TOKEN NOT FOUND')); | ||
} | ||
// validate and decode token to get UINFIN | ||
const decoded = securityHelper.verifyJWS(accessToken, _publicCertPath); | ||
if (!decoded) throw new Error('INVALID TOKEN'); | ||
resolve({ accessToken }); | ||
} | ||
}); | ||
}); | ||
}; | ||
getPerson(accessToken, attributes) { | ||
var self = this; | ||
return new Promise(function(resolve, reject) { | ||
var _authLevel = self._authLevel; | ||
var _privateKeyPath = self._privateKeyPath; | ||
var _publicCertPath = self._publicCertPath; | ||
const uinfin = decoded.sub; | ||
if (!uinfin) throw new Error('UINFIN NOT FOUND'); | ||
// validate and decode token to get UINFIN | ||
var decoded = securityHelper.verifyJWS(accessToken, _publicCertPath); | ||
if (decoded == undefined || decoded == null) { | ||
reject(new Error("INVALID TOKEN")); | ||
} | ||
const response = await this._createPersonRequest(uinfin, accessToken, attributes); | ||
const responseText = response.text; | ||
if (_authLevel === 'L0') { | ||
const personData = JSON.parse(responseText); | ||
return { person: personData }; | ||
} | ||
var uinfin = decoded.sub; | ||
if (uinfin == undefined || uinfin == null) { | ||
reject(new Error("UINFIN NOT FOUND")); | ||
} | ||
// _authLevel === 'L2' | ||
const [header, encryptedKey, iv, ciphertext, tag] = responseText.split('.'); | ||
// **** CALL PERSON API **** | ||
var request = self._createPersonRequest(uinfin, accessToken, attributes.join(',')); | ||
const personDataJWS = await securityHelper.decryptJWE(header, encryptedKey, iv, ciphertext, tag, _privateKeyPath); | ||
if (!personDataJWS) throw new Error('INVALID DATA OR SIGNATURE FOR PERSON DATA'); | ||
// Invoke asynchronous call | ||
request | ||
.buffer(true) | ||
.end(function(callErr, callRes) { | ||
if (callErr) { | ||
reject(callErr); | ||
} else { | ||
// SUCCESSFUL | ||
var data = { | ||
body: callRes.body, | ||
text: callRes.text | ||
}; | ||
var personData = data.text; | ||
if (personData == undefined || personData == null) { | ||
reject(new Error("PERSON DATA NOT FOUND")); | ||
} else { | ||
const decodedPersonData = securityHelper.verifyJWS(personDataJWS, _publicCertPath); | ||
if (!decodedPersonData) throw new Error('INVALID DATA OR SIGNATURE FOR PERSON DATA'); | ||
if (_authLevel == "L0") { | ||
personData = JSON.parse(personData); | ||
// personData = securityHelper.verifyJWS(personData, _publicCertPath); | ||
return { person: decodedPersonData }; | ||
} | ||
if (personData == undefined || personData == null) { | ||
reject(new Error("INVALID DATA OR SIGNATURE FOR PERSON DATA")); | ||
} | ||
_createPersonRequest(uinfin, validToken, attributes) { | ||
const { | ||
_authLevel, _clientId, _clientSecret, _personApiUrl, _privateKeyPath, | ||
} = this; | ||
// successful. return data back to frontend | ||
resolve({ person: personData }); | ||
const url = `${_personApiUrl}/${uinfin}/`; | ||
} | ||
else if(_authLevel == "L2"){ | ||
var jweParts = personData.split("."); // header.encryptedKey.iv.ciphertext.tag | ||
securityHelper.decryptJWE(jweParts[0], jweParts[1], jweParts[2], jweParts[3], jweParts[4], _privateKeyPath) | ||
.then(personDataJWS => { | ||
if (personDataJWS == undefined || personDataJWS == null) { | ||
reject(new Error("INVALID DATA OR SIGNATURE FOR PERSON DATA")); | ||
} | ||
var decodedPersonData = securityHelper.verifyJWS(personDataJWS, _publicCertPath); | ||
if (decodedPersonData == undefined || decodedPersonData == null) { | ||
reject(new Error("INVALID DATA OR SIGNATURE FOR PERSON DATA")); | ||
} | ||
// successful. return data back to frontend | ||
resolve({ person: decodedPersonData }); | ||
}) | ||
} | ||
else { | ||
reject(new Error("Unknown Auth Level")); | ||
} | ||
} // end else | ||
} | ||
}); //end asynchronous call | ||
}); | ||
} | ||
_createPersonRequest(uinfin, validToken, attributes) { | ||
var _attributes = attributes; | ||
var _authLevel = this._authLevel; | ||
var _clientId = this._clientId; | ||
var _clientSecret = this._clientSecret; | ||
var _personApiUrl = this._personApiUrl; | ||
var _privateKeyPath = this._privateKeyPath; | ||
var url = _personApiUrl + "/" + uinfin + "/"; | ||
var cacheCtl = "no-cache"; | ||
var method = "GET"; | ||
// assemble params for Person API | ||
var strParams = "client_id=" + _clientId + | ||
"&attributes=" + _attributes; | ||
const params = { | ||
client_id: _clientId, | ||
attributes: attributes.join(','), | ||
}; | ||
var params = querystring.parse(strParams); | ||
// assemble headers for Person API | ||
var strHeaders = "Cache-Control=" + cacheCtl; | ||
var headers = querystring.parse(strHeaders); | ||
const headers = { 'Cache-Control': 'no-cache' }; | ||
// Add Authorisation headers for connecting to API Gateway | ||
var authHeaders = securityHelper.generateAuthorizationHeader( | ||
const authHeaders = securityHelper.generateAuthorizationHeader( | ||
url, | ||
params, | ||
method, | ||
"", // no content type needed for GET | ||
'GET', | ||
'', // no content type needed for GET | ||
_authLevel, | ||
_clientId, | ||
_privateKeyPath, | ||
_clientSecret | ||
_clientSecret, | ||
); | ||
// NOTE: include access token in Authorization header as "Bearer " (with space behind) | ||
// NOTE: include access token in Authorization header as 'Bearer ' (with space behind) | ||
if (!_.isEmpty(authHeaders)) { | ||
_.set(headers, "Authorization", authHeaders + ",Bearer " + validToken); | ||
_.set(headers, 'Authorization', `${authHeaders},Bearer ${validToken}`); | ||
} else { | ||
_.set(headers, "Authorization", "Bearer " + validToken); | ||
_.set(headers, 'Authorization', `Bearer ${validToken}`); | ||
} | ||
// invoke person API | ||
var request = restClient.get(url); | ||
// Set headers | ||
if (!_.isUndefined(headers) && !_.isEmpty(headers)) | ||
request.set(headers); | ||
// Set Params | ||
if (!_.isUndefined(params) && !_.isEmpty(params)) | ||
request.query(params); | ||
return request; | ||
return superagent.get(url) | ||
.set(headers) | ||
.query(params) | ||
.buffer(true); | ||
} | ||
@@ -253,0 +150,0 @@ } |
@@ -6,3 +6,2 @@ const _ = require('lodash'); | ||
const jwt = require('jsonwebtoken'); | ||
const nonce = require('nonce')(); | ||
const qs = require('querystring'); | ||
@@ -14,5 +13,3 @@ | ||
function sortJSON(json) { | ||
if (_.isNil(json)) { | ||
return json; | ||
} | ||
if (_.isNil(json)) return json; | ||
@@ -23,4 +20,4 @@ const newJSON = {}; | ||
for (key in keys) { | ||
newJSON[keys[key]] = json[keys[key]]; | ||
for (let i = 0; i < keys.length; i += 1) { | ||
newJSON[keys[i]] = json[keys[i]]; | ||
} | ||
@@ -42,6 +39,7 @@ | ||
function generateSHA256withRSAHeader(url, _params, method, strContentType, appId, keyCertContent, keyCertPassphrase) { | ||
const nonceValue = nonce(); | ||
const nonceValue = crypto.randomBytes(16).toString('hex'); | ||
const timestamp = (new Date()).getTime(); | ||
let params = _params; | ||
// A) Construct the Authorisation Token | ||
@@ -60,2 +58,3 @@ const defaultApexHeaders = { | ||
// B) Forming the Signature Base String | ||
@@ -72,3 +71,3 @@ | ||
// iii) concatenate request elements | ||
const baseString = method.toUpperCase() + "&" + url + "&" + baseParamsStr; | ||
const baseString = `${method.toUpperCase()}&${url}&${baseParamsStr}`; | ||
@@ -81,3 +80,3 @@ | ||
if (!_.isUndefined(keyCertPassphrase) && !_.isEmpty(keyCertPassphrase)) _.set(signWith, "passphrase", keyCertPassphrase); | ||
if (!_.isUndefined(keyCertPassphrase) && !_.isEmpty(keyCertPassphrase)) _.set(signWith, 'passphrase', keyCertPassphrase); | ||
@@ -89,11 +88,9 @@ // Load pem file containing the x509 cert & private key & sign the base string with it. | ||
// D) Assembling the Header | ||
const strApexHeader = "PKI_SIGN timestamp=\"" + timestamp + | ||
"\",nonce=\"" + nonceValue + | ||
"\",app_id=\"" + appId + | ||
"\",signature_method=\"RS256\"" + | ||
",signature=\"" + signature + | ||
"\""; | ||
const strApexHeader = `\ | ||
PKI_SIGN timestamp="${timestamp}",\ | ||
nonce="${nonceValue}",\ | ||
app_id="${appId}",\ | ||
signature_method="RS256",\ | ||
signature="${signature}"`; | ||
@@ -111,12 +108,10 @@ return strApexHeader; | ||
*/ | ||
security.generateAuthorizationHeader = function(url, params, method, strContentType, authType, appId, keyCertContent, passphrase) { | ||
if (authType == "L2") { | ||
return generateSHA256withRSAHeader(url, params, method, strContentType, appId, keyCertContent, passphrase); | ||
} | ||
else { | ||
return ""; | ||
} | ||
security.generateAuthorizationHeader = function generateAuthorizationHeader( | ||
url, params, method, strContentType, | ||
authType, appId, keyCertContent, passphrase, | ||
) { | ||
if (authType !== 'L2') return ''; | ||
return generateSHA256withRSAHeader(url, params, method, strContentType, appId, keyCertContent, passphrase); | ||
}; | ||
} | ||
// Verify & Decode JWS or JWT | ||
@@ -129,19 +124,17 @@ security.verifyJWS = function verifyJWS(jws, publicCert) { | ||
algorithms: ['RS256'], | ||
ignoreNotBefore: true | ||
ignoreNotBefore: true, | ||
}); | ||
return decoded; | ||
} catch (error) { | ||
throw ("Error with verifying and decoding JWE"); | ||
throw new Error('ERROR WITH VERIFYING AND DECODING JWE'); | ||
} | ||
} | ||
}; | ||
// Decrypt JWE using private key | ||
security.decryptJWE = function decryptJWE(header, encryptedKey, iv, cipherText, tag, privateKey) { | ||
return new Promise((resolve, reject) => { | ||
security.decryptJWE = async function decryptJWE(header, encryptedKey, iv, cipherText, tag, privateKey) { | ||
try { | ||
const keystore = jose.JWK.createKeyStore(); | ||
const data = { | ||
type: "compact", | ||
type: 'compact', | ||
ciphertext: cipherText, | ||
@@ -152,23 +145,13 @@ protected: header, | ||
iv, | ||
header: JSON.parse(jose.util.base64url.decode(header).toString()) | ||
header: JSON.parse(jose.util.base64url.decode(header).toString()), | ||
}; | ||
keystore.add(fs.readFileSync(privateKey, 'utf8'), "pem") | ||
.then(function(jweKey) { | ||
// {result} is a jose.JWK.Key | ||
jose.JWE.createDecrypt(jweKey) | ||
.decrypt(data) | ||
.then(function(result) { | ||
resolve(JSON.parse(result.payload.toString())); | ||
}) | ||
.catch(function(error) { | ||
reject(error); | ||
}); | ||
}); | ||
}) | ||
.catch (error => { | ||
throw "Error with decrypting JWE"; | ||
}) | ||
} | ||
const jweKey = await keystore.add(fs.readFileSync(privateKey, 'utf8'), 'pem'); | ||
const result = await jose.JWE.createDecrypt(jweKey).decrypt(data); | ||
return JSON.parse(result.payload.toString()); | ||
} catch (error) { | ||
throw new Error('ERROR WITH DECRYPTING JWE'); | ||
} | ||
}; | ||
module.exports = security; |
{ | ||
"name": "node-my-info-sg", | ||
"version": "1.0.5", | ||
"version": "1.1.0", | ||
"description": "", | ||
"main": "lib/client.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "jest", | ||
"test:coverage": "jest --coverage --collectCoverageFrom='[\"**/src/**/*.js\", \"!**/examples/**\",\"!**/components/**\",\"!**/constants/**\"]'", | ||
"test:watch": "jest --watch", | ||
"lint": "eslint --ext .js ." | ||
}, | ||
"author": "", | ||
"author": "StashAway", | ||
"repository": { | ||
@@ -19,10 +22,30 @@ "type": "git", | ||
"dependencies": { | ||
"bluebird": "^3.5.4", | ||
"jsonwebtoken": "^8.5.1", | ||
"lodash": "^4.17.11", | ||
"node-jose": "^1.1.3", | ||
"nonce": "^1.0.4", | ||
"superagent": "^5.0.2", | ||
"superagent-bluebird-promise": "^4.2.0" | ||
"superagent": "^5.0.2" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.0.0-0", | ||
"@babel/preset-env": "^7.4.4", | ||
"babel-eslint": "^10.0.1", | ||
"eslint": "^5.16.0", | ||
"eslint-config-airbnb": "^17.1.0", | ||
"eslint-plugin-import": "^2.14.0", | ||
"eslint-plugin-jsx-a11y": "^6.1.1", | ||
"eslint-plugin-react": "^7.11.0", | ||
"jest": "^24.8.0", | ||
"jest-watch-typeahead": "^0.3.1", | ||
"puppeteer": "^1.15.0", | ||
"sinon": "^7.3.2" | ||
}, | ||
"jest": { | ||
"testMatch": [ | ||
"**/?(*.)(spec|test).js?(x)" | ||
], | ||
"watchPlugins": [ | ||
"jest-watch-typeahead/filename", | ||
"jest-watch-typeahead/testname" | ||
] | ||
} | ||
} |
# node-my-info-sg πΈπ¬ | ||
[![npm version](https://badge.fury.io/js/node-my-info-sg.svg)](https://badge.fury.io/js/node-my-info-sg) [![CircleCI](https://circleci.com/gh/stashaway-engineering/node-my-info-sg.svg?style=svg)](https://circleci.com/gh/stashaway-engineering/node-my-info-sg) | ||
@@ -62,3 +63,10 @@ Small wrapper around Singapore [MyInfo V3 API](https://www.ndi-api.gov.sg/library/trusted-data/myinfo/introduction) for node JS. Wraps the scary-scary π± security logic into easy to use APIs | ||
``` | ||
## Test | ||
```js | ||
yarn test | ||
``` | ||
## Example | ||
@@ -75,3 +83,3 @@ | ||
1. Add unit tests and sensible linting rules | ||
1. Pass this repository to the cool government guy, so they can maintain it | ||
1. Add sensible linting rules | ||
1. Pass this repository to the cool government guy, so they can maintain it |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
86170
4
13
387
0
1
84
12
1
- Removedbluebird@^3.5.4
- Removednonce@^1.0.4
- Removedsuperagent-bluebird-promise@^4.2.0
- Removedbluebird@3.7.2(transitive)
- Removednonce@1.0.4(transitive)
- Removedsuperagent-bluebird-promise@4.2.0(transitive)