Skip to content

Commit da1baef

Browse files
authored
Merge pull request #86 from mcode/jwt_keys
Jwt keys
2 parents 27afeb5 + 2e7a24b commit da1baef

File tree

7 files changed

+58
-84
lines changed

7 files changed

+58
-84
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ REACT_APP_PATIENT_VIEW = rems-patient-view
1616
REACT_APP_PATIENT_FHIR_QUERY = Patient?_sort=identifier&_count=12
1717
REACT_APP_USER = alice
1818
REACT_APP_PASSWORD = alice
19-
REACT_APP_PUBLIC_KEYS = http://localhost:3001/public_keys
19+
REACT_APP_PUBLIC_KEYS = http://localhost:3000/request-generator/.well-known/jwks.json
2020
REACT_APP_ALT_DRUG = true
2121
REACT_APP_LAUNCH_URL = http://localhost:4040/launch
2222
REACT_APP_SMART_LAUNCH_URL = http://localhost:4040/

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ This should open a browser window directed to the value set in `REACT_APP_URL`.
1616
## Versions
1717
This application requires node v14.
1818

19+
## Keys
20+
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.
1921

2022
### How To Override Defaults
2123
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:
5254
| HTTPS | `false` |
5355
| HTTPS_KEY_PATH | `server.key` |
5456
| HTTPS_CERT_PATH | `server.cert` |
55-
| REACT_APP_PATIENT_FHIR_QUERY | `Patient?_sort=identifier&_count=12` |
57+
| REACT_APP_PATIENT_FHIR_QUERY | `Patient?_sort=identifier&_count=12` |

public/.well-known/jwks.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"keys": [
3+
{
4+
"kty": "EC",
5+
"d": "boatWqmVCQvm8wapC7XIF33oydjzXUrb6Mwz4XclkXHCSEYtdxj345LMwFJQAvrN",
6+
"use": "sig",
7+
"crv": "P-384",
8+
"kid": "zGe023HzCFfY7NPb04EGvRDP1oYsTOtLNCNjDgr66AI",
9+
"x": "GJ1EKKadP512kbQLAhu3qftADevkhCcaOFFZi376S8dvhjZU9vxNy3wplJv_GiOr",
10+
"y": "-0nhaXoadjGOAOuMp4ekU7ricjF6So2n57k0N-VrJ9hqA-A0PhnShrmGQdBIEKah",
11+
"alg": "ES384"
12+
}
13+
]
14+
}

src/.DS_Store

6 KB
Binary file not shown.

src/containers/RequestBuilder.js

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@ import SettingsBox from '../components/SettingsBox/SettingsBox';
88
import RequestBox from '../components/RequestBox/RequestBox';
99
import buildRequest from '../util/buildRequest.js';
1010
import { types } from '../util/data.js';
11-
import { createJwt, setupKeys } from '../util/auth';
11+
import { createJwt } from '../util/auth';
1212
import env from 'env-var';
1313
import FHIR from 'fhirclient';
1414

