Skip to main content

Troubleshooting Authorization

A request returns 403 Forbidden and you are not sure why. This guide walks you through a systematic approach to find the root cause, starting from the simplest checks and working toward deeper diagnostics.

Before you start

Do not disable access validation

It is tempting to set default-validator: Allowed or turn off authorization entirely to "get things working." Do not do this, even temporarily. Setting the default validator to Allowed does not solve the underlying problem — it hides it, and every resource on the server becomes accessible to every client, authenticated or not. This includes patient records, practitioner data, and organizational resources.

The correct approach is always to find out why the request is denied and fix the actual cause. The steps below will get you there.

Step 1: Verify your identity with $me

The most common source of authorization failures is the client not being who you think it is. The $me operation returns the server's view of the authenticated caller — no authorization rules are applied to this endpoint, so it always works.

curl http://localhost:8080/fhir/\$me \
-H "Authorization: Bearer <your-token>"

What to look for:

A successful response returns the FHIR resource reference that the server resolved from the token:

{"resourceType": "Reference", "reference": "Practitioner/abc-123"}

Check two things:

  1. Is the resource type correct? If you expect Practitioner but see Patient, or if you see Public, the token is not mapping to the right identity. The resource type determines the client role, and the client role determines which authorization rules apply.

  2. Is the resource ID correct? If the resource type is right but the ID points to a different resource than expected, identity resolution matched the wrong record.

If the response shows "reference": "Public", the server could not resolve any FHIR resource from the token. This typically means:

  • The token's subject claim doesn't match any resource in the configured identifier system.
  • The email fallback didn't find a matching resource either.
  • Auto-create is not enabled, so no resource was created.

In all of these cases, review your authentication provider configuration — specifically the identifier-system, email mapping, and auto-create-enabled settings.

Step 2: Inspect the client identity resource

Once you have the resource reference from $me, fetch the actual resource:

curl http://localhost:8080/fhir/Practitioner/abc-123 \
-H "Authorization: Bearer <admin-token>"

Look at the resource and confirm it represents the right person or system. Pay attention to:

  • Identifiers — does the resource have the expected identifiers that link it to the authentication provider?
  • Active status — is the resource marked as active?
  • For Patients: does the resource have a managingOrganization? Validators like LegitimateInterest require this field to determine which organization the patient belongs to. A missing managingOrganization means the validator cannot establish any organizational link, and access will be denied.
{
"resourceType": "Patient",
"id": "patient-456",
"managingOrganization": {
"reference": "Organization/clinic-a"
}
}

If managingOrganization is missing, update the Patient resource to reference the correct organization.

Step 3: Check PractitionerRole assignments (for Practitioner clients)

If the client is a Practitioner using organization-based validators like LegitimateInterest, access depends entirely on the practitioner's PractitionerRole resources. A Practitioner without any PractitionerRole has no organizational affiliations and will be denied access to everything that requires an organizational link.

Search for the practitioner's roles:

curl "http://localhost:8080/fhir/PractitionerRole?practitioner=Practitioner/abc-123" \
-H "Authorization: Bearer <admin-token>"

For each PractitionerRole in the result, verify:

  1. Organization reference — does the PractitionerRole point to the right organization? The organization field must reference the Organization where the practitioner should have access.

  2. Role code — if your authorization rules use practitioner-role-system and practitioner-role-code filtering, the PractitionerRole must have a matching code entry. A PractitionerRole with code nurse will not match a rule that requires code doctor.

  3. Active status — is the PractitionerRole active? Inactive roles are ignored by the LegitimateInterest validator.

A correctly configured PractitionerRole looks like this:

{
"resourceType": "PractitionerRole",
"practitioner": { "reference": "Practitioner/abc-123" },
"organization": { "reference": "Organization/clinic-a" },
"code": [{
"coding": [{
"system": "http://hl7.org/fhir/ValueSet/practitioner-role",
"code": "doctor"
}]
}]
}

If the practitioner has no PractitionerRole at all, or if the existing roles point to wrong organizations, create or update the PractitionerRole resources accordingly.

Step 4: Trace the access chain to the target resource

Now that you have confirmed the client's identity and organizational affiliations, look at the resource being accessed and check whether a valid chain exists between the client and that resource.

Does a rule exist for this request?

Before tracing data relationships, confirm that your configuration has a rule that covers this combination of client role, resource type, and operation. For example, if a Practitioner tries to read an Observation, you need:

