Building healthcare applications on top of a FHIR server has become a common practice. FHIR servers provide standardized storage and APIs for clinical resources, allowing developers to focus on building features rather than managing healthcare data infrastructure. However, this introduces an important question: how do you ensure that different users see only the data they are allowed to see?
For example, a physician should have full access to patient observations — both laboratory results and vital signs. A lab technician, on the other hand, should only see finalized laboratory results relevant to their work. Implementing this kind of fine-grained access control traditionally requires complex application-level logic, custom authorization rules, and extensive testing.
There's a better way: a standards-based approach that leverages your existing identity provider and requires zero custom authorization code in your application.
In this post, we'll explore how to implement sophisticated role-based access control (RBAC) for FHIR resources using Keycloak (an open-source identity and access management solution) roles mapped to SMART on FHIR V2 scopes, with automatic enforcement by Aidbox. The best part? The same API endpoint returns different data based on who's asking — completely transparently.
Imagine you're building a healthcare application on top of the FHIR server with two types of users:
Dr. Sarah (Physician)
Mike (Lab Technician)
Both users call the same API endpoint: GET /fhir/Observation. But they should see completely different results.
Traditionally, developers would solve this by adding:
This approach works, but it’s hard to maintain and prone to errors as requirements evolve. There's a better way.
SMART on FHIR V2 introduces a powerful concept: scopes with query parameters. Instead of just anting broad access like "this user can read Observations," you can specify exact rules, such as:
user/Observation.rs?category=laboratory&status=final
This scope means: "The user can read and search Observations, but only laboratory observations with final status."
By combining this with Keycloak's composite role system, we can:
Here's how the flow works:
User Login → Keycloak resolves roles → Token with SMART scopes →
Aidbox validates token → Automatic data filtering → Correct results
In Keycloak, we create basic roles that directly map to SMART on FHIR V2 scopes:
Basic roles:
user/Patient.rs
- Read and search patient datauser/Encounter.rs
- Read and search encountersuser/Observation.rs
- Read and search ALL observationsuser/Observation.rs?category=laboratory&status=final
- Read and search ONLY finalized lab resultsThese basic roles are the building blocks. Each one represents a specific permission with optional query parameter restrictions.
Next, combine basic roles into composite roles that reflect real job functions:
Physician Role (full clinical access):
user/Patient.rs
user/Encounter.rs
user/Observation.rs
Lab Technician Role (limited to lab results):
user/Patient.rs
user/Observation.rs?category=laboratory&status=final
Notice that the Lab Technician role uses the restricted Observation scope — preventing access to vital signs or draft results
This is where the magic happens. Keycloak turns roles into access tokens. The token is a small, secure file (JSON Web Token — JWT) that travels with every API request and tells Aidbox what the user is allowed to do. We need Keycloak to:
scope
claim as SMART scopes atv: "2"
claim to indicate that there are SMART on FHIR scopes in the token to processWe accomplish this using a custom script-based protocol mapper. When a lab technician logs in, their access token looks like this:
{
"sub": "lab_technician",
"scope": "user/Patient.rs
user/Observation.rs?category=laboratory&status=final",
"atv": "2"
}
Here's the most elegant part: Aidbox automatically enforces the access rules defined in the token. All you need is:
{
"resourceType": "TokenIntrospector",
"jwt": {
"iss": "http://localhost:8888/realms/master"
},
"type": "jwt",
"jwks_uri":
"http://keycloak:8888/realms/master/protocol/openid-connect/certs",
"id": "external-auth-server",
"resourceType": "TokenIntrospector"
}
{
"resourceType": "AccessPolicy",
"id": "keycloak-access-policy",
"engine": "matcho",
"matcho": {
"jwt": {
"iss": "http://localhost:8888/realms/master"
}
}
}
That's it. When Aidbox receives a request with a token containing atv: "2" and SMART scopes, it automatically:
No custom authorization code. No complex access control logic. It just works.
Let's walk through both user scenarios to see how the same API call returns different results. Imagine you’ve set up your system with Aidbox connected to Keycloak, and both Dr. Sarah (the physician) and Mike (the lab technician) are using the same application.
In the system, there are two Observation resources:
Both users will access the same endpoint: GET /fhir/Observation
But Aidbox will return different responses depending on who’s making the request.
Login credentials:
Username: physician
Password: password
API call:
GET /fhir/Observation
Authorization: Bearer <physician_token>
Token scopes:
user/Patient.rs user/Encounter.rs user/Observation.rs
Result: The physician sees both observations:
The scope user/Observation.rs
grants unrestricted access to all Observation resources.
Login credentials:
Username: lab_technician
Password: password
API call:
GET /fhir/Observation
Authorization: Bearer <lab_technician_token>
Token scopes:
user/Patient.rs
user/Observation.rs?category=laboratory&status=final
Result: The lab technician sees only one observation:
The Blood Pressure observation is filtered out because it doesn't match the scope requirements:
category=laboratory&status=final
Same endpoint, same code, different results based on who's asking. No custom filtering logic required.
Want to see this in action? The complete example is ready to run with Docker.
Prerequisites: Docker and Docker Compose installed
Quick start:
cd aidbox-features/smart-keycloak-roles
docker compose up --build
The demo application shows side-by-side what each user can access, making the access control differences immediately visible.
This approach delivers several significant benefits:
1. Zero Custom Authorization Code
Your application doesn't need to understand roles or implement filtering logic. Aidbox handles it automatically based on SMART scopes.
2. Centralized Access Control
All role definitions live in your identity provider (Keycloak). Change a role's permissions in one place, and it applies everywhere.
3. Standards-Based
Built on SMART on FHIR V2, an established healthcare interoperability standard. No proprietary solutions or vendor lock-in.
4. Fine-Grained Control
Query parameter restrictions (?category=laboratory&status=final) enable precise access control without complex custom rules.
5. Easy to Extend
Need a new role? Create a composite role in Keycloak combining the appropriate basic roles. No code changes required.
Learn more:
Fine-grained access control for FHIR resources doesn't have to be complicated. By combining SMART on FHIR V2 scopes, Keycloak's composite role system, and Aidbox's automatic scope enforcement, you can implement sophisticated RBAC without writing custom authorization code.
The result: safer applications, easier maintenance, and better alignment with healthcare interoperability standards.
Ready to implement role-based access control in your FHIR application? Start with the example repository and customize the roles for your specific use case.
Get in touch with us today!