Skip to main content

Scope Enforcement Interceptor

What it does

SmartScopeAuthorizationInterceptor is a HAPI FHIR server interceptor that fires on every incoming FHIR request and enforces SMART scope rules:

  1. Reads Authorization: Bearer {token} from the request header
  2. Verifies the JWT RS256 signature using this server's public key
  3. Extracts the scope claim
  4. Checks the requested FHIR resource type and HTTP method against the granted scopes
  5. Throws ForbiddenOperationException if access is not permitted

Where it runs

warning

This interceptor runs on the HAPI FHIR JPA server (port 8080), not on this auth server (port 9000). It must be registered in the HAPI server's FhirServerConfig.

In your HAPI FHIR server project
@Bean
public SmartScopeAuthorizationInterceptor smartScopeInterceptor(RSAKey rsaPublicKey) {
return new SmartScopeAuthorizationInterceptor(rsaPublicKey);
}

The RSA public key is fetched from GET http://localhost:9000/oauth2/jwks and loaded as a RSAKey.

SMART scope grammar (v2)

ScopePermits
patient/Patient.rsRead and search Patient resources
patient/Condition.rsRead and search Condition resources
patient/MedicationRequest.rsRead and search MedicationRequest resources
patient/*.rsRead and search all patient resource types
patient/Patient.crudsFull access — create, read, update, delete, search

Operation letter mapping:

HTTP methodLetter checked
GETr (read)
POSTc (create)
PUTu (update)
DELETEd (delete)

Search (GET /fhir/Condition?patient=X) also maps to r — the interceptor does not distinguish read vs search at the HTTP level. For operation-level r vs s distinction, use the Consent Manager alongside this interceptor.

Scope matching logic

private boolean scopePermits(String scope, String resourceType, String requiredOp) {
if (!scope.startsWith("patient/")) return false;

String rest = scope.substring("patient/".length());
String scopeResource = rest.substring(0, rest.indexOf('.')); // "Patient" or "*"
String scopeOps = rest.substring(rest.indexOf('.') + 1); // "rs", "cruds"

boolean resourceMatches = "*".equals(scopeResource)
|| scopeResource.equalsIgnoreCase(resourceType);

return resourceMatches && scopeOps.contains(requiredOp);
}

Error response

When a scope check fails, HAPI returns an OperationOutcome:

{
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "forbidden",
"details": {
"text": "Insufficient scope for GET on Observation. Required: patient/Observation.r or patient/*.rs"
}
}]
}

Missing token

If no Authorization header is present, the interceptor throws AuthenticationException (HTTP 401):

{
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "login",
"details": {
"text": "Authorization header missing or not Bearer — all FHIR requests require a SMART access token"
}
}]
}

Next: Patient Picker Portal →