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/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` | diff --git a/public/.well-known/jwks.json b/public/.well-known/jwks.json new file mode 100644 index 00000000..0465086b --- /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" + } + ] +} diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..4efe7f24 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index adecf489..777538cd 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 { @@ -130,19 +124,20 @@ export default class RequestBuilder extends Component { 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 headers = { + 'Content-Type': 'application/json' }; + 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 }) @@ -230,7 +225,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} />
diff --git a/src/keys/crdPrivateKey.js b/src/keys/crdPrivateKey.js new file mode 100644 index 00000000..10b2c799 --- /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; diff --git a/src/util/auth.js b/src/util/auth.js index d0559037..0258583b 100644 --- a/src/util/auth.js +++ b/src/util/auth.js @@ -1,16 +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 = env.get('REACT_APP_AUTH').asString() + @@ -41,65 +33,30 @@ 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); +/** + * 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 jwtPayload = JSON.stringify({ + iss: baseUrl, + aud: audience, + exp: Math.round(Date.now() / 1000 + 300), + iat: Math.round(Date.now() / 1000), + jti: uuidv4() + }); - const header = { - alg: 'RS256', + const jwtHeader = JSON.stringify({ + alg: 'ES384', typ: 'JWT', - kid: kid, + kid: 'zGe023HzCFfY7NPb04EGvRDP1oYsTOtLNCNjDgr66AI', 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); - - const keypair = { - private: jwkPrv2, - public: jwkPub2, - kid: kid - }; - - const pubPem = { - pem: jwkPub2, - id: kid - }; + }); - 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 };