FHIR Programming using Java and HAPI FHIR Server - Advanced Topics (including Security)
Introduction
Welcome to the continuation of my series on FHIR Programming using Java and HAPI FHIR Server. In previous articles, we've covered setting up an environment, creating, updating, deleting, searching, validating, and chaining FHIR operations. In this article, we'll delve into advanced topics such as security, auditing, and transaction management to help you build robust and secure healthcare applications.
Understanding the FHIR Security Model
Before implementing security features, it's essential to understand FHIR's comprehensive security framework and the concepts that underpin secure healthcare data exchange.
Authentication vs Authorization
These two concepts are often confused but serve distinct purposes:
Authentication - Verifying the identity of the user or system:
- "Who are you?"
- Typically handled via OAuth2, SAML, or OpenID Connect
- Results in a verified identity (user, application, or system)
- Does not determine what actions are permitted
Authorization - Determining what the authenticated entity can do:
- "What are you allowed to do?"
- Based on roles, permissions, patient relationships, or other criteria
- In FHIR, often implemented via SMART scopes
- Controls access to specific resources and operations
SMART on FHIR Scopes
SMART on FHIR defines a standardized set of OAuth2 scopes that precisely control access to FHIR resources:
Scope Format: [context]/[resource].[permission]
Context Types:
patient/- Access limited to data for the specific patient in contextuser/- Access based on the user's permissions across patientssystem/- Backend service access (no user context)
Permission Types:
.read- Read access to the resource type.write- Create/Update/Delete access.*- Full access (read and write)
Examples:
patient/Observation.read- Read observations for the patient in contextuser/Patient.*- Full access to Patient resources based on user permissionssystem/MedicationRequest.read- Backend service reading medication requestspatient/*.read- Read all resource types for the patient in context
Launch Context
SMART on FHIR defines two launch contexts for applications:
EHR Launch - Application is launched from within an EHR:
- EHR passes a launch parameter to the app
- Patient and/or encounter context is provided by the EHR
- App requests
launchscope during authorization - Seamless integration with clinical workflow
Standalone Launch - Application runs independently:
- App initiates the authorization flow directly
- User may need to select a patient
- App requests
launch/patientscope to get patient selection UI - Suitable for patient-facing apps or administrative tools
AuditEvent Resource and ATNA Compatibility
FHIR's AuditEvent resource is designed to be compatible with the IHE ATNA (Audit Trail and Node Authentication) profile, enabling interoperability with existing healthcare audit infrastructure:
Key AuditEvent Elements:
- type - Categorizes the event (e.g., "rest" for RESTful operations)
- subtype - More specific event type (e.g., "create", "read", "update")
- action - CRUD action: C (Create), R (Read), U (Update), D (Delete), E (Execute)
- recorded - When the event was logged
- outcome - Success (0) or various failure codes
- agent - Who/what performed the action (user, system, device)
- source - The system that generated the audit event
- entity - What resources were accessed or modified
This structure maps directly to ATNA DICOM audit messages, allowing FHIR audit events to be consumed by existing healthcare audit repositories.
Provenance: Tracking Data Lineage
While AuditEvent tracks system access, Provenance tracks the origin and history of clinical data:
- target - The resource(s) the provenance relates to
- recorded - When the provenance was recorded
- activity - What activity occurred (create, revise, transform)
- agent - Who was involved in creating/modifying the data
- entity - What the data was derived from (source documents, etc.)
// Creating a Provenance record for data created from a CDA document
Provenance provenance = new Provenance();
provenance.addTarget(new Reference("Observation/123"));
provenance.setRecorded(new Date());
// Activity: derived from a document
provenance.setActivity(new CodeableConcept().addCoding(
new Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/v3-DataOperation")
.setCode("CREATE")));
// Agent: the practitioner who created the data
Provenance.ProvenanceAgentComponent agent = provenance.addAgent();
agent.setWho(new Reference("Practitioner/456"));
agent.setType(new CodeableConcept().addCoding(
new Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/provenance-participant-type")
.setCode("author")));
// Source entity: the CDA document
Provenance.ProvenanceEntityComponent entity = provenance.addEntity();
entity.setRole(Provenance.ProvenanceEntityRole.SOURCE);
entity.setWhat(new Reference("DocumentReference/789"));
Provenance is essential for clinical decision support, regulatory compliance, and data quality initiatives.
Consent Management
FHIR's Consent resource manages patient privacy preferences and data sharing agreements:
- status - active, inactive, rejected, entered-in-error
- scope - What the consent covers (treatment, research, privacy)
- category - Type of consent (e.g., HIPAA authorization)
- patient - Who the consent belongs to
- provision - Specific rules about what is permitted or denied
Consent Provisions can specify:
- Actors who can access data
- Specific data classes or resources covered
- Time periods for access
- Purposes for which data may be used
- Security labels that trigger the consent
Implementing consent enforcement requires server-side logic that evaluates consent resources when processing requests, which is typically done at the FHIR server level rather than in client code.
Prerequisites
Before diving into advanced topics, ensure your development environment is properly set up:
- Java Development Kit (JDK) installed and configured.
- Apache Maven installed and your project is set up.
- HAPI FHIR Server is running and accessible.
- You can find all the code demonstrated in this tutorial on GitHub here
“Time is a created thing. To say ‘I don’t have time’ is to say ‘I don’t want to.’” ~ Lao Tzu
Step 1 of 3: Implementing Security in FHIR
Security is a critical aspect of any healthcare application. FHIR, being a standard for healthcare data exchange, includes several mechanisms to ensure data security, including authentication, authorization, and encryption. HAPI FHIR provides built-in security features that integrate with various authentication systems.
Authentication and Authorization
HAPI FHIR supports OAuth2 for secure access control. This allows you to manage authentication and authorization using a robust identity management platform. Below is an example of how to configure OAuth2 authentication in your Java application:
package com.saravanansubramanian.fhir;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor;
import org.hl7.fhir.r4.model.Patient;
public class OAuth2Client {
public static void main(String[] args) {
// Initialize FHIR context
FhirContext ctx = FhirContext.forR4();
// Create a bearer token interceptor with your access token
String accessToken = "your-access-token";
BearerTokenAuthInterceptor authInterceptor = new BearerTokenAuthInterceptor(accessToken);
// Create a FHIR client
IGenericClient client = ctx.newRestfulGenericClient("https://your-fhir-server-url");
// Register the interceptor with the client
client.registerInterceptor(authInterceptor);
// Now you can use the authenticated client to access protected resources
Patient patient = client.read()
.resource(Patient.class)
.withId("123")
.execute();
System.out.println("Retrieved patient: " + patient.getNameFirstRep().getNameAsSingleString());
}
}
In this example, we create a BearerTokenAuthInterceptor with an OAuth2 access token and register it with the FHIR client. This interceptor adds the Authorization header with the Bearer token to each request, ensuring secure access to protected resources.
Data Encryption
Data encryption is crucial for protecting sensitive healthcare information. HAPI FHIR ensures that data is encrypted in transit by supporting HTTPS. Here's how you can configure your client to enforce HTTPS:
package com.saravanansubramanian.fhir;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
public class SecureClient {
public static void main(String[] args) throws Exception {
// Initialize FHIR context
FhirContext ctx = FhirContext.forR4();
ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
// Configure SSL context with a trusted keystore
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(new FileInputStream("path/to/truststore.jks"), "password".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
// Set the SSL context on the client factory
ctx.getRestfulClientFactory().setSocketFactory(sslContext.getSocketFactory());
// Create a FHIR client with HTTPS URL
IGenericClient client = ctx.newRestfulGenericClient("https://your-secure-fhir-server-url");
// Add a logging interceptor to see the requests and responses
LoggingInterceptor loggingInterceptor = new LoggingInterceptor();
loggingInterceptor.setLogRequestBody(true);
loggingInterceptor.setLogResponseBody(true);
client.registerInterceptor(loggingInterceptor);
// Now you can use the secure client to access resources
System.out.println("Client configured with secure connection.");
}
}
This code configures the FHIR client with a custom SSLContext that trusts specific certificates. This is particularly important when connecting to servers with self-signed certificates or when your organization uses its own certificate authority.
Step 2 of 3: Auditing and Logging
Auditing is essential in healthcare applications to track access and modifications to sensitive data. FHIR provides the AuditEvent resource specifically for this purpose, and HAPI FHIR makes it easy to create and manage audit logs.
Implementing Auditing
package com.saravanansubramanian.fhir;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.hl7.fhir.r4.model.*;
import java.util.Date;
public class AuditLogger {
public static void main(String[] args) {
// Initialize FHIR context
FhirContext ctx = FhirContext.forR4();
// Create a FHIR client
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");
// Create an AuditEvent resource
AuditEvent auditEvent = new AuditEvent();
// Set the timestamp
auditEvent.setRecorded(new Date());
// Set the outcome
auditEvent.setOutcome(AuditEvent.AuditEventOutcome._0);
// Set the action (C = Create, R = Read, U = Update, D = Delete)
auditEvent.setAction(AuditEvent.AuditEventAction.C);
// Set the type of event (AuditEvent.type is a Coding in R4)
auditEvent.setType(new Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/audit-event-type")
.setCode("rest")
.setDisplay("RESTful Operation"));
// Add agent information (who performed the action)
AuditEvent.AuditEventAgentComponent agent = auditEvent.addAgent();
agent.setWho(new Reference("Practitioner/123"));
agent.setRequestor(true);
// Add source information (the application that logged the event)
auditEvent.getSource().setObserver(new Reference("Device/system"));
// Add entity information (the resource that was acted upon)
AuditEvent.AuditEventEntityComponent entity = auditEvent.addEntity();
entity.setWhat(new Reference("Patient/456"));
// Set detailed type information (AuditEventEntity.type is a Coding in R4)
entity.setType(new Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/audit-entity-type")
.setCode("1")
.setDisplay("Person"));
// Send the AuditEvent to the FHIR server
ca.uhn.fhir.rest.api.MethodOutcome outcome = client.create()
.resource(auditEvent)
.execute();
System.out.println("Audit event logged with ID: " + outcome.getId().getIdPart());
}
}
This example creates an AuditEvent resource to log a creation action performed by a Practitioner on a Patient resource. The AuditEvent includes information about who performed the action, what action was performed, and which resource was affected.
For a more automated approach, you can implement an interceptor that automatically logs AuditEvents for each interaction with the FHIR server:
package com.saravanansubramanian.fhir;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import org.hl7.fhir.r4.model.*;
import java.util.Date;
public class AuditInterceptor implements IClientInterceptor {
private final IGenericClient auditClient;
private final String practitionerId;
// Store request info captured during interceptRequest
private final ThreadLocal<String> requestMethod = new ThreadLocal<>();
private final ThreadLocal<String> requestUri = new ThreadLocal<>();
public AuditInterceptor(FhirContext ctx, String fhirServerUrl, String practitionerId) {
this.auditClient = ctx.newRestfulGenericClient(fhirServerUrl);
this.practitionerId = practitionerId;
}
@Override
public void interceptRequest(IHttpRequest request) {
// Capture request information before it's sent
requestMethod.set(request.getHttpVerbName());
requestUri.set(request.getUri());
}
@Override
public void interceptResponse(IHttpResponse response) throws java.io.IOException {
// Get the captured request info
String method = requestMethod.get();
String uri = requestUri.get();
// Parse resource type and ID from URI (e.g., "http://server/fhir/Patient/123")
String resourceType = null;
String resourceId = null;
if (uri != null) {
String[] parts = uri.split("/");
for (int i = 0; i < parts.length - 1; i++) {
if (parts[i].matches("^[A-Z][a-zA-Z]+$")) { // Resource type pattern
resourceType = parts[i];
if (i + 1 < parts.length && !parts[i + 1].contains("?")) {
resourceId = parts[i + 1].split("\\?")[0];
}
break;
}
}
}
// Only log successful operations
if (response.getStatus() >= 200 && response.getStatus() < 300) {
// Create an AuditEvent
AuditEvent auditEvent = new AuditEvent();
auditEvent.setRecorded(new Date());
auditEvent.setOutcome(AuditEvent.AuditEventOutcome._0);
// Map HTTP method to FHIR action
if ("GET".equals(method)) {
auditEvent.setAction(AuditEvent.AuditEventAction.R);
} else if ("POST".equals(method)) {
auditEvent.setAction(AuditEvent.AuditEventAction.C);
} else if ("PUT".equals(method)) {
auditEvent.setAction(AuditEvent.AuditEventAction.U);
} else if ("DELETE".equals(method)) {
auditEvent.setAction(AuditEvent.AuditEventAction.D);
}
// Set the event type (AuditEvent.type is a Coding in R4)
auditEvent.setType(new Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/audit-event-type")
.setCode("rest")
.setDisplay("RESTful Operation"));
// Add agent information
AuditEvent.AuditEventAgentComponent agent = auditEvent.addAgent();
agent.setWho(new Reference("Practitioner/" + practitionerId));
agent.setRequestor(true);
// Add source information
auditEvent.getSource().setObserver(new Reference("Device/system"));
// Add entity information if we have a resource
String finalResourceType = resourceType;
String finalResourceId = resourceId;
if (finalResourceType != null && finalResourceId != null) {
AuditEvent.AuditEventEntityComponent entity = auditEvent.addEntity();
entity.setWhat(new Reference(finalResourceType + "/" + finalResourceId));
}
// Send the AuditEvent asynchronously
new Thread(() -> {
try {
auditClient.create().resource(auditEvent).execute();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
// Clean up thread locals
requestMethod.remove();
requestUri.remove();
}
}
To use this interceptor, you would register it with your FHIR client:
// Initialize FHIR context
FhirContext ctx = FhirContext.forR4();
// Create a FHIR client
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");
// Register the audit interceptor
AuditInterceptor auditInterceptor = new AuditInterceptor(ctx, "http://localhost:8080/fhir", "123");
client.registerInterceptor(auditInterceptor);
// Now all operations with this client will be logged
Step 3 of 3: Managing FHIR Transactions
Transactions in FHIR allow you to bundle multiple operations into a single request, ensuring atomicity. This is particularly useful when you need to ensure that a group of related operations either all succeed or all fail.
Implementing a Transaction
package com.saravanansubramanian.fhir;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.hl7.fhir.r4.model.*;
import java.util.Date;
import java.util.UUID;
public class TransactionExample {
public static void main(String[] args) {
// Initialize FHIR context
FhirContext ctx = FhirContext.forR4();
// Create a FHIR client
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");
// Create a bundle with transaction type
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
// Add a Patient resource
Patient patient = new Patient();
patient.addIdentifier()
.setSystem("http://hospital.example.org/patients")
.setValue("12345");
patient.addName()
.setFamily("Smith")
.addGiven("John");
patient.setBirthDate(new Date());
// Generate a UUID for this resource
String patientUuid = "urn:uuid:" + UUID.randomUUID().toString();
// Add the Patient resource to the bundle
bundle.addEntry()
.setFullUrl(patientUuid)
.setResource(patient)
.getRequest()
.setMethod(Bundle.HTTPVerb.POST)
.setUrl("Patient");
// Add an Observation resource that references the Patient
Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.FINAL);
// Set the code
CodeableConcept code = new CodeableConcept();
code.addCoding()
.setSystem("http://loinc.org")
.setCode("8867-4")
.setDisplay("Heart rate");
observation.setCode(code);
// Set the value
Quantity value = new Quantity();
value.setValue(80)
.setUnit("beats/minute")
.setSystem("http://unitsofmeasure.org")
.setCode("/min");
observation.setValue(value);
// Reference the Patient
observation.setSubject(new Reference(patientUuid));
// Add the Observation resource to the bundle
bundle.addEntry()
.setResource(observation)
.getRequest()
.setMethod(Bundle.HTTPVerb.POST)
.setUrl("Observation");
// Execute the transaction
Bundle responseBundle = client.transaction()
.withBundle(bundle)
.execute();
// Process the response
System.out.println("Transaction completed successfully");
System.out.println("Response bundle has " + responseBundle.getEntry().size() + " entries");
for (Bundle.BundleEntryComponent entry : responseBundle.getEntry()) {
System.out.println("Resource created: " + entry.getResponse().getLocation());
}
}
}
In this example, we create a transaction bundle that includes both a Patient resource and an Observation resource that references the Patient. By wrapping these operations in a transaction, we ensure that either both resources are created or neither is, maintaining the consistency of our data.
Transactions are particularly useful in healthcare scenarios, such as:
- Creating a Patient and their initial set of clinical data
- Recording a clinical encounter with multiple observations and diagnoses
- Updating a medication regimen with discontinuations and new prescriptions
When using transactions, it's important to remember that:
- All operations in a transaction must succeed, or none will be applied
- References between resources in the transaction should use UUID URIs (urn:uuid:...)
- The server will replace these temporary UUIDs with actual resource IDs in the response
- The order of resources in the bundle matters when there are dependencies
Conclusion
In this article, we've explored advanced topics in FHIR programming with Java and HAPI FHIR Server, focusing on security, auditing, and transaction management. These features are crucial for building secure, reliable, and maintainable healthcare applications.
By implementing proper authentication and authorization, you can ensure that only authorized users access sensitive healthcare data. With comprehensive auditing, you can track all interactions with your FHIR server, providing accountability and helping to meet regulatory requirements. And by using transactions, you can maintain data integrity across complex operations.
Stay tuned for the next article in this series, where we will continue to explore more advanced FHIR programming techniques.