-
Notifications
You must be signed in to change notification settings - Fork 37
/
Copy pathcds-hooks-launch.js
170 lines (141 loc) · 5.75 KB
/
cds-hooks-launch.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/* eslint import/no-extraneous-dependencies: ['error', {'devDependencies': true}] */
/* eslint no-console: 0, import/no-unresolved: 0 */
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const axios = require('axios');
const fs = require('fs');
const Client = require('../../lib/client');
const CapabilityTool = require('../../lib/capability-tool');
const app = express();
/* global pemPath */
// const pemPath = './ecpublickey.pem';
// const jku = 'http://sandbox.cds-hooks.org/.well-known/jwks.json';
const whitelistedEHRs = [
{ iss: 'https://sandbox.cds-hooks.org', sub: '48163c5e-88b5-4cb3-92d3-23b800caa927' },
];
/**
* This is an example of a SMART app responding to CDS Hooks requests from an EHR.
*
* In this example, there are two routes:
* - /cds-services
* - /cds-services/patient-greeter
*
* The EHR will call the cds-services route (the "discovery endpoint")
* in order to configure any available CDS services for this SMART application.
*
* Based on this configuration, the EHR may then post to /cds-services/patient-view
* with prefetch data as prescribed by the cds-services discovery route and,
* optionally, FHIR authorization details (i.e., access_token and scope). If
* an access token is provided, it may then be used by the FHIR client for
* requests to the EHR, as seen in the MedicationOrder example.
* Before every request, the EHR is first directed through the `authenticateEHR` middleware.
*
* `authenticateEHR` expects a JSON Web Token (JWT) from the EHR's authorization
* request header. It is used to establish that the request is from a trusted party.
* The JWT can be verified by one of 3 different ways in this example:
*
* 1) By setting a PEM file in the current directory on line 15.
* 2) By generating a PEM file from a `jku` variable set on line 16.
* 3) By generating a PEM file from a `jku` in the decoded JWT header.
*
*/
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
async function authenticateEHR(req, res, next) {
const token = req.headers.authorization.replace('Bearer ', '');
const decodedJwt = jwt.decode(token, { complete: true });
const asymmetricAlgs = ['ES256', 'ES384', 'ES384', 'RS256', 'RS384', 'RS512'];
const { alg, jku, kid } = decodedJwt.header;
const { iss, sub } = decodedJwt.payload;
let pem;
let verified;
const isWhitelisted = whitelistedEHRs.find((ehr) => ehr.iss === iss && ehr.sub === sub);
if (!isWhitelisted) { return res.status(401).json('Authentication failed'); }
console.log(`token: ${token}`);
console.log(`decodedJwt: ${JSON.stringify(decodedJwt)}`);
if (asymmetricAlgs.includes(alg)) {
if (typeof pemPath !== 'undefined') {
// Use existing public key
try {
pem = fs.readFileSync(pemPath);
} catch (err) {
console.log(err);
}
} else if (typeof jku !== 'undefined') {
// Generate public key with an jwks.json endpoint
const jwks = await axios.get(jku);
const targetJwk = jwks.data.keys.find((key) => key.kid === kid);
pem = jwkToPem(targetJwk);
}
try {
verified = jwt.verify(token, pem, { algorithms: [alg] });
console.log('Verified with PEM');
} catch (error) {
console.error('Invalid Token Error', error.message);
return res.status(401).json('Authentication failed');
}
}
console.log(`Authenticated Token: ${JSON.stringify(verified)}`);
return next();
}
async function authenticateClient(req, res, next) {
const { fhirServer, fhirAuthorization } = req.body;
req.fhirClient = new Client({ baseUrl: fhirServer });
if (typeof fhirAuthorization === 'undefined') {
return next();
}
console.log('The token is : ', fhirAuthorization.access_token);
req.fhirClient.bearerToken = fhirAuthorization.access_token;
return next();
}
app.get('/cds-services', async (req, res) => res.status(200).json({
services: [
{
hook: 'patient-view',
id: 'patient-greeter',
title: 'Patient Greeter with Med Count',
description: 'Example of CDS service greeting patient based on prefetch and counting meds with FHIR Kit client.',
prefetch: {
patientToGreet: 'Patient/{{context.patientId}}',
},
},
],
}));
app.post('/cds-services/patient-greeter', [authenticateEHR, authenticateClient], async (req, res) => {
let patientGreeting = `Hello ${req.body.prefetch.patientToGreet.name[0].given[0]}! `;
if (typeof req.fhirClient !== 'undefined') {
const capabilityStatement = await req.fhirClient.capabilityStatement();
const capabilities = new CapabilityTool(capabilityStatement);
const medOrderSupport = capabilities.resourceCan('MedicationOrder', 'read');
const medRequestSupport = capabilities.resourceCan('MedicationRequest', 'read');
const searchParams = { patient: req.body.context.patientId };
let medOrders;
if (medOrderSupport) {
medOrders = await req.fhirClient.search({ resourceType: 'MedicationOrder', searchParams });
} else if (medRequestSupport) {
medOrders = await req.fhirClient.search({ resourceType: 'MedicationRequest', searchParams });
} else {
medOrders = { total: '0' };
}
patientGreeting += `You have ${medOrders.total} medication orders on file.`;
}
return res.status(200).json({
cards: [
{
summary: patientGreeting,
source: {
label: 'Patient greeting and med count service',
},
indicator: 'info',
},
],
});
});
const server = app.listen(3000, 'localhost', () => {
console.log('Express server started on port 3000');
console.log(`CDS Discovery endpoint is at http://${server.address().address}:3000/cds-services`);
});