- client-role: Practitioner
resource: Observation
operation: read
validator: LegitimateInterest # or another appropriate validator

If no rule exists, the default validator applies — which should be Forbidden in a properly configured system.

Tracing access with LegitimateInterest

The LegitimateInterest validator builds access chains through organizational relationships. When a request is denied, it helps to manually trace the chain and find where it breaks.

Example: Practitioner reading a Patient's Observation

Dr. Smith (Practitioner/smith) tries to read Observation/obs-1, which is a lab result for patient Jane (Patient/jane). The access chain the validator checks is:

Trace each link:

  1. Practitioner → PractitionerRole → Organization: Does Practitioner/smith have an active PractitionerRole at Organization/clinic-a?
  2. Patient → Organization: Does Patient/jane have managingOrganization set to Organization/clinic-a (or another organization where Dr. Smith has a role)?
  3. Observation → Patient: Does Observation/obs-1 reference Patient/jane as its subject?

If any link in this chain is broken — the practitioner has no role at the patient's organization, the patient has no managing organization, or the observation doesn't reference the patient — access is denied.

Example: Practitioner accessing a CarePlan

CarePlan resources follow the same pattern through the patient compartment. If Dr. Smith tries to read CarePlan/cp-1:

The chain is identical to the Observation example — the CarePlan must reference a patient whose managing organization is one of the practitioner's organizations. Verify:

  1. CarePlan/cp-1 has a subject reference pointing to Patient/jane.
  2. Patient/jane has managingOrganization set to an organization where Dr. Smith has a PractitionerRole.

A common mistake is creating a CarePlan without setting the subject field, or setting it to a patient who belongs to a different organization than the practitioner.

Example: Patient accessing organizational resources

Patient Jane (Patient/jane, managingOrganization: Organization/clinic-a) tries to browse practitioners at her clinic by searching for PractitionerRole resources:

The access chain here is: the patient's managing organization must match the PractitionerRole's organization. If the patient has no managingOrganization, no organizational resources are accessible through LegitimateInterest.

Tracing access with compartment validators

For simpler validators like PatientCompartment, the chain is shorter — the accessed resource must be in the patient's FHIR compartment (i.e., it must reference the patient directly). If a Patient tries to read Observation/obs-1, the validator checks that Observation/obs-1 has a subject or patient reference pointing to the patient's own resource.

Step 5: Enable rule debugging

If manual tracing doesn't reveal the issue, enable the authorization rule debugger. It records the evaluation of every rule for a denied request and reports exactly what matched, what didn't, and why.

Enable debug mode

Add this to your server configuration:

fire-arrow:
authorization:
debug-enabled: true

Or set the environment variable:

FIRE_ARROW_AUTHORIZATION_DEBUG_ENABLED=true

Send a request with the debug header

Replay the failing request with the X-Fire-Arrow-Debug: true header:

curl -H "Authorization: Bearer <your-token>" \
-H "X-Fire-Arrow-Debug: true" \
http://localhost:8080/fhir/Observation/obs-1

If the request is denied, the response body includes detailed [DEBUG] trace entries instead of the generic "access denied" message. If the request succeeds, no debug output is produced.

Reading the debug output

Here is an example of a debug response for a Practitioner who is denied access to a Patient resource:

{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "forbidden",
"diagnostics": "Access denied by authorization policy"
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Client identity: role=Practitioner, resourceId=456, resolvedType=Practitioner, jwtClaimType=Practitioner, [email protected]"
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Request: operation=read, resourceType=Patient, includes=[], path=Patient/789"
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Rule evaluation: 0/8 rules matched, 1 total auth rules produced"
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Rule #0 (a1b2c3d4) Patient/Patient/read/PatientCompartment: ROLE_MISMATCH"
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Rule #1 (b2c3d4e5) Practitioner/Observation/read/LegitimateInterest: RESOURCE_MISMATCH"
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Rule #2 (c3d4e5f6) Practitioner/Patient/search/LegitimateInterest: OPERATION_MISMATCH"
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Default validator 'Forbidden' produced 1 rules"
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Near-miss: Your client role 'Practitioner' has rules for 'Patient' with operations [search] but NOT 'read'."
},
{
"severity": "information",
"code": "informational",
"diagnostics": "[DEBUG] Near-miss: A 'search' rule exists for Practitioner/Patient but no 'read' rule. Search results require read access to return individual resources."
}
]
}

