The FHIR Authorization Tax
Why authorization in a FHIR backend is harder than it looks, what the standard pattern is for getting it right, and how a default-deny rule chain at the data layer pays back over the life of the product.
The cost no one budgets for
When a digital health team scopes their first backend, thorough authorization rarely appears as a major line item. The team budgets for the data model. The team budgets for the integrations. The team budgets for the operational tooling. Authorization shows up as “we’ll add OAuth and some checks.” Six months later, authorization is the part of the backend the team spends the most time defending.
The reason is that authorization in a digital health backend is not the same shape as authorization in a typical web application. A typical web application has a small number of resources, a small number of roles, and a small number of operations. A digital health backend, as small as it may start out, quickly grows to hundreds of resource types, dozens of clinically-meaningful relationships between users and data, and a search surface that lets a single query touch tens of thousands of records.
The cost of getting it wrong is not a missed feature. The cost is a leak. A patient who can see another patient’s records. A clinician who can see patients outside their care relationship. A research role that can re-identify subjects through search parameters the property filters did not block. Each of these is a regulatory finding that can take the product off the market.
The fix is to move the access boundary into the data layer, where every read and every write flow through and the boundary is enforced once for every request.
The wrong place: scattered checks
The most common authorization architecture in early-stage products is scattered checks. The application code that handles each request adds an if statement that verifies the user has the right to perform the operation. The check is written into the controller, into the service layer, sometimes into the database query.
The architecture works for a small product. As the product grows the architecture fails in a predictable way. There are now ten controllers, each with their own check. The check is correct in nine of them. The tenth was written by a different engineer at a different time and has a subtly different shape, and the difference is a leak.
The architecture also fails along a second dimension: search. A check that protects a single-resource read does not necessarily protect a search that returns the same resource as a result. The check protects GET /Observation/123; the same check does not necessarily protect GET /Observation?subject=Patient/456.
Adding more checks moves the failure mode but does not eliminate it. The fix is to move the check itself.
The data layer
The architecture that works puts the authorization rule chain at the data layer. Every read and every write flow through the data layer; the rule chain evaluates against the request; the access boundary is a property of the data layer rather than of each controller.
The rule chain is a default-deny chain. A request that does not match an explicit grant is denied. The grants are evaluated in sequence; the first match wins. The grants are expressed in a language the data layer can evaluate against the data, not in code that has to be re-implemented for each controller.
The chain handles single-resource reads, searches, writes, and bulk operations uniformly. The same grant that permits a patient to read their own Observation also narrows the search the patient performs. The same grant that permits a clinician to write an Observation for a patient in their care relationship also narrows the bulk export the clinician runs.
The architecture does not require fewer rules than scattered checks. It requires the same rules, expressed in one place where the data layer evaluates them.
The compounding payback
The architecture pays back in three ways over the life of the product.
The first is correctness. A new controller that uses the data layer inherits the rule chain. The rule chain does not have to be re-implemented for each new endpoint. The new endpoint cannot bypass the boundary because the boundary is in the data layer the endpoint depends on.
The second is auditability. The rule chain knows which rule authorised each request. The audit log records the rule. A regulator who wants to know how a particular access was authorised can read the audit log and see the rule by name. The audit becomes documentation; the documentation becomes a property of the system.
The third is composability. New roles add new grants. New resource types fit the existing grants because the grants are expressed against FHIR concepts (subject compartment, care relationship, organisation membership) that apply to every FHIR resource. Adding the third role to the system is the same shape of work as adding the second.
The side channels you have to close
The data layer rule chain is necessary but not sufficient. A rule chain that returns the right resources can still leak through search parameters. A rule chain that filters fields can still leak through the existence of the field.
The two most common side channels are search parameters and reference traversal.
Search parameters leak when a role can search by a field but cannot see the field’s value. The role learns the value from the search result set: a role that can search by subject.identifier but not see the identifier can search for a known value and confirm whether a record with that identifier exists. The fix is per-role search-parameter blocklists that prevent the role from searching by fields it cannot see.
Reference traversal leaks when a role can read a resource and follow its references to resources the role cannot see directly. A role that can read an Observation but not the Patient the Observation references can sometimes infer the Patient’s existence from the Observation’s link. The fix is per-role reference-traversal limits that follow only the references the role is permitted to see.
The combination of property filters, search-parameter blocklists, and reference-traversal limits is what closes the side channels. A backend that has all three is a backend that does not leak; a backend that has only one or two has the leaks the missing one or two would have closed.
A worked example
A research role on a FHIR backend wants to see Observation values without seeing patient identifiers. The naive implementation grants the role read access to Observations and applies a property filter that strips the patient identifier from the response.
The naive implementation leaks in three ways.
First, the role can search by subject and learn which patient owns each Observation from the result set. Fix: blocklist subject from the role’s allowed search parameters.
Second, the role can include the patient through _include=Observation:subject and read the patient directly. Fix: limit the role’s includes to references the role can see.
Third, the role can read the Observation’s subject.reference field and infer the patient’s identifier from the URL. Fix: extend the property filter to strip the entire subject field from the response.
The corrected implementation grants the role read access to Observations, blocks the subject search parameter, limits the includes, and strips the subject field. The role sees Observation values; the role does not see the patients the Observations belong to; the role cannot infer the patients through any of the three side channels.
The corrected implementation is a small amount of additional rule configuration. The naive implementation produces three leaks; the corrected implementation produces none.
The pattern at the product level
Across the backends we have built, the pattern that survives the longest is the pattern above: data layer rule chain, default-deny, identity-attributable audit, property filters, search-parameter blocklists, reference-traversal limits. The pattern is not unique to Fire Arrow. The pattern is unique to backends that have been audited enough times to learn what the audit finds.
What Fire Arrow ships is the pattern as a product feature: the rule chain is the access boundary, the configuration is the rule chain, the audit log is identity-attributable by construction, and the side-channel controls are part of the rule chain itself.
The “tax” in the title is the cost a team pays when the pattern is not built into the architecture. The cost is paid in audit findings, in incident response, in re-architecture under deadline pressure, in the parts of the backend the team has to defend instead of build on. The tax is avoidable. The architecture above is what avoids it.
What to read next
For more depth on the patterns above, the FHIR Authentication and Authorization Patterns whitepaper walks through the production patterns. The Identity Filtering whitepaper covers the property-filter-and-blocklist combination in detail.
For the related landing pages on this site, see FHIR authorization (RBAC), FHIR anonymization, and LLM access control for healthcare.