Skip to content

FHIR Proxy ​

The FHIR proxy is the core component of Proxy Smart. It sits between SMART apps and upstream FHIR servers, providing authentication, authorization, consent enforcement, and capability-aware request normalization.

Route Structure ​

All proxied FHIR requests follow this URL pattern:

{BASE_URL}/proxy-smart-backend/{server_name}/{fhir_version}/{resource_path}

For example:

https://api.proxy-smart.com/proxy-smart-backend/hapi-fhir/R4/Patient/123
SegmentDescription
proxy-smart-backendFixed prefix (derived from the backend package name)
server_nameIdentifier of a registered FHIR server (see FHIR Servers)
fhir_versionFHIR version -- R4, R5, etc. (configured via FHIR_SUPPORTED_VERSIONS)
resource_pathStandard FHIR path -- Patient/123, Observation?patient=123, etc.

Request Pipeline ​

Every proxied request passes through a six-stage pipeline:

1. Authentication ​

All requests (except GET /metadata) require a valid Bearer token. The token is validated against Keycloak's JWKS endpoint. If validation fails, the proxy returns 401.

2. Operation Policy ​

Some requests are refused whatever the token's scopes say, and regardless of SCOPE_ENFORCEMENT_MODE: no SMART grant covers them.

RequestResult
Server administration operations: $expunge, $reindex, $reindex-terminology, $perform-reindexing-pass, $mark-all-resources-for-reindexing, $get-resource-counts, $trigger-subscription, the HAPI merge and replace-references operations, and code system uploads403
$export and $export-poll-status without a system/ scope403. Bulk Data export is for SMART Backend Services.
DELETE with _expunge or _cascade403

Purging data is still possible for the one case a patient is entitled to it: see Record Erasure.

When consent enforcement is enabled (CONSENT_MODE=enforce), the proxy checks whether the token holder has consent to access the requested resource.

This means the FHIR Consent resource throughout -- the patient's standing decision about who may reach their data. It is unrelated to the OAuth consent screen (Keycloak's consentRequired on a client), where a user approves the scopes an app asked for. That grants access to nobody's record.

The rule: a patient's data may be reached by someone else only if the patient consented to that someone.

  • The consent service evaluates the request against FHIR Consent resources
  • Identity Assurance Level (IAL) checks verify the trust level of the Person→Patient link
  • If consent is denied, the proxy returns 403 with details:
    • consent_denied -- no active consent for this access
    • ial_verification_failed -- identity assurance level insufficient

Consent enforcement has three modes:

ModeBehavior
disabledNo consent checks (default)
audit-onlyChecks consent and logs decisions, but never blocks requests
enforceBlocks requests without valid consent

The patient the token is about, resolved in this order: a patient claim, then the launch context captured at token exchange, then fhirUser when it names a Patient. The requested URL is a last resort only, used when the token identifies no patient at all.

The URL never outranks the token. Judging the URL's patient would check a token for one patient against another patient's consent.

Which consents apply ​

provision.actor is the grantee -- the recipient the consent names. An actor matches when it references the requesting fhirUser (for example Practitioner/dr-123, which is what the consent app writes when a patient approves an access request), or when its reference or identifier carries the OAuth client id (for grants written against an app rather than a person).

A provision with no actor names no recipient and therefore grants nothing. An actor carrying only a display and no reference -- as an SHL mirror does -- likewise grants no FHIR access.

Self-access ​

A patient reaching their own record is not a disclosure, so no Consent is required and the check is skipped. This is decided per request, by comparing the token's fhirUser to the patient the request is about -- not per client. A client serving both patients and practitioners is therefore skipped for the patient and still enforced for the practitioner, which CONSENT_EXEMPT_CLIENTS cannot express.

Self-access answers only whether the patient consented. It does not decide which record may be read -- see Role-Based Data Isolation below, which must be enforcing for that.

4. SMART Scope Enforcement ​

