Access Control in Vonk

TL;DR

Vonk provides Access Control based on OpenIDConnect and the claims as defined by SMART on FHIR. It can be used in Vonk FHIR Server and Facade.

May I borrow your bike?

Last week a neighbor of ours wanted to borrow a bike. He had planned for a ride with friends in the beautiful Dutch polder landscape. I like nature a lot, and birds in particular, so I totally encourage people to enjoy it too. Go ahead! Borrow the bike! The sun came out and they all had a wonderful afternoon.

So I gave my neighbor access to my bike. What factors did I take into account to make this decision? First of all I could authenticate him. Apart from the details of human face recognition that was trivial, since I know him for quite some time already. He *claimed* he would use the bike for recreational use on normal roads. I had no means to check that, so I could only trust him on that. Which I did.

But would I also give him access to my medical record? Well, certainly not all of it. But just my demographic data? He obviously already knows where I live… And my allergies? I tell everyone remotely involved in what I eat that I can’t have gluten. This requires fine grained access control. So we built that into Vonk.

Standardized Access Control to Clinical Data

Vonk can be your FHIR interface to data all across your organization. Use Vonk FHIR Server as a Clinical Data Repository. Or fit Vonk FHIR Facades to your source systems and provide real time access. The key to Vonk Access Control is that we designed it so that it works across all data access layers. Yours (in a Facade) or ours (in a Server), it does not matter. This way Vonk can not only provide a uniform FHIR interface, but also a uniform way of defining access rights.

SMART

Not much has been standardized in the area of FHIR and Access Control. SMART on FHIR is primarily about launching apps in the context of an EHR and have them access data from the EHR through a FHIR REST API. I think that is the best start we have today, so we implemented Vonk Access Control based on that. And extended a bit on it as well. Keywords: OpenIDConnect, OAuth2, read/write claims, launch context. If that is really not enough for your use case, you can also implement the raw interfaces yourself. But that is beyond the scope of this blog.

Access Control steps

Access Control broadly consists of four basic steps:

  • who are you (identification);
  • can you prove that (authentication);
  • what rights are granted to you (authorization);
  • what can you do in Vonk with those rights (access control decision).

The first two steps are not part of Vonk. We expect you to already have your users registered in some system like an Active Directory, a user portal, or an EHR system. The key to using Access Control in Vonk is that this system can hand out JWT Tokens.

