From 49533e96bb7e57d2efcd964dfccb71867ba9e48b Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Mon, 18 Dec 2023 11:29:52 -0500 Subject: [PATCH 1/5] adding pre-generated public private keys for signing and verifying json web tokens used in cds-hooks calls --- .env | 2 +- public/.well-known/jwks.json | 14 +++++ src/containers/RequestBuilder.js | 24 +++------ src/keys/crdPrivateKey.js | 6 +++ src/util/auth.js | 90 +++++++++----------------------- 5 files changed, 53 insertions(+), 83 deletions(-) create mode 100644 public/.well-known/jwks.json create mode 100644 src/keys/crdPrivateKey.js diff --git a/.env b/.env index b7107e30..661d24f2 100644 --- a/.env +++ b/.env @@ -16,7 +16,7 @@ REACT_APP_PATIENT_VIEW = rems-patient-view REACT_APP_PATIENT_FHIR_QUERY = Patient?_sort=identifier&_count=12 REACT_APP_USER = alice REACT_APP_PASSWORD = alice -REACT_APP_PUBLIC_KEYS = http://localhost:3001/public_keys +REACT_APP_PUBLIC_KEYS = http://localhost:3000/request-generator/.well-known/jwks.json REACT_APP_ALT_DRUG = true REACT_APP_LAUNCH_URL = http://localhost:4040/launch REACT_APP_SMART_LAUNCH_URL = http://localhost:4040/ diff --git a/public/.well-known/jwks.json b/public/.well-known/jwks.json new file mode 100644 index 00000000..f553fc46 --- /dev/null +++ b/public/.well-known/jwks.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "kty": "EC", + "d": "boatWqmVCQvm8wapC7XIF33oydjzXUrb6Mwz4XclkXHCSEYtdxj345LMwFJQAvrN", + "use": "sig", + "crv": "P-384", + "kid": "zGe023HzCFfY7NPb04EGvRDP1oYsTOtLNCNjDgr66AI", + "x": "GJ1EKKadP512kbQLAhu3qftADevkhCcaOFFZi376S8dvhjZU9vxNy3wplJv_GiOr", + "y": "-0nhaXoadjGOAOuMp4ekU7ricjF6So2n57k0N-VrJ9hqA-A0PhnShrmGQdBIEKah", + "alg": "ES384" + } + ] +} \ No newline at end of file diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index adecf489..06f52f72 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -8,7 +8,7 @@ import SettingsBox from '../components/SettingsBox/SettingsBox'; import RequestBox from '../components/RequestBox/RequestBox'; import buildRequest from '../util/buildRequest.js'; import { types } from '../util/data.js'; -import { createJwt, setupKeys } from '../util/auth'; +import { createJwt } from '../util/auth'; import env from 'env-var'; import FHIR from 'fhirclient'; @@ -16,7 +16,6 @@ export default class RequestBuilder extends Component { constructor(props) { super(props); this.state = { - keypair: null, loading: false, logs: [], patient: {}, @@ -55,11 +54,6 @@ export default class RequestBuilder extends Component { } componentDidMount() { - const callback = keypair => { - this.setState({ keypair }); - }; - - setupKeys(callback); if (!this.state.client) { this.reconnectEhr(); } else { @@ -129,16 +123,12 @@ export default class RequestBuilder extends Component { this.consoleLog("ERROR: unknown hook type: '", hook, "'"); return; } - - const createHeaders = () => { - const init = { 'Content-Type': 'application/json' }; - if (this.state.generateJsonToken) { - const jwt = 'Bearer ' + createJwt(this.state.keypair, this.state.baseUrl, cdsUrl); - init.authorization = jwt; - } - return new Headers(init); - }; - + let baseUrl = this.state.baseUrl; + const jwt = 'Bearer ' + createJwt( baseUrl, cdsUrl); + const headers = new Headers({ + 'Content-Type': 'application/json', + authorization: jwt + }); try { fetch(cdsUrl, { method: 'POST', diff --git a/src/keys/crdPrivateKey.js b/src/keys/crdPrivateKey.js new file mode 100644 index 00000000..f8ad3a14 --- /dev/null +++ b/src/keys/crdPrivateKey.js @@ -0,0 +1,6 @@ +const privKey = `-----BEGIN PRIVATE KEY----- +ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDBuhq1aqZUJC+bzBqkLtcgX +fejJ2PNdStvozDPhdyWRccJIRi13GPfjkszAUlAC+s0= +-----END PRIVATE KEY-----`; + +export default privKey; \ No newline at end of file diff --git a/src/util/auth.js b/src/util/auth.js index d0559037..9dcee5ac 100644 --- a/src/util/auth.js +++ b/src/util/auth.js @@ -1,15 +1,8 @@ +import privKey from '../keys/crdPrivateKey.js'; import KJUR, { KEYUTIL } from 'jsrsasign'; +import { v4 as uuidv4 } from 'uuid'; import env from 'env-var'; -function makeid() { - var text = []; - var possible = '---ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - for (var i = 0; i < 25; i++) - text.push(possible.charAt(Math.floor(Math.random() * possible.length))); - - return text.join(''); -} function login() { const tokenUrl = @@ -41,65 +34,32 @@ function login() { }); } -function createJwt(keypair, baseUrl, cdsUrl) { - console.log('creating jwt'); - const currentTime = KJUR.jws.IntDate.get('now'); - const endTime = KJUR.jws.IntDate.get('now + 1day'); - const kid = KJUR.jws.JWS.getJWKthumbprint(keypair.public); - - const header = { - alg: 'RS256', - typ: 'JWT', - kid: kid, - jku: env.get('REACT_APP_PUBLIC_KEYS').asString() - }; - const body = { - iss: baseUrl, - aud: cdsUrl, - iat: currentTime, - exp: endTime, - jti: makeid() - }; - - var sJWT = KJUR.jws.JWS.sign( - 'RS256', - JSON.stringify(header), - JSON.stringify(body), - keypair.private - ); - return sJWT; -} -function setupKeys(callback) { - const { prvKeyObj, pubKeyObj } = KEYUTIL.generateKeypair('RSA', 2048); - const jwkPrv2 = KEYUTIL.getJWKFromKey(prvKeyObj); - const jwkPub2 = KEYUTIL.getJWKFromKey(pubKeyObj); - const kid = KJUR.jws.JWS.getJWKthumbprint(jwkPub2); +/** + * Generates a JWT for a CDS service call, given the audience (the URL endpoint). The JWT is signed using a private key stored on the repository. + * + * Note: In production environments, the JWT should be signed on a secured server for best practice. The private key is exposed on the repository + * as it is an open source client-side project and tool. + * @param {*} audience - URL endpoint acting as the audience + */ + function createJwt(baseUrl, audience) { - const keypair = { - private: jwkPrv2, - public: jwkPub2, - kid: kid - }; + const jwtPayload = JSON.stringify({ + iss: baseUrl, + aud: audience, + exp: Math.round((Date.now() / 1000) + 300), + iat: Math.round((Date.now() / 1000)), + jti: uuidv4(), + }); - const pubPem = { - pem: jwkPub2, - id: kid - }; + const jwtHeader = JSON.stringify({ + alg: 'ES384', + typ: 'JWT', + kid: 'zGe023HzCFfY7NPb04EGvRDP1oYsTOtLNCNjDgr66AI', + jku: env.get('REACT_APP_PUBLIC_KEYS').asString() + }); - fetch(`${env.get('REACT_APP_PUBLIC_KEYS').asString()}/`, { - body: JSON.stringify(pubPem), - headers: { - 'Content-Type': 'application/json' - }, - method: 'POST' - }) - .then(response => { - callback(keypair); - }) - .catch(error => { - console.log(error); - }); + return KJUR.jws.JWS.sign(null, jwtHeader, jwtPayload, privKey); } -export { createJwt, login, setupKeys }; +export { createJwt, login }; From 2540f6f60cda7f3e3243bfa528d540a163104d42 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Tue, 19 Dec 2023 10:20:00 -0500 Subject: [PATCH 2/5] Prttier fixes --- public/.well-known/jwks.json | 22 +++++++++++----------- src/.DS_Store | Bin 0 -> 6148 bytes src/containers/RequestBuilder.js | 2 +- src/keys/crdPrivateKey.js | 2 +- src/util/auth.js | 11 ++++------- 5 files changed, 17 insertions(+), 20 deletions(-) create mode 100644 src/.DS_Store diff --git a/public/.well-known/jwks.json b/public/.well-known/jwks.json index f553fc46..0465086b 100644 --- a/public/.well-known/jwks.json +++ b/public/.well-known/jwks.json @@ -1,14 +1,14 @@ { "keys": [ - { - "kty": "EC", - "d": "boatWqmVCQvm8wapC7XIF33oydjzXUrb6Mwz4XclkXHCSEYtdxj345LMwFJQAvrN", - "use": "sig", - "crv": "P-384", - "kid": "zGe023HzCFfY7NPb04EGvRDP1oYsTOtLNCNjDgr66AI", - "x": "GJ1EKKadP512kbQLAhu3qftADevkhCcaOFFZi376S8dvhjZU9vxNy3wplJv_GiOr", - "y": "-0nhaXoadjGOAOuMp4ekU7ricjF6So2n57k0N-VrJ9hqA-A0PhnShrmGQdBIEKah", - "alg": "ES384" - } + { + "kty": "EC", + "d": "boatWqmVCQvm8wapC7XIF33oydjzXUrb6Mwz4XclkXHCSEYtdxj345LMwFJQAvrN", + "use": "sig", + "crv": "P-384", + "kid": "zGe023HzCFfY7NPb04EGvRDP1oYsTOtLNCNjDgr66AI", + "x": "GJ1EKKadP512kbQLAhu3qftADevkhCcaOFFZi376S8dvhjZU9vxNy3wplJv_GiOr", + "y": "-0nhaXoadjGOAOuMp4ekU7ricjF6So2n57k0N-VrJ9hqA-A0PhnShrmGQdBIEKah", + "alg": "ES384" + } ] -} \ No newline at end of file +} diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4efe7f2488c12a7abaa0448d03911fb783443795 GIT binary patch literal 6148 zcmeHKJ5EC}5S)b+5i}_&eFbh{Md1Wo07xj1BA!H`e--D-(K7ofh+fi#CYqJjW3P8? zd5X7h0od|q^9U>e%;}E!^@1ciWrg3GhXq5PrTy`yGi!xfOGG##z5ZpU;bvd z-o6dP))Qn>Knh3!DIf);z{M1((mKDt*oitw3P^!#QNX_sjqcbB$He$_aEKOwIAb`B z^XMgr%>%?=eJ0Q^+b(QKnffy zaGT48*Z(K_ng0Knq?Htq0#~Jg%~sE=C7)Eab@Di`wT=El_nbZ5jq{*zh;mGfa?FL7 e}w-uuR literal 0 HcmV?d00001 diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index 06f52f72..8efb4d36 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -124,7 +124,7 @@ export default class RequestBuilder extends Component { return; } let baseUrl = this.state.baseUrl; - const jwt = 'Bearer ' + createJwt( baseUrl, cdsUrl); + const jwt = 'Bearer ' + createJwt(baseUrl, cdsUrl); const headers = new Headers({ 'Content-Type': 'application/json', authorization: jwt diff --git a/src/keys/crdPrivateKey.js b/src/keys/crdPrivateKey.js index f8ad3a14..10b2c799 100644 --- a/src/keys/crdPrivateKey.js +++ b/src/keys/crdPrivateKey.js @@ -3,4 +3,4 @@ ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDBuhq1aqZUJC+bzBqkLtcgX fejJ2PNdStvozDPhdyWRccJIRi13GPfjkszAUlAC+s0= -----END PRIVATE KEY-----`; -export default privKey; \ No newline at end of file +export default privKey; diff --git a/src/util/auth.js b/src/util/auth.js index 9dcee5ac..0258583b 100644 --- a/src/util/auth.js +++ b/src/util/auth.js @@ -3,7 +3,6 @@ import KJUR, { KEYUTIL } from 'jsrsasign'; import { v4 as uuidv4 } from 'uuid'; import env from 'env-var'; - function login() { const tokenUrl = env.get('REACT_APP_AUTH').asString() + @@ -34,7 +33,6 @@ function login() { }); } - /** * Generates a JWT for a CDS service call, given the audience (the URL endpoint). The JWT is signed using a private key stored on the repository. * @@ -42,14 +40,13 @@ function login() { * as it is an open source client-side project and tool. * @param {*} audience - URL endpoint acting as the audience */ - function createJwt(baseUrl, audience) { - +function createJwt(baseUrl, audience) { const jwtPayload = JSON.stringify({ iss: baseUrl, aud: audience, - exp: Math.round((Date.now() / 1000) + 300), - iat: Math.round((Date.now() / 1000)), - jti: uuidv4(), + exp: Math.round(Date.now() / 1000 + 300), + iat: Math.round(Date.now() / 1000), + jti: uuidv4() }); const jwtHeader = JSON.stringify({ From 63acae2f7f4ba18101d1233bba6d091bddc970fa Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Tue, 19 Dec 2023 10:42:24 -0500 Subject: [PATCH 3/5] fixing issue introduced in rebase --- src/containers/RequestBuilder.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index 8efb4d36..8976f5d3 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -123,16 +123,22 @@ export default class RequestBuilder extends Component { this.consoleLog("ERROR: unknown hook type: '", hook, "'"); return; } + let baseUrl = this.state.baseUrl; - const jwt = 'Bearer ' + createJwt(baseUrl, cdsUrl); - const headers = new Headers({ + + const headers = { 'Content-Type': 'application/json', authorization: jwt - }); + }; + if (this.state.generateJsonToken) { + const jwt = 'Bearer ' + createJwt(baseUrl, cdsUrl); + headers.authorization = jwt; + } + try { fetch(cdsUrl, { method: 'POST', - headers: createHeaders(), + headers: new Headers(headers), body: JSON.stringify(json_request), signal: this.timeout(10).signal //Timeout set to 10 seconds }) @@ -220,7 +226,7 @@ export default class RequestBuilder extends Component { ref={this.requestBox} loading={this.state.loading} consoleLog={this.consoleLog} - patientFhirQuery ={this.state.patientFhirQuery} + patientFhirQuery={this.state.patientFhirQuery} />
From 9c0a70466dca2cab2b91a1aa860c2376313857dd Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Tue, 19 Dec 2023 10:50:03 -0500 Subject: [PATCH 4/5] fixing undefined var issue --- src/containers/RequestBuilder.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index 8976f5d3..777538cd 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -127,8 +127,7 @@ export default class RequestBuilder extends Component { let baseUrl = this.state.baseUrl; const headers = { - 'Content-Type': 'application/json', - authorization: jwt + 'Content-Type': 'application/json' }; if (this.state.generateJsonToken) { const jwt = 'Bearer ' + createJwt(baseUrl, cdsUrl); From 2e7a24bcb128f828492dfc2a7d64c67134cf808c Mon Sep 17 00:00:00 2001 From: rdingwell Date: Fri, 22 Dec 2023 11:11:47 -0500 Subject: [PATCH 5/5] Update README.md Adding description for public/private keys --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 04e21b68..830d0c66 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ This should open a browser window directed to the value set in `REACT_APP_URL`. ## Versions This application requires node v14. +## Keys +Embedded in the application are the public and provate keys used to generate and verify JSON Web Tokens (JWT) that are used to authenticate/authorize calls to a CDS-Hooks service. The public key is contained in the public/.well-known/jwks.json document. The private key is contained in src/keys/crdPrivateKey.js file. The keys were generated from https://mkjwk.org/. To update these keys you can generate a new key pair from this site, ensure that you request the Show X.509 option is set to yes. Once generated you can replace the public and private keys. You will also need to update the src/utils/auth.js file with the corrisponding key information. ### How To Override Defaults The .env file contains the default URI paths, these can be overwritten from the start command as follows: @@ -52,4 +54,4 @@ Following are a list of modifiable paths: | HTTPS | `false` | | HTTPS_KEY_PATH | `server.key` | | HTTPS_CERT_PATH | `server.cert` | -| REACT_APP_PATIENT_FHIR_QUERY | `Patient?_sort=identifier&_count=12` | \ No newline at end of file +| REACT_APP_PATIENT_FHIR_QUERY | `Patient?_sort=identifier&_count=12` |