1515
export default class RequestBuilder extends Component {
1616
constructor(props) {
1717
super(props);
1818
this.state = {
19-
keypair: null,
2019
loading: false,
2120
logs: [],
2221
patient: {},
@@ -55,11 +54,6 @@ export default class RequestBuilder extends Component {
5554
}
5655

5756
componentDidMount() {
58-
const callback = keypair => {
59-
this.setState({ keypair });
60-
};
61-
62-
setupKeys(callback);
6357
if (!this.state.client) {
6458
this.reconnectEhr();
6559
} else {
@@ -130,19 +124,20 @@ export default class RequestBuilder extends Component {
130124
return;
131125
}
132126

133-
const createHeaders = () => {
134-
const init = { 'Content-Type': 'application/json' };
135-
if (this.state.generateJsonToken) {
136-
const jwt = 'Bearer ' + createJwt(this.state.keypair, this.state.baseUrl, cdsUrl);
137-
init.authorization = jwt;
138-
}
139-
return new Headers(init);
127+
let baseUrl = this.state.baseUrl;
128+
129+
const headers = {
130+
'Content-Type': 'application/json'
140131
};
132+
if (this.state.generateJsonToken) {
133+
const jwt = 'Bearer ' + createJwt(baseUrl, cdsUrl);
134+
headers.authorization = jwt;
135+
}
141136

142137
try {
143138
fetch(cdsUrl, {
144139
method: 'POST',
145-
headers: createHeaders(),
140+
headers: new Headers(headers),
146141
body: JSON.stringify(json_request),
147142
signal: this.timeout(10).signal //Timeout set to 10 seconds
148143
})
@@ -230,7 +225,7 @@ export default class RequestBuilder extends Component {
230225
ref={this.requestBox}
231226
loading={this.state.loading}
232227
consoleLog={this.consoleLog}
233-
patientFhirQuery ={this.state.patientFhirQuery}
228+
patientFhirQuery={this.state.patientFhirQuery}
234229
/>
235230
</div>
236231
<br />

src/keys/crdPrivateKey.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const privKey = `-----BEGIN PRIVATE KEY-----
2+
ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDBuhq1aqZUJC+bzBqkLtcgX
3+
fejJ2PNdStvozDPhdyWRccJIRi13GPfjkszAUlAC+s0=
4+
-----END PRIVATE KEY-----`;
5+
6+
export default privKey;

src/util/auth.js

Lines changed: 23 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1+
import privKey from '../keys/crdPrivateKey.js';
12
import KJUR, { KEYUTIL } from 'jsrsasign';
3+
import { v4 as uuidv4 } from 'uuid';
24
import env from 'env-var';
35

4-
function makeid() {
5-
var text = [];
6-
var possible = '---ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
7-
8-
for (var i = 0; i < 25; i++)
9-
text.push(possible.charAt(Math.floor(Math.random() * possible.length)));
10-
11-
return text.join('');
12-
}
13-
146
function login() {
157
const tokenUrl =
168
env.get('REACT_APP_AUTH').asString() +
@@ -41,65 +33,30 @@ function login() {
4133
});
4234
}
4335

44-
function createJwt(keypair, baseUrl, cdsUrl) {
45-
console.log('creating jwt');
46-
const currentTime = KJUR.jws.IntDate.get('now');
47-
const endTime = KJUR.jws.IntDate.get('now + 1day');
48-
const kid = KJUR.jws.JWS.getJWKthumbprint(keypair.public);
36+
/**
37+
* 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.
38+
*
39+
* Note: In production environments, the JWT should be signed on a secured server for best practice. The private key is exposed on the repository
40+
* as it is an open source client-side project and tool.
41+
* @param {*} audience - URL endpoint acting as the audience
42+
*/
43+
function createJwt(baseUrl, audience) {
44+
const jwtPayload = JSON.stringify({
45+
iss: baseUrl,
46+
aud: audience,
47+
exp: Math.round(Date.now() / 1000 + 300),
48+
iat: Math.round(Date.now() / 1000),
49+
jti: uuidv4()
50+
});
4951

50-
const header = {
51-
alg: 'RS256',
52+
const jwtHeader = JSON.stringify({
53+
alg: 'ES384',
5254
typ: 'JWT',
53-
kid: kid,
55+
kid: 'zGe023HzCFfY7NPb04EGvRDP1oYsTOtLNCNjDgr66AI',
5456
jku: env.get('REACT_APP_PUBLIC_KEYS').asString()
55-
};
56-
const body = {
57-
iss: baseUrl,
58-
aud: cdsUrl,
59-
iat: currentTime,
60-
exp: endTime,
61-
jti: makeid()
62-
};
63-
64-
var sJWT = KJUR.jws.JWS.sign(
65-
'RS256',
66-
JSON.stringify(header),
67-
JSON.stringify(body),
68-
keypair.private
69-
);
70-
return sJWT;
71-
}
72-
73-
function setupKeys(callback) {
74-
const { prvKeyObj, pubKeyObj } = KEYUTIL.generateKeypair('RSA', 2048);
75-
const jwkPrv2 = KEYUTIL.getJWKFromKey(prvKeyObj);
76-
const jwkPub2 = KEYUTIL.getJWKFromKey(pubKeyObj);
77-
const kid = KJUR.jws.JWS.getJWKthumbprint(jwkPub2);
78-
79-
const keypair = {
80-
private: jwkPrv2,
81-
public: jwkPub2,
82-
kid: kid
83-
};
84-
85-
const pubPem = {
86-
pem: jwkPub2,
87-
id: kid
88-
};
57+
});
8958

90-
fetch(`${env.get('REACT_APP_PUBLIC_KEYS').asString()}/`, {
91-
body: JSON.stringify(pubPem),
92-
headers: {
93-
'Content-Type': 'application/json'
94-
},
95-
method: 'POST'
96-
})
97-
.then(response => {
98-
callback(keypair);
99-
})
100-
.catch(error => {
101-
console.log(error);
102-
});
59+
return KJUR.jws.JWS.sign(null, jwtHeader, jwtPayload, privKey);
10360
}
10461

105-
export { createJwt, login, setupKeys };
62+
export { createJwt, login };

0 commit comments

Comments
 (0)