
Access Control in Firely Server
Subscribe to our newsletter
SubscribePlease note that Firely Server was formerly called Vonk.
TL;DR
Firely Server provides Access Control based on OpenIDConnect and the claims as defined by SMART on FHIR. It can be used in Firely Server Facade and FHIR Server.
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 Firely Server.
Standardized Access Control to Clinical Data
Firely Server can be your FHIR interface to data all across your organization. Use our FHIR Server as a Clinical Data Repository. Or fit Firely Server FHIR Facades to your source systems and provide real time access. The key to Firely Server 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 Firely Server 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 Firely Server Access Control based on that. And extended a bit on it as well. Keywords: OpenIDConnect, OAuth2, read/write claims, launch context.
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 Firely Server with those rights (access control decision).
The first two steps are not part of Firely Server. 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 Firely Server 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 Firely Server. 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 Firely Server needs to ask for the correct claims needed to do it’s job. This requires careful configuration of your Identity Management system. Firely Server (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 Firely Server 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 Firely Server 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, Firely Server 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). Firely Server 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 Firely Server 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, Firely Server 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 Firely Server 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 server@fire.ly or on chat.fhir.org.
Try it
You can easily try Firely Server 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.
2 thoughts
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.
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 🙂