When enabled (SCOPE_ENFORCEMENT_MODE=enforce), validates that the token's scopes grant permission for the requested operation.

  • Supports SMART v1 format (patient/Observation.read) and v2 format (patient/Observation.cruds)
  • Validates resource type and HTTP method against granted scopes
  • Wildcard scopes (patient/*.read) match any resource type
  • Returns 403 if the requested operation exceeds granted scopes

5. Role-Based Data Isolation ​

When enabled (ROLE_BASED_FILTERING_MODE=enforce), confines a request to one patient's FHIR compartment. This is the only stage that decides which patient may be read; scope enforcement checks resource types, and consent checks who may receive data.

Two rules, in order:

A patient/-scoped grant is confined to one patient, whoever the user is -- per SMART, "if the app has any patient-level scopes, they will be scoped to Patient 123". The patient is resolved the same way consent resolves it: patient claim, then the launch context captured at token exchange, then fhirUser when it names a Patient. A token holding patient/ scopes with none of those resolving is refused, rather than widened to the whole server.

A user who IS a patient (fhirUser: Patient/…) sees only their own data.

Either way the compartment is applied as:

RequestResult
GET Patient?…_id={ownId} injected
GET Patient/{other}403
GET Observation (any PATIENT_SCOPED_RESOURCES type)patient=Patient/{ownId} injected
GET Observation/{id}ownership verified upstream; 403 if the resource is not the patient's

A user with only user/-scoped access and a non-Patient fhirUser -- a practitioner -- is not compartment-filtered here. Their access is governed by consent instead.

Not yet implemented: narrowing a practitioner to the patients assigned to them via generalPractitioner. There are no generalPractitioner links in the system yet and the proxy performs no such lookup, so nothing here bounds a practitioner to an assigned panel. Until it exists, consent is the only thing limiting which patients a practitioner can reach -- which makes actor matching (above) load-bearing rather than advisory.

6. Capability-Aware Normalization ​

The proxy fetches and caches each upstream server's CapabilityStatement to enable intelligent request handling.

Strict Mode (per-server opt-in) ​

When strictCapabilities is enabled on a FHIR server:

  • Interaction checks -- rejects unsupported CRUD operations with 405
  • History checks -- rejects _history requests if the server doesn't declare history support
  • Operation checks -- rejects $operation calls not declared in the CapabilityStatement
  • PATCH format checks -- rejects PATCH with unsupported content types with 415

Search Parameter Normalization (always active) ​

Regardless of strict mode, the proxy strips search parameters and _include/_revinclude values not declared by the upstream server. This prevents 400 errors from servers that reject unknown parameters. Stripped parameters are listed in the x-proxy-stripped-params response header.

Record Erasure ​

POST [base]/[type]/[id]/$erase permanently removes one record the signed-in patient created, together with its history. The proxy answers it rather than forwarding it, and the upstream server must support $expunge.

It runs after consent, tenant and scope checks, and the scope check treats it as a delete, so the token needs d (or write) on the resource type. The proxy then refuses with an OperationOutcome unless all of these hold:

CheckRefusal
The user is the patient (fhirUser: Patient/…), and any patient context names the same patient403
Every version of the record belongs to that patient (subject or patient)403
No version has verificationStatus confirmed, and no version names a Practitioner, PractitionerRole or Organization as asserter, recorder, performer, requester, author or attester409
No Provenance about the record has such an agent409
Nothing but Provenance references the record409, naming the referencing resources
The upstream server advertises $expunge501

A record a clinician attested stays: mark it entered-in-error instead, which keeps the original recognisable as medical record law requires. The Patient resource itself cannot be erased this way.

When the checks pass, a Provenance about this record alone is deleted and one about several records loses only this target; both are expunged. Then the record is deleted and expunged, including earlier versions. The proxy logs the erasure (reference and subject, no content) and answers 200.

URL Rewriting ​

Response bodies are rewritten so that all FHIR resource URLs point back through the proxy rather than directly at the upstream server. This ensures clients always route through the proxy's access control pipeline.

mTLS Support ​

Each FHIR server can be configured with mutual TLS (mTLS) certificates for upstream connections. When enabled, the proxy presents a client certificate when connecting to that server. See FHIR Servers for configuration.

SMART Configuration ​

Each FHIR server exposes a /.well-known/smart-configuration endpoint through the proxy, dynamically generated from Keycloak's OIDC configuration and cached for performance.

Monitoring ​

All proxied requests are tracked with metrics including server name, HTTP method, resource type, status code, response time, and client ID. These metrics are available via the Monitoring dashboard.

Environment Variables ​

VariableDescriptionDefault
FHIR_SERVER_BASEComma-separated upstream FHIR server URLshttp://localhost:8081/fhir
FHIR_SUPPORTED_VERSIONSComma-separated FHIR versionsR4
CONSENT_MODEConsent enforcement mode: disabled, audit-only, enforce; only applies when CONSENT_ENABLED=trueaudit-only
CONSENT_ENABLEDEnable consent checksfalse
CONSENT_CACHE_TTLConsent decision cache TTL (ms)60000
CONSENT_EXEMPT_CLIENTSComma-separated client IDs exempt from consent. Not needed for patient self-access, which is detected per request--
CONSENT_REQUIRED_RESOURCE_TYPESResource types that always require consent--
CONSENT_EXEMPT_RESOURCE_TYPESResource types exempt from consentCapabilityStatement,metadata
IAL_ENABLEDEnable Identity Assurance Level checksfalse
IAL_MINIMUM_LEVELMinimum IAL for general accesslevel1
IAL_SENSITIVE_RESOURCE_TYPESResource types requiring elevated IAL--
IAL_SENSITIVE_MINIMUM_LEVELMinimum IAL for sensitive resourceslevel3
IAL_VERIFY_PATIENT_LINKVerify token patient matches Person.link[]true
IAL_ALLOW_ON_PERSON_LOOKUP_FAILUREAllow access if Person lookup failsfalse
IAL_CACHE_TTLPerson resource cache TTL (ms)300000
SCOPE_ENFORCEMENT_MODEScope enforcement: disabled, audit-only, enforceenforce
ROLE_BASED_FILTERING_MODERole-based filtering: disabled, audit-only, enforceaudit-only
PATIENT_SCOPED_RESOURCESResource types subject to patient-scoped filteringObservation,Condition,...
SMART_CONFIG_CACHE_TTLSMART configuration cache TTL (ms)300000

Proxy Smart — Healthcare Interoperability Platform