Te Whatu Ora, Central Region Integration Hub
1.0.10-rc1 - ci-build
Te Whatu Ora, Central Region Integration Hub, published by Te Whatu Ora, Te Pae Hauora o Ruahine o Tararua, MidCentral. This guide is not an authorized publication; it is the continuous build for version 1.0.10-rc1 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/tewhatuora/centralRegion-integrationHub-ig/ and changes regularly. See the Directory of published versions
The one-off process for registering an API Client for the Central Region Integration Hub (iHub) is as follows:
Step 1 of the process involves generation of a set of RSA keys, giving both PrivateKey and matching PublicKey Certificate. This can be done with the following command:
openssl req -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out myIHubClient.crt -keyout myIHubClient.pvtKey -subj '/L=City/O=Company Name/OU=Developer/CN=email.address@example.co.nz'
Depending upon which Microsoft Authentication Library (MSAL) you use to
request access tokens, you may need a .pubKey
rather than .crt
file. the PublicKey can be extracted from the .crt
with the following OpenSSL command:
openssl x509 -in myIHUbClient.crt -noout -pubkey > myIHubClient.pubKey
At the completion of the API Client Provisioning process, MidCentral IT Support Team will return the following pieces of information to you:
Token | Description | DEV Environment Value |
---|---|---|
IdProvider UUID | UUID Identifying which iHub IdProvider is being used | 8407d85d-ed23-4577-91c2-92e22dafe8e5 |
IdProvider URL | URL to the iHub IdProvider being used | https://login.microsoftonline.com/8407d85d-ed23-4577-91c2-92e22dafe8e5/oauth2/v2.0/token |
Scope UUID | UUID Identifying which iHub environment [DEV, UAT, PROD] you are using | 3a7df794-61b6-4b09-b6fe-eb6f6e10021d |
Scope URI | Identifying which iHub environment [DEV, UAT, PROD] you are using | api://0e1840b7-b897-4573-b677-489731b1e319/.default |
ClientId UUID | Identies the newly registered iHub API Client | per client identifier |
Notes:
Depending upon which Microsoft Authentication Library (MSAL) you use to obtain an access token, you will need to use either IdProvider UUID or URL; and either Scope UUID or URI.
Scope UUID
and URI
for UAT and PROD environments will be provided as required.
Client UUID
s issued for DEV environment will not work on UAT or PROD environments, and vice-versa.
Once your API Client has been provisioned, you are then able to request access tokens from the IdProvider, and then use the access token to make FHIR API calls against the Central Region Integration Hub (iHub):
Our IdProvider returns a base64 encoded string, which is a JWT; you can inspect the contents of your JWTs at jwt.io. The access token expires after 30 minutes; when this happens, your software can repeat steps 1 through 6 to obtain another.
Once you have an access token you can authenticate multiple FHIR API calls by placing the access token into the Authorization
header with Bearer *access_token*
Sample code is available for the following languages, mostly using the Microsoft Authentication Library (MSAL) library.
Depending upon which technology you are using, you will need the following:
UUID
or URL
address of the the IdProvider we're using.UUID
or URI
to indicate which Integration Hub environment [DEV, UAT, or PROD] you will be accessing.UUID
value issued to you as part of the API Client Provisioning ProcessprivateKey
Your generated private key for signing the access token request.
either
base64
or hex
encoding depending upon the authentication library you’re using orThe following code samples do the following:
This cURL
& bash
example is a bash
script that makes use of cURL
, openSSL
, jq
, base64
, xxd
and sed
to assemble and sign the JWT token, makes the access request, and finally displays the access token. jq
is a lightweight and flexible command line JSON processor.
#!/bin/bash
# Inspired by implementation by Will Haley at:
# http://willhaley.com/blog/generate-jwt-with-bash/
# Pre-requisites
# - openssl - Linux or git bash on Windows
# - jq - from https://stedolan.github.io/jq/
# - base64 - Linux or git for Windows
# - xxd - Linux or git for Windows
# - sed - Linux or git for Windows
#
## We're using MidCentral's IdProvider and DEV Integration Hub environment
idProvider="https://login.microsoftonline.com/8407d85d-ed23-4577-91c2-92e22dafe8e5/oauth2/v2.0/token"
scope="api://0e1840b7-b897-4573-b677-489731b1e319/.default"
#
## API clientId, Certificate & pvtKey
myClientId="a UUID goes here"
myCrtFile="${HOME}/.ssh/myIHubClient.crt"
myPvtKeyFile="${HOME}/.ssh/myIHubClient.pvtKey" ## Obviously this should be more secure
# errors in pipelines please
set -o pipefail
#
## We will need a UUID
uuid()
{
local N B C='89ab'
for (( N=0; N < 16; ++N ))
do
B=$(( $RANDOM%256 )) # A random number in range 0 - 255
case $N in
6)
printf '4%x' $(( B%16 )) # '4' plus a random HEX digit
;;
8)
printf '%c%x' ${C:$RANDOM%4:1} $(( B%16 )) # A random char from $C plus random HEX digit
;;
3 | 5 | 7 | 9)
printf '%02x-' $B # 2 random HEX digits plus '-'
;;
*)
printf '%02x' $B # 2 random HEX digits
;;
esac
done
echo
}
# JWT has a header...
header_template='{
"typ": "JWT",
"alg": "RS256",
"x5t": "thumbnail"
}'
#
## Use JQ to fill in the template...
build_header() {
local thumbnail=$1
jq -c --arg x5t "$thumbnail" '.x5t = $x5t' <<<"$header_template" | tr -d '\n'
}
# JWT also has a set of claims...
claims_template='{
"iss": "issuer",
"sub": "subject",
"aud": "audience",
"jti": "v4uuid",
"exp": "expiry",
"nbf": "notbefore"
}'
#
## Use JQ to fill in the template...
build_claims() {
local clientId=$1
jq -c \
--arg iss "$clientId" \
--arg aud "$idProvider" \
--arg uuid $(uuid) \
--arg iat_str "$(date +%s)" \
'
($iat_str | tonumber) as $iat
| .iss = $iss
| .sub = $iss
| .aud = $aud
| .jti = $uuid
| .nbf = $iat
| .exp = ($iat + 540)
' <<<"$claims_template" | tr -d '\n'
}
## rfc-4648 - URL safe version of BASE64
## '_'+ and '/' are switched to '-' and '_'; remove (optional) padding '='
b64enc() { base64 | tr '+/' '-_' | tr -d '='; }
b64dec() { base64 --decode; }
##
## Use JQ to prettify json; remove newlines
json() { jq -c . | LC_CTYPE=C tr -d '\n'; }
## Let openssl do all the signing work...
rs_sign() { openssl dgst -binary -sha256 -sign $1; }
#
## Assemble (and sign) JWT assertion
assembleJWT() {
local header payload sig cert=$1 pvtKey=$2 clientId=$3
## Gotta get the thumbnail from the publicKey....in base64
thumbnail_hex=$(openssl x509 -noout -fingerprint -sha1 -in $cert | sed -e 's/://g' -e 's/.*=//')
thumbnail_b64=$(xxd -r -p <<<$thumbnail_hex | b64enc)
## Assemble build header & payload
header=$(build_header $thumbnail_b64)
payload=$(build_claims $clientId)
## Glue them together and BASE64 encode
signed_content="$(json <<<"$header" | b64enc).$(json <<<"$payload" | b64enc)"
## Get signature with privateKey
sig=$(printf %s "$signed_content" | rs_sign $pvtKey | b64enc )
## Here's the JWT - with Certificate Signed Assertion
printf '%s.%s\n' "${signed_content}" "${sig}"
}
#
## Put a JWT with Certificate Signed Assertion together; and make request for accessToken...
getToken() {
local crtFile=$1 pvtKeyFile=$2 clientId=$3
jwt=$(assembleJWT $crtFile $pvtKeyFile $clientId)
# echo "Client Assertion:"
# echo $jwt
curl -s -X POST "$idProvider" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer\
&client_assertion=$jwt\
&client_id=$clientId\
&scope=$scope\
&grant_type=client_credentials" | jq -r .access_token
}
# go get accessToken
accessToken=$(getToken $myCrtFile $myPvtKeyFile $myClientId)
echo "Access Token:"
echo $accessToken
This javascript example lets the MSAL do all the hard work; you need to have the following environment variables set:
.pvtKey
file..crt
file in hex
format// *
// ** Grab the relevant Microsoft Authentication Library
// *
const msal = require('@azure/msal-node');
const AuthToken = require('@azure/msal-common').AuthToken;
const request = require('request'); // We're going to make an HTTP Request to mockServer...
process.env.HTTP_PROXY=""; // Tell the proxy to go away
// *
// ** We need five things to use MSAL ...
// *
const idProvider="https://login.microsoftonline.com/8407d85d-ed23-4577-91c2-92e22dafe8e5/oauth2/v2.0/token"
const devScope="api://0e1840b7-b897-4573-b677-489731b1e319/.default"
const pvtKey = process.env['IHUB_PVT_KEY']?.replace(/\n\s+/g, "\n"); // The contents of the .pvtFile not the file location
const crtThumbnail = process.env['IHUB_CERT_THUMBNAIL']; // in HEX rather than BASE64
const clientId = process.env['IHUB_CLIENT_ID'];
// *
// ** Use MSAL to go get an accessToken
// *
const msalConfig = {
auth: {
clientId: clientId,
authority: idProvider,
clientCertificate: {
thumbprint: crtThumbnail,
privateKey: pvtKey
}
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Info,
}
}
};
// Create msal application object
const cca = new msal.ConfidentialClientApplication(msalConfig);
// We're making a request for a particular scope...
const clientCredentialRequest = {
scopes: [devScope],
skipCache: true
};
console.log("Retrieving access token from IdProvider....");
cca.acquireTokenByClientCredential(clientCredentialRequest).then(response => {
console.log("iHub IdProvider Response: ", response);
// Retrieve the acces token...
let accessToken = response.accessToken;
// ...and now we have the access token
console.log(accessToken);
});
This python example lets the MSAL do all the hard work; you need to have the following environment variables set:
.pvtKey
file..crt
file in hex
format#!/usr/bin/env python
## Pre-requisites
## pip install msal
## pip install pyjwt
## pip install requests
from msal import ConfidentialClientApplication
import requests
import jwt
import json
import sys
import os
import re
## We've got utf-8 strings in our response JSON
sys.stdout.reconfigure(encoding='utf-8')
##
## We need five things to use MSAL ...
##
const idProvider="https://login.microsoftonline.com/8407d85d-ed23-4577-91c2-92e22dafe8e5/oauth2/v2.0/token"
const devScope="api://0e1840b7-b897-4573-b677-489731b1e319/.default"
pvtKey = re.sub('\n\s+', '\n', os.environ['IHUB_PVT_KEY']) ## The contents of the .pvtFile not the file location
crtThumbnail = os.environ['IHUB_CERT_THUMBNAIL'] ## in HEX rather than BASE64
clientId = os.environ['IHUB_CLIENT_ID']
## Setup the MSAL application
cca = ConfidentialClientApplication(
clientId,
authority=idProvider,
client_credential={
"thumbprint": crtThumbnail,
"private_key": pvtKey,
"scope": devScope,
"grant_type": "client_credentials"
}
)
#
## Go get the AccessToken
result = cca.acquire_token_for_client(scopes=[devScope])
#
## Make sure that worked
if not "access_token" in result:
print(result.get("error"))
print(result.get("error_description"))
print(result.get("correlation_id"))
exit(1)
#
## unpick the access token
accessToken = result["access_token"]
print(accessToken)
This C# .Net example lets the MSAL do all the hard work; you need to have the following pre-requisites in place:
.crt
~and .pvtKey` files need to be availableusing Microsoft.Identity.Client;
using System.Security.Cryptography.X509Certificates;
using Newtonsoft.Json;
using System;
using Newtonsoft.Json.Linq;
class Program
{
public static async Task Main(string[] args)
{
// We're using MidCentral's Azure AD...
var tenantId = "8407d85d-ed23-4577-91c2-92e22dafe8e5";
// My client and certificate details...
var clientId = "your client UUID";
var myCRTFile = "location of your .crt file";
var myKeyFile = "location of your .pvtkey file";
// Assemble CRT & pvtKey into useable certificate
X509Certificate2 myCert = X509Certificate2.CreateFromPemFile(myCRTFile, myKeyFile);
// We want to use DEV instance of IHub
var scopes = new String[] { "api://0e1840b7-b897-4573-b677-489731b1e319/.default" };
// Build CCA
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithCertificate(myCert)
.WithTenantId(tenantId)
.Build();
// Go get the accessToken
AuthenticationResult tokenResult = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
// Let's make an HTTP request ....
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + tokenResult.AccessToken); // put the accessToken in place
client.DefaultRequestHeaders.Add("User-Agent", "C# Testing Client"); // WAF won't let request through without a User-Agent header...
var url = "https://test-smilecdr.mdhb.health.nz:8000/Patient?_summary=count"; // Count the no of Patient records
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
// Grab the response (as a JSON string)
var responseContent = await response.Content.ReadAsStringAsync();
// Turn response into an object with named properties
JObject result = (JObject)JsonConvert.DeserializeObject(responseContent);
Console.WriteLine("There are {0} Patients", result.GetValue("total"));
}
else
{
Console.WriteLine("Failed - " + response.ToString());
}
}
}
This Java example lets the MSAL do all the hard work; you need to have the following pre-requisites in place:
.crt
and .pvtKey
files need to be available// Borrowed from MSAL Java examples
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
/* You'll need some dependencies in a pom.xml file
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>msal4j</artifactId>
<version>1.10.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
*/
import com.google.gson.Gson;
import com.google.gson.internal.LinkedTreeMap;
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IAuthenticationResult;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import org.apache.commons.codec.binary.Base64;
class ClientCredentialGrant {
private static String authority;
private static String clientId;
private static String scope;
private static String keyPath;
private static String certPath;
private static ConfidentialClientApplication app;
public static void main(String args[]) throws Exception {
setUpSampleData();
try {
BuildConfidentialClientObject();
IAuthenticationResult result = getAccessTokenByClientCredentialGrant();
System.out.println("AccessToken: " + result.accessToken());
} catch (Exception ex) {
System.out.println("Oops! We have an exception of type - " + ex.getClass());
System.out.println("Exception message - " + ex.getMessage());
throw ex;
}
}
private static void BuildConfidentialClientObject() throws Exception {
// Uh Oh ... the library doesn't like our .pvtKey file
// PKCS8EncodedKeySpec(Files.readAllBytes(Paths.get(keyPath)));
// So we'll do it ourselves...
String keyStr = new String(Files.readAllBytes(Paths.get(keyPath)), Charset.defaultCharset())
.replace("-----BEGIN PRIVATE KEY-----", "")
.replaceAll(System.lineSeparator(), "")
.replace("-----END PRIVATE KEY-----", "");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(keyStr));
PrivateKey key = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
// But the .crt file is okay...
InputStream certStream = new ByteArrayInputStream(Files.readAllBytes(Paths.get(certPath)));
X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(certStream);
app = ConfidentialClientApplication.builder(
clientId,
ClientCredentialFactory.createFromCertificate(key, cert))
.authority(authority)
.build();
}
private static IAuthenticationResult getAccessTokenByClientCredentialGrant() throws Exception {
// With client credentials flows the scope is ALWAYS of the shape
// "resource/.default", as the
// application permissions need to be set statically (in the portal), and then
// granted by a tenant administrator
ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder(
Collections.singleton(scope))
.build();
CompletableFuture<IAuthenticationResult> future = app.acquireToken(clientCredentialParam);
return future.get();
}
/**
* Helper function unique to this sample setting. In a real application these
* wouldn't be so hardcoded, for example
* different users may need different authority endpoints and the key/cert paths
* could come from a secure keyvault
*/
private static void setUpSampleData() throws IOException {
// Load properties file and set properties used throughout the sample
Properties properties = new Properties();
properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("application.properties"));
authority = properties.getProperty("AUTHORITY");
clientId = properties.getProperty("CLIENT_ID");
keyPath = properties.getProperty("KEY_PATH");
certPath = properties.getProperty("CERT_PATH");
scope = properties.getProperty("SCOPE");
}
}
During the provisioning process, each API Client will be assigned one or more API Access roles that governs which API endpoints they will have access to. The roles are:
Role | Access Notes |
---|---|
Patient.ReadAll | Grants read-only access to Patient, Flag, AllergyIntolerance, ClinicalImpression and Practitioner FHIR resources. |
ServiceRequest.ReadAll | Grants read-only access to ServiceRequest FHIR resources |
Subscription.ReadWrite | Grants read/write access to Subscription FHIR resources that the client has made |
Note: all roles also grant read-only
access to CodeSystem, CodeValue and ConceptMap resources