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:
- Reads
Authorization: Bearer {token}from the request header - Verifies the JWT RS256 signature using this server's public key
- Extracts the
scopeclaim - Checks the requested FHIR resource type and HTTP method against the granted scopes
- Throws
ForbiddenOperationExceptionif access is not permitted
Where it runs
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.
@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)
| Scope | Permits |
|---|---|
patient/Patient.rs | Read and search Patient resources |
patient/Condition.rs | Read and search Condition resources |
patient/MedicationRequest.rs | Read and search MedicationRequest resources |
patient/*.rs | Read and search all patient resource types |
patient/Patient.cruds | Full access — create, read, update, delete, search |
Operation letter mapping:
| HTTP method | Letter checked |
|---|---|
GET | r (read) |
POST | c (create) |
PUT | u (update) |
DELETE | d (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 →