What rights can be granted to you is defined by SMART on FHIR in terms of claims.  It’s the scopes like patient/*.read and user/Observation.write. Along with a launch context which like patient=123. These claims will be recognized by Vonk. However, the claims must be put in the token by the token issuer. Which may be your Active Directory, user portal or EHR system. And the client app that wants to request data from Vonk needs to ask for the correct claims needed to do it’s job. This requires careful configuration of your Identity Management system. Vonk (or actually SMART) defines which claims you can provide in JWT Tokens, but not how to do that in your system.

Access Control Decision

The final bullet is about how Vonk processes the claims and how it decides upon accessing specific data. The scope claims that define access to resourcetypes are fairly straightforward. The launch context however is expanded by Vonk to a ‘compartment’ – a definition of the set of resources that you have access to. The compartment for a patient launch context is defined by:

  • the patient with the id from the context;
  • all resources connected to that patient by the Patient CompartmentDefinition (e.g. Observation, AllergyIntolerance);
  • all resources of types that are not part of the Patient CompartmentDefinition (e.g. Organization).

Let’s discuss this by some example FHIR interactions.

GET [base]/Patient/123

The token must contain the claim scope=user/Patient.read or scope=user/*.read, or scope=patient/Patient.read or scope=patient/*.read. In the latter case, a launch context ‘patient’ must also be present to know which patient we’re talking about: patient=123. If the id in the launch context matches the id requested (123 in this case), the result is returned as usual. Otherwise the requested patient is not in the ‘compartment’ defined by the launch context, and the response will be a 404 (not found). Note that we don’t return 401 (Unauthorized) because that would still disclose that there *is* a patient with that id.

GET [base]/Patient?name=Fred

For this you need the same claims as for the read interaction above. If a launch context of patient=123 is present, the search result will be filtered with an extra parameter &_id=123. So if patient 123 happens to be called Fred, you’re lucky.

GET [base]/Observation?patient.name=Fred

First of all you must be allowed to read Observation resources (user/Observation.read; user/*.read; patient/Observation.read; patient/*.read – the latter two again with a mandatory patient launch context). This request, however, has a searchparameter ‘patient’ that references a Patient. So you need access to the resourcetype Patient as well, just as in the examples above. If you don’t have access, Vonk will not consider the argument and (by lack of further arguments) return all Observation resources. If a patient launch context is present (patient=123), this will again be added as an extra filter so the user can never reach resources outside of the defined compartment. This is not only enforced on chained searches, but also on reverse chaining (_has), includes and reverse includes.

PUT [base]/Observation/456

First of all, you must be allowed to read the current version of Observation/456. This is similar to the previous example, but with ?_id=456. Then you need a scope that allows you to write an Observation (user/Observation.write; user/*.write; patient/Observation.write; patient/*.write – the latter two again with a mandatory patient launch context). Vonk will check whether the new version of the Observation (from the body you sent) is still in the compartment defined by the launch context. If so, the update is authorized. If not, you apparently try to move this Observation to a patient that you have no access to and you won’t be allowed to do that.

Let Vonk handle it

As you can see in the examples above, there is a lot to take care of when enforcing access control. If this is more than you would like to know about it, just skip it and simply remember that if you make sure the user gets the correct claims in his token, Vonk will take care of the rest. And REST, yes.

One step beyond

The SMART on FHIR specification tells us that the value of a launch context is an id, for example: patient=123. This means that you can limit access to exactly 1 patient at a time, and it’s connected resources.

But with Vonk we thought we could offer you more. So we enable you to define access based on any property of a patient. Or even of a connected resource. For example by an identifier of a general practitioner. Then the claim “patient=GP98.34” is interpreted as allowing access to all patients with GP98.34 as their general practitioner. Might be useful if you provide a portal to the GP’s in the region. The documentation explains how you can configure the Filters to accomplish this.

In the future we may even add additional lauch context claims to make this meaning more explicit.

Opinions?

Access Control is both important and hard to get right. So if you have any opinions or ideas about our implementation: We always welcome feedback. Meet us at vonk@fire.ly or on chat.fhir.org.

Try it

You can easily try Vonk by downloading it from our Simplifier.net platform, or running the Docker image. Details are in the Getting Started section of the documentation. We even provided an Identity Server for you to mock the necessary token. Or be bold and enable the access control in your own facade.

Comments

  1. Chris Grenz on said:

    Great stuff Christiaan! Comment: I’d argue that a request for a patient record outside the allowed scope should return a 403 regardless of whether the patient exists or not. Technically, this makes sense as the authorization step happens prior to any data access, and semantically this makes sense since the reason for the error is authorization, not data availability. It does NOT reveal any information about the presence or absence of a record since the 403 would be the same in either situation.

  2. Christiaan Knaap on said:

    Thanks Chris!
    Note: I prefer to use ‘launch context’ or ‘compartment’ here instead of ‘scope’, since ‘scope’ is already used for the scope claims in the tokens (like ‘user/*read’).
    We translate the compartment into criteria on the database query, so we still need only one query (performance reason) and we can’t forget to filter out data afterwards (security reason). So the authorization step does not happen prior to data access, it happens during data access. On a search this implies we simply return the resultset limited to your compartment. And on a read this translates to a a 404 if the resource you try to read does not match the criteria from the compartment. And as far as your authorization goes, it is indeed not there.
    Of course I’m happy to discuss this in Cologne in May 🙂

Post a comment

Your email address will not be published. Required fields are marked *