JSON Scopes for FHIR

(also posted here).

Summary: Discussing an example implementation of JSON OAuth scopes for controlling access to FHIR resources and how wildcard and negative scopes were added for the flexibility required by some use-cases.

As more complex applications use OAuth 2.0, some use-cases call for more detailed scopes to express what a client is permitted to do and to what extent. In a previous post I argued why it is important to use JSON for modelling such scopes instead of encoding data structures into plain strings using arbitrary grammars as proposed, for example, by SMART. In this post I will show an example of using JSON scopes in Pauldron Hearth and how the JSON structure was naturally extended to support wildcard and negative scopes for more flexibility.

Pauldron Hearth: A Practical Example of JSON Scopes

Pauldron Hearth (a reverse HTTP proxy for protecting FHIR servers using Pauldron) uses a JSON scope structure similar to the following:

{
"resource_set_id": {
"patientId": {
"system": "urn:official:id",
"value": "10001"
},
"resourceType": ["Specimen"],
"securityLabel": {
"system": "http://terminology.hl7.org/ValueSet/v3-ConfidentialityClassification",
"code": "N"
}
},
"scopes": ["read"]
}

This example, grants read access to all Specimen resources with the normal (i.e. non-restricted) confidentiality belonging to the patient with the particular identifier.

One of the reasons for using this structure was to be compatible with the permission structure as defined by the User-Managed Access profile of OAuth which includes a resource_set_id and a scopes array (see footnote). This enables Pauldron Hearth to interchangeably support UMA permissions and OAuth 2.0 scopes as discussed further below.

Wildcard and Negative Scopes

Issuing access tokens in UMA hinges on the UMA ticket presented by the requesting client, which is essentially a reference to a set of permissions/scopes previously registered by the resource server with the authorization server. In UMA, it is up to the resource server to figure out all the permission/scopes implied by a client’s query and inform the authorization server about them via permission registration. So, when a client requests an access token in this case, the authorization server already knows what scopes are being requested based on the registered permissions associated with the presented ticket.

Pauldron also supports issuing access tokens based on Section 4.4.2 of the OAuth 2.0 Specifications, sometimes referred to as the two-legged OAuth. In this mode, there is no ticket or prior registration of scopes with the authorization server and the client must explicitly specify the requested scopes when requesting an access token.

So, unlike UMA in which the client does not need to know the scopes/permissions implied by its request or even be aware of what their structure looks like, in the two-legged OAuth 2.0 mode, the client needs to know exactly what scopes are implied by its upcoming queries and explicitly ask for them when requesting an access token. This can be tricky because it requires prior knowledge by the client about its access rights according to the policies, the details of the scopes, and the type of information which may appear in the response to its future queries. Without such knowledge, it is almost impossible for the client to determine a precise set of scopes to request.

In such cases, it is very helpful if the client can request a maximal approximation of what it believes will suffice for its future queries and leave it up to the authorization server to refine the granted scopes after consulting the applicable policies. This is why Pauldron implements wildcards and negative scopes, so that a client can request a broad set of access rights while the authorization can refine such scopes by adding exceptions in the form of negative scopes which deny a specific pattern of access.

For example, the client can request the following scope:

{
"resource_set_id": {
"patientId": {
"system": "urn:official:id",
"value": "10001"
},
"resourceType": ["Specimen", "Encounter"],
"securityLabel": "*"
},
"scopes": ["read"]
}

The arrays indicate any of the listed values and the asterisk is interpreted as any value. So, in this case the client is requesting read access to Specimen and Encounter resources with any security labels belonging to the particularly identified patient.

Now, consider the case that based on the applicable policies, the client cannot be granted access to restricted resources (of any kind and belonging to any patient). In response, the Pauldron server can issue an access token associated with the following scopes, including a negative scope which makes an exception to the broader granted scope:

[
{
"resource_set_id": {
"patientId": {
"system": "urn:official:id",
"value": "10001"
},
"resourceType": ["Specimen","Encounter"],
"securityLabel": "*"
},
"scopes": ["read"]
},
{
"deny": true,
"resource_set_id": {
"patientId": "*",
"resourceType": "*",
"securityLabel": {
"system": "http://terminology.hl7.org/ValueSet/v3-ConfidentialityClassification",
"code": "R"
}
},
"scopes": "*"
}
]

The second scope in the array includes a deny attribute set to true which indicates that it is a negative scope and prohibits any type of access to any resources for any patient which bears a restricted (R) confidentiality label.

The ability to use negative scopes also makes it easier to express and evaluate authorization policies since a lot of policy rules are easier as a general grant with an exception. For example, it is much easier and more intuitive to say “allow access to everything except restricted resources,” instead of determining an exhaustive list of all other security labels to which access is granted.

Wildcards and negative scopes also make it easier for the authorization server to evaluating policies and determine the granted scopes without knowing too much about their semantics and value sets. For example, without these mechanisms, denying access to a particular type of resource would require enumerating the list of all the other resource types to which access is permitted. Aside from the length of the resulting scope structures, the authorization server would also have to somehow be informed of the list of all possible resource types, security labels, or actions. This would introduce an undesirable coupling between the authorization server and domain-specific scope semantics and may even be impractical considering the large number of possible values and the dynamic nature of these value sets and their frequency of change.

Understanding and Enforcing Scopes

Using wildcard and negative scopes means the resource server will have to implement some authorization logic so that it can determine whether a specific instance of access is granted by the pattern of granted and denied scopes. This involves the matching logic for wildcard scopes and the resolution logic to correctly accommodate negative scopes.

Pauldron Hearth follows a simple logic here: if access is explicitly prohibited by a negative scope it is denied; if it is explicitly permitted by a granted scope, it is allowed; otherwise it is denied (this logic is not very complicated as you can see in the source code of Pauldron Hearth module which implements it here).

It is important to note, however, that JSON scopes, especially with the addition of wildcard and negative scopes, are essentially a simple authorization language for expressing the access rights of a client and as they get more complicated and expressive, more authorization logic must be implemented by the resource server to understand, interpret, and enforce them. So, the caveat here is that moving towards excessively expressive scopes could gradually shave portions of the authorization logic off of the authorization server and smuggle them into the resource sever which is contrary to the initial raison d’être of the authorization server as a central place to handle all the authorization logic.


* Note that there is some terminology confusions between UMA and OAuth 2.0 here. While in OAuth 2.0 the authorization service grants a set of scopes to the client, in UMA, the authorization service grants a set of permissions each of which includes a scopes array which confusingly overloads the term scope. In Pauldron Hearth, admitting this confusing terminology was the price to pay for interchangeably supporting both OAuth scopes and UMA permissions; as far as Pauldron is concerned, UMA permissions are simply JSON scopes with a particular structure.