OZO FHIR implementation guide
0.1.0 - ci-build
OZO FHIR implementation guide - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
baseUrl, from internal NUTS url: example https://nuts-node-int.example.comsubject, the subject representing the OZO system, for example ozo-connect.async function _createSubject(baseUrl: string, subject: string) {
let url = `${baseUrl}/internal/vdr/v2/subject`;
const data = {
'subject': subject
}
let resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data)
})
if (resp.ok) {
return await resp.json()
}
}
{
"documents": [
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json"
],
"assertionMethod": [
"did:web:nuts-node.example.com:iam:5db756b2-ce83-4dcd-bf07-f0c7c2271576#0c9f6bce-d70e-449f-bfbf-4a4b4aa5a316"
],
"authentication": [
"did:web:nuts-node.example.com:iam:5db756b2-ce83-4dcd-bf07-f0c7c2271576#0c9f6bce-d70e-449f-bfbf-4a4b4aa5a316"
],
"capabilityDelegation": [
"did:web:nuts-node.example.com:iam:5db756b2-ce83-4dcd-bf07-f0c7c2271576#0c9f6bce-d70e-449f-bfbf-4a4b4aa5a316"
],
"capabilityInvocation": [
"did:web:nuts-node.example.com:iam:5db756b2-ce83-4dcd-bf07-f0c7c2271576#0c9f6bce-d70e-449f-bfbf-4a4b4aa5a316"
],
"id": "did:web:nuts-node.example.com:iam:5db756b2-ce83-4dcd-bf07-f0c7c2271576",
"verificationMethod": [
{
"controller": "did:web:nuts-node.example.com:iam:5db756b2-ce83-4dcd-bf07-f0c7c2271576",
"id": "did:web:nuts-node.example.com:iam:5db756b2-ce83-4dcd-bf07-f0c7c2271576#0c9f6bce-d70e-449f-bfbf-4a4b4aa5a316",
"publicKeyJwk": {
"crv": "P-256",
"kty": "EC",
"x": "Yg6NXofe3qjdjssmWbTAtzce97JZu60T5DsGyKnezyc",
"y": "AikUTHwezwd2EOp8BqiuiCAdADplVWIW5CYl8Ls25N0"
},
"type": "JsonWebKey2020"
}
]
}
],
"subject": "ozo-test"
}
baseUrl, from internal NUTS url: example https://nuts-node-int.example.com
*subject, the subject representing the OZO system, for example ozo-connect.async function _fetchDid(baseUrl: string, subject: string) {
url = `${baseUrl}/internal/vdr/v2/subject/${subject}`
resp = await fetch(url)
if (resp.ok) {
let dids = await resp.json() as Array<string>;
for (const did of dids) {
if (did.startsWith('did:web:')) {
return did
}
}
console.error("Failed to find a did web")
} else {
console.error("Failed to get did", resp.statusText)
}
}
[
"did:web:nuts-node.ozo-pen.headease.nl:iam:5db756b2-ce83-4dcd-bf07-f0c7c2271576"
]
OzoSystemCredential and load it into the walletbaseUrl, from internal NUTS url: example https://nuts-node-int.example.comsubject, the subject representing the OZO system, for example ozo-connectozo.export async function selfIssue(baseUrl: string, subject: string) {
const own_did = _fetchDid(baseUrl, subject);
const issuer_did = _getOrCreateDid(baseUrl, "ozo");
let url = `${baseUrl}/internal/vcr/v2/issuer/vc`;
const data = {
"type": "OzoSystemCredential",
"issuer": issuer_did,
"credentialSubject": {
"id": own_did
},
"expirationDate": "2025-01-01T00:00:00Z",
"format": "ldp_vc"
}
const credential = await fetch(url, {
method: "POST",
cache: "no-store",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then((data) => data.json())
url = `${baseUrl}/internal/vcr/v2/holder/${subject}/vc`
await fetch(url, {
method: "POST",
cache: "no-store",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(credential),
})
}
/**
* Helper function to create the subject if missing.
* @param subject
* @param internalBaseUrl
*/
async function _getOrCreateDid(subject: string, internalBaseUrl: string) {
let did: string = ''
did = await _fetchDid(subject, internalBaseUrl, agent)
if (did === '') {
await _createSubject(internalBaseUrl, subject);
did = await _fetchDid(subject, internalBaseUrl)
}
return did
}
access_tokenbaseUrl, from internal NUTS url: example https://nuts-node-int.example.comsubject, the subject representing the OZO system, for example ozo-connect.token_type, the token type can be either Bearer or DPoP. DPoP is highly preferred and token type Bearer might become deprecated as soon as all parties have implemented DPoP.A JSON map with the access_token as access token and, in case of token type the field dpop_kid is also returned.
export async function getAccessToken(baseUrl: string, subject:string) {
const authorization_server = `https://nuts-node.example.com/oauth2/ozo`
const url = `${baseUrl}/internal/auth/v2/${subject}/request-service-access-token`;
const data = {
"authorization_server": authorization_server,
"scope": "ozo",
"token_type" : 'DPoP' // "Bearer" if skipping DPoP
}
return await fetch(url, {
method: "POST",
cache: "no-store",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then((data) => data.json())
}
access_token as access tokendpop_kid if the token_type is DPoP, used for requesting the DPoP headerexpires_in depicting the validity of the token.token_type, either Bearer of DPoP.{
"access_token": "Gcmjdz....tYlWs",
"dpop_kid": "did:web:nuts-node.example.com:iam:b4ca905f-a8b3-450f-93ad-9e7f9fb400f7#e740976c-1b24-4cd8-8a9f-679793f93bea",
"expires_in": 900,
"token_type": "DPoP",
"scope": "ozo_internal"
}
DPoP tokenThe DPoP header ensures that the same public/private key pair used in requesting the access_token is associated with the key pair used in using the access_token. The access_token can be used for multiple requests as long as it is valid, a dpop header has to be requested for each individual request. Each request needs to be signed by the private key, making sure that the access_token cannot be used by anyone other than the owner of the key pair, adding an extra layer of security. The signature method takes as input the URL and request method.
Fortunately, the NUTS node takes care of most of the complexity in getting the DPoP header, the NUTS client just needs to call the NUTS internal endpoint to fetch the DPoP header.
The following example code fetches the header:
baseUrl, from internal NUTS url: example https://nuts-node-int.example.comdpop_kid: the dpop_kid from the access token response.access_token: the access_token from the token response.export async function getDpopHeader(baseUrl: string, dpop_kid: string, token: string, requestMethod: string, requestUrl: string) : Promise<{ dpop: string }> {
const url = `${baseUrl}/internal/auth/v2/dpop/${encodeURIComponent(dpop_kid)}`;
const data = {
"htm": requestMethod,
"htu": requestUrl,
"token": token
}
return await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
agent: createNutsAgent(),
})
.then((data) => data.json() as any)
}
dpop as dpop token.{
"dpop": "eyJhbG...JVDQ"
}
The API request can be done with the access_token as follows:
access_token, the access token as requested above.dpop_header, the dpop header as requested above.GET /fhir/Patient HTTP/1.1
Authorization: DPoP {access_token}
DPoP: {dpop_header}
Host: proxy.example.com