Add support for SMART on FHIR to HAPI FHIR with APISIX and Keycloak
Introduction
In a previous post, I wrote about the steps I followed to add Authentication (AuthN) to HAPI FHIR by utilising APISIX and Keycloak.
In this post we are going to look at adding support for SMART on FHIR to HAPI FHIR.
SMART on FHIR
SMART on FHIR (Substitutable Medical Applications and Reusable Technologies on FHIR) is a healthcare standard that promotes interoperability between client applications and FHIR-enabled systems.
SMART on FHIR Scopes
SMART on FHIR defines OAuth 2.0 scopes that allow client applications to request a specific set of access rights. The client conveys this information to the authorization server in the form of a 'scope' request parameter.
For example:
system/Patient.read
system/Patient.write
APISIX
OpenID Connect Plugin
We can configure the OpenID Connect plugin's required_scopes
attribute to require one or more scopes.
For example:
...
- name: hapi-fhir-api
uri: /fhir/Patient*
methods: [ "GET" ]
upstream_id: 1
plugins:
openid-connect:
bearer_only: true
client_id: ${{CLIENT_ID}}
client_secret: ${{CLIENT_SECRET}}
discovery: ${{PROTOCOL}}://${{KEYCLOAK_HOSTNAME}}:${{KEYCLOAK_PORT}}/realms/${{KEYCLOAK_REALM}}/.well-known/openid-configuration
required_scopes: [ "system/Patient.read" ]
APISIX will call the token introspection endpoint and check that the Access Token includes the required scopes.
Keycloak
Request a token
To access the HAPI FHIR API, you must first request an access token. You will need to POST to the token URL.
For example (scope=system/Patient.read
):
ACCESS_TOKEN=$(curl -s -X POST https://keycloak.au.localhost:8443/realms/hapi-fhir-dev/protocol/openid-connect/token \
-H 'content-type: application/x-www-form-urlencoded' \
-d grant_type=client_credentials \
-d client_id=oauth2-proxy \
-d client_secret=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM \
-d scope=system/Patient.read | (jq -r '.access_token'))
# echo "$ACCESS_TOKEN"
Note: You can use jwt.io to decode the access token.
Introspect a token
To introspect an Access Token you will need to POST to the introspect URL.
For example:
curl -X POST "https://keycloak.au.localhost:8443/realms/hapi-fhir-dev/protocol/openid-connect/token/introspect" \
-H 'content-type: application/x-www-form-urlencoded' \
-d client_id=oauth2-proxy \
-d client_secret=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM \
-d "token_type_hint=access_token&token=$ACCESS_TOKEN"
Client Scopes
In Keycloak, an OAuth 2.0 scope is mapped to a client scope.
HAPI FHIR
Call the API
To call the HAPI FHIR API, a client application must pass an access token as a Bearer token in the Authorization header of your HTTP request.
For example:
curl -X GET https://hapi-fhir.au.localhost/fhir/Patient?_id=baratz-toni \
-H 'Content-Type: application/fhir+json' \
-H "Authorization: Bearer $ACCESS_TOKEN"
You should see something like:
{
"resourceType": "Bundle",
"id": "9d80c83a-0b06-4b78-bce2-21e6666348d8",
"meta": {
"lastUpdated": "2025-05-23T05:43:01.959+00:00"
},
"type": "searchset",
"total": 1,
"link": [ {
"relation": "self",
"url": "https://hapi-fhir.au.localhost/fhir/Patient?_id=baratz-toni"
} ],
"entry": [ {
"fullUrl": "https://hapi-fhir.au.localhost/fhir/Patient/baratz-toni",
"resource": {
"resourceType": "Patient",
"id": "baratz-toni",
"meta": {
"versionId": "1",
"lastUpdated": "2025-05-23T05:42:36.551+00:00",
"source": "#rKTCeZfmnReSjm8X",
"profile": [ "http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient" ]
},
"extension": [ {
"url": "http://hl7.org.au/fhir/StructureDefinition/indigenous-status",
"valueCoding": {
"system": "https://healthterminologies.gov.au/fhir/CodeSystem/australian-indigenous-status-1",
"code": "1",
"display": "Aboriginal but not Torres Strait Islander origin"
}
}, {
"url": "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity",
"extension": [ {
"url": "value",
"valueCodeableConcept": {
"coding": [ {
"system": "http://snomed.info/sct",
"code": "446141000124107",
"display": "Identifies as female gender"
} ]
}
} ]
}, {
"url": "http://hl7.org/fhir/StructureDefinition/individual-pronouns",
"extension": [ {
"url": "value",
"valueCodeableConcept": {
"coding": [ {
"system": "http://loinc.org",
"code": "LA29519-8",
"display": "she/her/her/hers/herself"
} ]
}
} ]
}, {
"url": "http://hl7.org/fhir/StructureDefinition/individual-recordedSexOrGender",
"extension": [ {
"url": "type",
"valueCodeableConcept": {
"coding": [ {
"system": "http://snomed.info/sct",
"code": "1515311000168102",
"display": "Biological sex at birth"
} ]
}
}, {
"url": "value",
"valueCodeableConcept": {
"coding": [ {
"system": "http://snomed.info/sct",
"code": "248152002",
"display": "Female"
} ]
}
} ]
} ],
"identifier": [ {
"extension": [ {
"url": "http://hl7.org.au/fhir/StructureDefinition/ihi-status",
"valueCoding": {
"system": "https://healthterminologies.gov.au/fhir/CodeSystem/ihi-status-1",
"code": "active"
}
}, {
"url": "http://hl7.org.au/fhir/StructureDefinition/ihi-record-status",
"valueCoding": {
"system": "https://healthterminologies.gov.au/fhir/CodeSystem/ihi-record-status-1",
"code": "verified",
"display": "verified"
}
} ],
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "NI"
} ],
"text": "IHI"
},
"system": "http://ns.electronichealth.net.au/id/hi/ihi/1.0",
"value": "8003608000311662"
}, {
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MC"
} ],
"text": "Medicare Number"
},
"system": "http://ns.electronichealth.net.au/id/medicare-number",
"value": "69518252411"
} ],
"name": [ {
"use": "official",
"family": "BARATZ",
"given": [ "Toni" ]
} ],
"telecom": [ {
"system": "phone",
"value": "0870101270",
"use": "home"
}, {
"system": "phone",
"value": "0491570156",
"use": "mobile"
}, {
"system": "phone",
"value": "0870108006",
"use": "work"
} ],
"gender": "female",
"birthDate": "1978-06-16",
"address": [ {
"line": [ "24 Law Cir" ],
"city": "Bassendean",
"state": "WA",
"postalCode": "6054",
"country": "AU"
} ]
},
"search": {
"mode": "match"
}
} ]
}
Source Code
What's Next
In the next post, we'll take a look at APISIX's authz-keycloak
plugin.
References
System Hardening
- Australian Signals Directorate: Implementing Certificates, TLS, HTTPS and Opportunistic TLS
- Cloudflare docs: Cipher suites recommendations
HL7
- HL7: Implementation Guide
- HL7: FHIR NPM Packages
- AU Core: Publication (Version) History
- AU Core FHIR Implementation Guide: AU Core - 1.0.0-preview
- AU Core FHIR Implementation Guide: Testing FAQs
- Sparked AU Core Test Data Postman collection
SMART on FHIR
- HL7: SMART App Launch
- SMART Health IT: SMART on FHIR
SMART on FHIR - Standalone Launch
- Project Alvearie: SMART App Launch
- Project Alvearie: Keycloak extensions for FHIR
- Keycloak extensions for FHIR: Upgrade to the Quarkus-based distribution
- Keycloak discussion: Fine grained scope consent management
SMART on FHIR - EHR Launch
Keycloak
- Keycloak docs: Configuring Keycloak for production
- Keycloak docs: Configuring TLS
- Keycloak docs: Configuring trusted certificates
- Keycloak docs: Configuring the hostname
- Keycloak docs: Using a reverse proxy
- Keycloak docs: Running Keycloak in a container
- Keycloak docs: Migrating to the Quarkus distribution
- Keycloak docs: Upgrading Guide - 26.1.0
- Keycloak docs: Authorization Services Guide
Keycloak-based Development
- GitHub: Keycloak Project Example
- GitHub: Awesome Keycloak
Keycloak Support
- Google Group: Keycloak User
- Google Group: Keycloak Dev
APISIX
- APISIX docs: Deployment modes
- APISIX docs: SSL Protocol
- APISIX docs: Certificate
- APISIX docs: Plugins - OpenID Connect
HAPI FHIR
- HAPI FHIR: Website
- HAPI FHIR: Documentation
- Google Group: HAPI FHIR