Skip to content

1. Design and functionality

serefarikan edited this page Apr 9, 2021 · 2 revisions

Introduction

This plugin changes the behaviour of Keycloak to support the scenarios described by SMART On FHIR specification.

Please read the SMART ON FHIR (SOF) specs for the specifics, but basically, this specification uses OAuth2 (and OIDC, which is built on OAuth) extensively. This means that information is communicated between SOF applications and an EHR platform using OAuth scopes and OAuth claims (scopes are requested, claims are returned by the OAuth server).

This approach makes the OAuth component of the EHR, i.e. the Authorisation server act like the orchestrator of flows. The meaning of the term EHR in USA is different than its meaning in Europe and most of the world. In USA, EHR is a term used to refer to actual products: massive clinical information systems (usually hospital information systems) such as CERNER, EPIC, Allscripts etc. The definition of platform from a SOF perspective is therefore these systems and SOF assumes these systems provide an OAuth based authentication/authorisation mechanism. In our case, the Authorisation component of our platform is Keycloak and it is supposed to provide the SOF defined features/behaviour.

At a high level, how Keycloak behaves in a SOF scenario depends on which SOF application type we're supporting. For example, SOF defines applications integrated to an EHR: a user clicks a link while looking at a CERNER screen/app/UI and an application is launched in the context of the EHR session, i.e. if there is a selected patient whose information is displayed on the screen, then the app launches with that context. This operation uses OAuth to send the current EHR context (the selected patient, the user viewing their info etc) in the form of OAuth scopes, and OAuth server is supposed to respond with relevant access and identity tokens which contain custom claims. These claims can be things such as the fhir patient resource id, some suggestions to launching SOF application: extra information to display on the screen when the app is launched etc etc.

The initial version of this plugin implements the simplest SOF app: a standalone patient application. Standalone means our application will launch not from within an EHR (a product in US speak) but on its own. So it'll send some scopes to Keycloak and the returned access token will include some information that'll help the app figure out where the patient resource is etc. Since the same tokens will be mandatory to access APIs of the platform, they not only allow the app to figure out who the user is, but they also allow APIs to limit access to data based on who the patient is.

All of the above requires that Keycloak can talk to other components of the Central Transactional Repository (CTR) platform. For example:

  • In order to figure out which fhir patient resource the app user (patient) corresponds to, there must be an association between the unique Keycloak user id and fhir resource id. Keycloak can establish this association by creating the fhir resource when the user is registered.
  • In order to make sure that fhir patient resource is also associated to an openEHR EHR in EhrBase, Keycloak can also create the EHR during user registration, along with FHIR resource etc.

To implement the above functionality, we need a Keycloak plugin. This plugin allows us to intercept Keycloak processes and read scopes and write claims, while making calls to other systems. This is exactly what our plugin does:

  • Intercept relevant events
  • Read scopes
  • Make calls to demographic and other components of CTR if necessary
  • Write claims into tokens so that both applications and APIs can use those claims and the token itself.

This is both a complex and capable approach, since capability is seldom available without paying the price of complexity.

Another important requirement for our plugin is that it must not intercept non SOF applications, which CTR must also support. Therefore, there are various configurations at the Keycloak configuration/administration level, which makes sure that our plugin won't attempt to write patient resource ids to access tokens when users of a non SOF application are authenticating.

The big picture representation of this design is depicted in the following diagram:

Implementation

The implementation of the plugin is based on the SPI based framework of Keycloak. Please see the server developer documentation of Keycloak to get a better understanding of the particular extension points it supports.

As explained above, this plugin's primary functionality is to intercept the processes that take place when Keycloak receives a request to provide a token and change Keycloak behaviour. In this context, the most important types in the code base are the following ones:

  • SmartPatientAppAuthenticator
  • SmartOnFhirEventListenerProvider
  • ClientSessionNoteMapper

SmartPatientAppAuthenticator type handles authentication requests from the login form provided to a user. So when a user of a SOF app without a Keycloak session (no login yet, or an expired token, with no valid refresh token at hand) attempts to access a secured resource, they're redirected to a login form and upon successful login, they end up in the authenticate method of this type. This method checks if the user's request contains a SOF related scope, such as launch/patient and if it does, it then queries the demographic service and finds the patient resource id. It then places this information into session, using the client note mechanism of Keycloak. This mechanism allows us to place some data into user's session which can be accessed from various points during request processing.

During authentication from a login form, the fhir patient resource id placed into the authentication session (client notes) will be picked by ClientSessionNoteMapper type, and it'll be written into access token as a claim. Therefore, these two types communicate with client notes mechanism. This communication won't happen just by deploying the plugin, the types in the plugin must be configured and activated from Keycloak management UI or via Keycloak admin REST API

However, there is a prerequisite for the previous scenario to work: we must have FHIR patient resource in the demographic repository, and we must also have an openEHR EHR associated to the user who's logging in (for clinical data to be stored), and this EHR must be associated to patient resource.

This means that we must make some calls when the user is registered for the first time. We accomplish this by using the SmartOnFhirEventListenerProvider type. This event listener listens to registration events and checks to see if the registering user is a SOF app user (by checking if the request contains SOF claim(s)). If that is the case, it then creates the fhir patient resource and openEHR EHR by making REST calls to relevant endpoints. However, these calls are not made in a straightforward manner in the current code. This is due to two bugs in Keycloak 11.

One of these bugs (arguably not a bug but an inconvenient design) is that user details provided in the registration form are not available when our plugin intercepts the event. This means that we cannot create a fhir patient resource because we do not have some information we may want to put into the resource, such as user's email, even username etc. This requires us to use the transaction mechanism of Keycloak so that we attach a transaction step to the process which will take place at the right time, when the user information is available. VitaKeycloakTransaction allows us to do that. However, we still have a problem: there is another bug in Keycloak, which omits the values written into session notes, if those values are written from a transaction. So even if we have access to registration form info, and we create a fhir resource, placing that fhir resource id into session notes so that it can be written to access token later by ClientSessionNoteMapper won't work. ClientSessionNoteMapper just won't see the value in session notes. So the code implements a workaround by using an in memory, least-recently-used-evicting map to connect various steps. See the comments in the code for details.

Even though ClientSessionNoteMapper's only role is to write claims to tokens, in case of registration, it also performs the tasks of writing patient resource id into session notes, because otherwise using the refresh token obtained from the initial registration won't be able to find this info in the session.

Leaving this detail aside, the authentication handler and event listener mechanisms allow our plugin to put information into tokens. This mechanism can be extended to any information we'd like to put into tokens. We can also refuse login, or cancel registration based on calls made to other systems, such as the ABAC server etc. The high level idea is to use session notes and write information to tokens by reading from the session notes.

Clone this wiki locally