Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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` |
| REACT_APP_PATIENT_FHIR_QUERY | `Patient?_sort=identifier&_count=12` |
14 changes: 14 additions & 0 deletions public/.well-known/jwks.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Binary file added src/.DS_Store
Binary file not shown.
27 changes: 11 additions & 16 deletions src/containers/RequestBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ 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';

export default class RequestBuilder extends Component {
constructor(props) {
super(props);
this.state = {
keypair: null,
loading: false,
logs: [],
patient: {},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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}
/>
</div>
<br />
Expand Down
6 changes: 6 additions & 0 deletions src/keys/crdPrivateKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const privKey = `-----BEGIN PRIVATE KEY-----
ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDBuhq1aqZUJC+bzBqkLtcgX
fejJ2PNdStvozDPhdyWRccJIRi13GPfjkszAUlAC+s0=
-----END PRIVATE KEY-----`;

export default privKey;
89 changes: 23 additions & 66 deletions src/util/auth.js
Original file line number Diff line number Diff line change
@@ -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() +
Expand Down Expand Up @@ -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 };