diff --git a/Global Variables/fhirInterface.js b/Global Variables/fhirInterface.js new file mode 100644 index 0000000..c631c2a --- /dev/null +++ b/Global Variables/fhirInterface.js @@ -0,0 +1,88 @@ +return { + getToken() { + return new Promise(async (resolve, reject) => { + try { + getRedisKey = function () { + return `fhir-${fhirSettings.jwt.clientId}`; + }; + + const tokenEntry = await redis.hGetAll(getRedisKey()); + const currentTimeInSeconds = Math.floor(new Date().getTime() / 1000); + + if (tokenEntry.exp >= currentTimeInSeconds) { + resolve(JSON.parse(tokenEntry.token)); + return; + } + + // start the expiry base before we create a new token + let expiryInSeconds = Math.floor(new Date().getTime() / 1000); + + // Define headers + const headers = { + alg: "RS256", + typ: "JWT", + }; + + // Define your payload + const payload = { + iss: fhirSettings.jwt.client_id, + sub: fhirSettings.jwt.client_id, + aud: fhirSettings.token_url, + }; + + // Define token options (optional) + const options = { + expiresIn: "4m", // Token expires in 1 hour + notBefore: "0", + algorithm: "RS384", + keyid: "iva_id_1234", + jwtid: uuidv5("jwtid", fhirSettings.jwt.uuid_namespace), + }; + + let data = qs.stringify({ + grant_type: "client_credentials", + client_assertion_type: + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: jwt.sign( + payload, + fhirSettings.jwt.privateKey, + options + ), + }); + + let config = { + method: "post", + maxBodyLength: Infinity, + url: fhirSettings.token_url, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + data: data, + }; + + const result = await axios(config); + + if (result.status == 200) { + expiryInSeconds += result.data.expires_in; + console.log( + `fhirInterface.getToken: Adding ${getRedisKey()} to redis with expiry=${new Date( + expiryInSeconds * 1000 + )}` + ); + await redis.hSet(getRedisKey(), "exp", expiryInSeconds); + await redis.hSet(getRedisKey(), "token", JSON.stringify(result.data)); + resolve(result.data); + } else { + reject(result.status); + } + } catch (err) { + console.log("fhirInterface.getToken: Error getting Token"); + console.log(err); + if (err.response) { + console.log(err.response.data); // => the response payload + } + reject(err); + } + }); + }, +}; diff --git a/Global Variables/fhirSettings.json b/Global Variables/fhirSettings.json new file mode 100644 index 0000000..bea5dc1 --- /dev/null +++ b/Global Variables/fhirSettings.json @@ -0,0 +1,10 @@ +{ + "base_url": "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR", + "token_url": "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", + "jwt": { + "uuid_namespace": "30e3078d-59fe-4151-8571-304c87580209", + "client_id": "66eaeb38-c5f7-41cb-8cd3-791efacf25e5", + "privateKey": "*****" + }, + "patient_id": "T81lum-5p6QvDR7l6hv7lfE52bAbA2ylWBnv9CZEzNb0B" +} diff --git a/Proxy Scripts/fhirPatientRead.js b/Proxy Scripts/fhirPatientRead.js new file mode 100644 index 0000000..5e0bb14 --- /dev/null +++ b/Proxy Scripts/fhirPatientRead.js @@ -0,0 +1,23 @@ +(async () => { + try { + const token = await fhirInterface().getToken(); + + let config = { + method: "get", + url: `${fhirSettings.base_url}/DSTU2/Patient/${fhirSettings.patient_id}`, + headers: { + Accept: "application/json", + Authorization: `${token.token_type} ${token.access_token}`, + }, + }; + + const result = await axios(config); + res.json(result.data); + } catch (err) { + if (err.response) { + res.status(err.response.status).json(err.response.data); + } else { + res.status(400).json(JSON.stringify(err)); + } + } +})(); diff --git a/README.md b/README.md index 3688055..0f872de 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,87 @@ # fhir-iva-integration -Example integration to https://fhir.epic.com/ \ No newline at end of file +Example integration to https://fhir.epic.com/ + +## Introduction + +This repository provides and example integration to EPIC using the [Epic on FHIR](https://fhir.epiv.com) Sandbox. To use this example, you will need to create an account and Build an app run the APIs. + +## FHIR App + +This integration example requires you to build an App that allow the [SMART Backend Services (Backend OAUTH 2.0)](https://fhir.epic.com/Documentation?docId=oauth2§ion=BackendOAuth2Guide) workflow to obtain an access token. + +To use this integration, you will need to create a public and private key as described in the section [Creating a Public Private Key Pair for JWT Signature](https://fhir.epic.com/Documentation?docId=oauth2§ion=Creating-Key-Pair) and upload public key file to the FHIR Application and the Private Key to the *fhirSettings* global variable. + +When configuring the FHIR App, please make sure to: + +1. Select *Backend System* as the *Application Audience* +2. Select the APIs you wish to use. NOTE: Only DSTU2 APIs have been tested +3. Use the *Non-Production Client ID* in the *fhirSettings* Global Variable +4. Upload your *publickey509.pem* file to *Sandbox JWT Signing Public Key +5. Select *DSTU2* for *SMART on FHIR Version*. Other versions have not been tested +6. The rest should be optional for the Sandbox and not required for this example + +![FHIR App Example](/screenshots/Example%20FHIR%20App.png "FHIR App Example") + +## IVA Studio + +### Global Variables + +Add both *fhirInterface* as a Function and *fhirSettings* as an Object to Global Variables. + +*fhirInterface* currently as one function *getToken* that first checks REDIS for an unexpired token, then create one using the JWT method described by FHIR's documentation. + +> Because this is just an example, limited error handling and no Token refresh protocols have been implemented. + +#### fhirSettings + +Open the file fhirSettings and add your private key. + +> NOTE: Remember, because this is a JSON Object, new lines from the private key will need to be replaced with '\n'. +> +> This is not recommended for a production environment. + +The file should look something like this when complete. + +```json +{ + "base_url" : "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR", + "token_url" : "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", + "jwt" : { + "uuid_namespace" : "30e3078d-59fe-4151-8571-304c87580209", + "client_id" : "66eaeb38-c5f7-41cb-8cd3-791efacf25e5", + "privateKey" : "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA45nS5ecQak+ewzLOCkWnvwp+cjw1NiZOLV1aX+DGxDkuEX3h\ndQLAYT+bQ+MFojqPQErxnp3kFR+uLQSfQ3tkBL3v4H07hLnV/oZQ5d1CCoCfNVQS\nI3FyN0jr/qPMrqrih+QVouA01GvupShSGrrBYks204lCmHE0VWnvP/+BKDdlEeD5\n1uuDXKpDdE4tQh52y92YhDEoSmw9q6RW2IFKMeaQt3h4nOLVkMNcMyLWaspqmbCE\nTpduwVseY19QV8bNQiy6JaWp3AiVL2+M9VjEr5i2DFMRfQVSPOElCzDlwXZ4LFyk\nOMvPYyECnly7TCiGWcenR/iCjdmgFsrUZKbbqQIDAQABAoIBAHbktQnJ8YZHf1zi\nnkU5a85dMf6EuxtFWVNTT9GD/vEkGY+jnXHddReX/YiyABBl3M0uGRfNzQbH3NnB\nb1z2CSJ9AeDYKo5D8aibC4l4UnZgCEr4Vt1S9uIwYq9La7HWrK1mFXNXAeHxW+HE\ntVcnNbweJE7Ohg5SHI993jAlTZfuk6jqbRFQiOsdSAWcXAYDOeGNHJW/OEmcHa1v\n+vFA8eOjRo8fgxDWgOASdHlWfe4oY3BgKvIlFxtnO7bbqfE9+66lZ+VZMSBivWKZ\nspW1jgJCMZY+s5HNAj74aTV273N2Dd5UA/B3au/8yihHWG1DCuvIJgBhlyuebymA\n0mbnBu0CgYEA+vqR1/6sUbjvIe0Q5hKIzgmQTB5JBhMdVhN9jjkWbzmmHg14X8bx\nMnyomOOOYMs2BL/XnnYELdvLXbNsdfn3z7YLnRRWJYz5BeZKo0cziJ1qAU/GQivl\n+yUQDMWAI+tOCRO2eWU4H5pfWinYgb/Fjnk1e6WU1EEUJI4oE6nC5MsCgYEA6CeF\nKph4579OrjM5aEeOa+v/cHbWen3sw8tYuX5YIUsceJkwnlr7vdgwxfWjWHpQ1vA7\nJTHpTiiU9NBYyIw3RJTCC45+nZB0Wozco6Iimscggceupd8ewbAtk/hAAjWXp7y5\n6+PrqLrML4KGqEJS3CDyOx/NKFW3vXkJjq8FJtsCgYEA5rCQg7fsDkXtUALGqKNa\nqf+yabTgrDu/mFHb83FXxK55mWAKSAblxuE8WyO2yBOhOGZZu6aAmuJPoHX+eMZl\n2L9dF2oM8QEOGDUgX8pffPAr8r6v3jzZbKoZgZO7/8gWd1NuQ1EdcDcF9CtIfaKW\n5SlWVqvRC/QxnpQoFELTCFcCgYEA5HPfi6c3c3bDCpHF8GRaNsGqQRXwweGhWJuG\n2CMIvtqXTeYR/gMysANLG8M51xum6ZzF1zhiilNNIgzVEaVJzedFfPHgj1VT6rer\neCtZOk6yIoRJzVjff2LLt00YUBRFBP+nRgaoJQaNYENmF7YMrCqPtLb6wLJ5ea7e\nRNbejvkCgYEA1nMBDVtFbGyfWtmBB12ip5Xj76DhMvjsTc72cE8aKlDPHMYg7t2B\nSaeks8I20f+yRBWBQW9VdyAflTfZBCSM1fdFGj1M14bTr+ikNT4TNdBmXDbS//3t\nlOoomQDl08DhtkH/wSZrwJKgnNVCSPT+fLZzbTcmrsGkv6FdRthPjFY=\n-----END RSA PRIVATE KEY-----" + }, + "patient_id" : "T81lum-5p6QvDR7l6hv7lfE52bAbA2ylWBnv9CZEzNb0B" +} +``` + +### Proxy Scripts + +An example proxy script has been provided to test our a Patient.READ(DSTU2) API Call. Ideally, you should work this into your Conversation Flows using the *Call API* Widget + +```javascript +(async () => { + try { + const token = await fhirInterface().getToken(); + + let config = { + method: "get", + url: `${fhirSettings.base_url}/DSTU2/Patient/${fhirSettings.patient_id}`, + headers: { + "Accept" : "application/json", + "Authorization" : `${token.token_type} ${token.access_token}` + } + }; + + const result = await axios(config); + res.json(result.data); + } catch (err) { + if( err.response ){ + res.status(err.response.status).json(err.response.data); + } else { + res.status(400).json(JSON.stringify(err)); + } + } +})(); +``` diff --git a/screenshots/Example FHIR App.png b/screenshots/Example FHIR App.png new file mode 100644 index 0000000..8dd98ae Binary files /dev/null and b/screenshots/Example FHIR App.png differ