Articles
/
Integrations

Implementing Role-Based Access Control for FHIR Resources with Keycloak and SMART on FHIR V2

Aleksandr Kislitsyn
October 14, 2025
7 minutes

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.

The Problem: One Endpoint, Multiple Access Levels

Imagine you're building a healthcare application on top of the FHIR server with two types of users:

Dr. Sarah (Physician)

  • Needs to see all patient observations
  • Reviews both laboratory results and vital signs
  • Makes clinical decisions based on complete patient data

Mike (Lab Technician)

  • Works only with finalized laboratory results
  • Shouldn't see vital signs or preliminary lab data
  • Needs access limited to their specific job function

Both users call the same API endpoint: GET /fhir/Observation. But they should see completely different results.

Traditionally, developers would solve this by adding:

  • Custom middleware to check user roles
  • Complex query filters in your application code
  • Separate API endpoints or parameters for different user types
  • Extensive unit tests for each authorization scenario
  • Maintenance headaches when requirements change

This approach works, but it’s hard to maintain and prone to errors as requirements evolve. There's a better way.

The Solution: SMART Scopes Meet Keycloak Roles

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:

  1. Define granular permissions as basic Keycloak roles (each representing a SMART scope)
  2. Group them into job functions using composite roles (like "physician" or "lab_technician")
  3. Automatically resolve composite roles into SMART scopes in the access token
  4. Let Aidbox enforce the rules automatically — no custom code required

Here's how the flow works:

User Login → Keycloak resolves roles → Token with SMART scopes →
Aidbox validates token → Automatic data filtering → Correct results

Implementation: Building the Role Structure

Step 1: Define Basic Roles (Granular SMART Scopes)

In Keycloak, we create basic roles that directly map to SMART on FHIR V2 scopes:

Basic roles:

  • user/Patient.rs - Read and search patient data
  • user/Encounter.rs - Read and search encounters
  • user/Observation.rs - Read and search ALL observations
  • user/Observation.rs?category=laboratory&status=final - Read and search ONLY finalized lab results

These basic roles are the building blocks. Each one represents a specific permission with optional query parameter restrictions.

Step 2: Create Composite Roles (Job Functions)

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

Step 3: Configure the Token Mapper

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:

  1. Resolve composite roles (e.g., "physician") into their constituent basic roles
  2. Include those basic roles in the token's scope claim as SMART scopes
  3. Add the atv: "2" claim to indicate that there are SMART on FHIR scopes in the token to process

We 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"
}

Step 4: Configure Aidbox for Automatic Enforcement

Here's the most elegant part: Aidbox automatically enforces the access rules defined in the token. All you need is:

  1. TokenIntrospector - Verifies that the token is valid and issued by your Keycloak instance:
{
  "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"
}
  1. AccessPolicy — Allows requests with validated tokens:
{
  "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:

  • Parses the scopes to determine allowed resource types
  • Applies query parameter filters from the scopes
  • Returns only data matching the scope constraints
  • Rejects requests for unauthorized resource types

No custom authorization code. No complex access control logic. It just works.

Seeing It in Action

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:

  • Hemoglobin — a laboratory observation with status “final”
  • Blood Pressure — a vital signs observation

Both users will access the same endpoint: GET /fhir/Observation
But Aidbox will return different responses depending on who’s making the request.

Test 1: Physician Access (Full Access)

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:

  1. Hemoglobin (laboratory, final)
  2. Blood Pressure (vital signs)

The scope user/Observation.rs grants unrestricted access to all Observation resources.

Test 2: Lab Technician Access (Restricted Access)

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:

  1. Hemoglobin (laboratory, final)

The Blood Pressure observation is filtered out because it doesn't match the scope requirements:

  • It's category "vital-signs" (not "laboratory")
  • The scope explicitly requires category=laboratory&status=final

Same endpoint, same code, different results based on who's asking. No custom filtering logic required.

Try It Yourself

Want to see this in action? The complete example is ready to run with Docker.

Prerequisites: Docker and Docker Compose installed

Quick start:

  1. Clone the repository and navigate to the example:
cd aidbox-features/smart-keycloak-roles
  1. Start all services:
docker compose up --build
  1. Initialize Aidbox: Navigate to http://localhost:8080 and complete the Aidbox initialization.
  2. Open the demo application: Navigate to http://localhost:3000.
  3. Test both user roles:
    • Login as physician/password — see all observations
    • Click “Start Over” and login as lab_technician/password — see only lab results

The demo application shows side-by-side what each user can access, making the access control differences immediately visible.

Key Takeaways

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:

Conclusion

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.

How did you like the article?

contact us

Get in touch with us today!

By submitting the form you agree to Privacy Policy and Cookie Policy.
Thank you!
We’ll be in touch soon.

In the meantime, you can:
Oops! Something went wrong while submitting the form.

Never miss a thing
Subscribe for more content!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
By clicking “Subscribe” you agree to Health Samurai Privacy Policy and consent to Health Samurai using your contact data for newsletter purposes