Work through the output from top to bottom:

  1. Client identity — confirm the role and resource ID match what you expect. If the role is Public when it should be Practitioner, go back to Step 1.

  2. Request context — confirm the operation and resource type. A graphql-read is different from a read — if your client uses GraphQL, you need separate rules for GraphQL operations.

  3. Individual rule evaluations — look for the rule that should match your request. Each rule shows one of these results:

    ResultWhat it meansWhat to check
    MATCHEDRule produced auth rules successfullyThe rule works — the denial is caused by the validator's runtime check (e.g., the resource isn't in the compartment)
    ROLE_MISMATCHClient role doesn't match the ruleIs the client authenticating with the right identity?
    RESOURCE_MISMATCHResource type doesn't match the ruleIs there a rule for this resource type?
    OPERATION_MISMATCHOperation doesn't match the ruleDo you have both read and search rules? REST and GraphQL rules?
    IDENTITY_FILTER_FAILEDFHIRPath filter on the client identity returned falseCheck the filter expression and the client's identity resource
    NO_RULES_PRODUCEDValidator ran but produced no auth rulesFor LegitimateInterest: check PractitionerRole/managingOrganization setup
  4. Near-miss hints — these are actionable suggestions. The analyzer detects patterns like missing companion rules (search without read), role mismatches that suggest wrong authentication, and missing organizational memberships.

  5. Default validator — if the output ends with "Default validator 'Forbidden' produced 1 rules", no configured rule matched the request at all. You either need to add a rule or fix the matching criteria on an existing one.

Common patterns in debug output

"0/N rules matched" — No rule matched the request. Look at each rule's mismatch reason and find the one closest to matching. The near-miss hints often point directly to the fix.

A rule shows MATCHED but access is still denied — The rule matched and the validator produced auth rules, but the HAPI FHIR authorization engine denied access at runtime. This means the data relationship is wrong — for example, the observation's subject doesn't point to a patient in the practitioner's organizations. Go back to Step 4 and trace the access chain.

IDENTITY_FILTER_FAILED — The rule matched on role, resource, and operation, but the FHIRPath identity filter expression evaluated to false on the client's identity resource. Fetch the client's identity resource and manually evaluate the filter expression to understand why.

Disable debug mode after troubleshooting

Debug output exposes your complete authorization rule configuration. Keep debug-enabled: false in production, and only enable it temporarily when investigating an issue. See Authorization Debug Mode for full details on security considerations.

Step 6: Use the validation rule table in Fire Arrow UI

If you have access to the Fire Arrow web interface, the Server Config > Authorization Rules tab provides a visual overview of all configured rules as a matrix. It shows client roles as columns, resource types as rows, and the configured validator for each combination of role, resource, and operation.

This view makes it easy to spot gaps in your configuration:

  • Empty cells indicate that no rule exists for that combination — the default validator applies.
  • Unexpected validators stand out visually when you scan the matrix.
  • Missing operations are apparent when you see read configured but not search, or vice versa.

Open the Server Config page from Administration > Server Config in the sidebar, then switch to the Authorization Rules tab. The table reflects the currently active configuration, including any changes made through the YAML editor.

When investigating a 403 error, locate the row for the denied resource type and the column for the client's role. Check whether the expected operation has a validator configured. If the cell is empty, the default validator (Forbidden) applies, and you need to add a rule.

The validation rule table is particularly helpful for getting a "big picture" view of your authorization setup. Combined with the debug output from Step 5, which tells you exactly which rule failed and why, you can usually resolve any authorization issue quickly.

Troubleshooting checklist

Use this quick-reference checklist when a request returns 403:

#CheckCommand / Action
1Is the client authenticated correctly?GET /fhir/$me — verify role and resource ID
2Is the identity resource correct?Fetch the resource returned by $me
3(Practitioner) Does the practitioner have PractitionerRoles?GET /fhir/PractitionerRole?practitioner=Practitioner/{id}
4(Practitioner) Do the PractitionerRoles reference the right organization?Check the organization field on each PractitionerRole
5(Patient) Does the patient have a managingOrganization?Check the managingOrganization field on the Patient resource
6Does a rule exist for this role + resource + operation?Check the validation rule table in Fire Arrow UI
7Does the target resource link back to the client?Trace the access chain (subject → patient → managingOrganization → PractitionerRole → practitioner)
8What does the debug output say?Replay the request with X-Fire-Arrow-Debug: true