Security Recipes - Smile CDR Documentation

Security Recipes

This page provides examples of how to configure Smile CDR for specific security designs.

App: Local Users with HTTP Basic Authentication

This example illustrates how to set up a simple application. In this example, we're using Smile CDR's database for both resource storage as well as user storage and authentication. We will set up two users:

The following diagram illustrates this setup.

Setting Up Modules

If your configuration does not already have one, first you should create a Local Inbound Security module. Configure this module with appropriate password encoding settings for your needs.

Create a FHIR Storage (Relational) module for the version of FHIR you are looking to support.

Create a FHIR Listener module for the version of FHIR you are looking to support.

The FHIR REST endpoint should be configured with HTTP Basic security enabled, and the Username/Password Authentication dependency pointed to your Inbound Security module. This is shown in the screenshot below.

Defining Permissions: App Admin

We will define permissions for an administrative user, who is allowed to perform any function on the repository. This user can create, update, and delete resources as required.

To set this up, grant the user the following permissions:

Permission Argument Explanation
ROLE_FHIR_CLIENT (n/a) Allow user to be a FHIR client.
ROLE_FHIR_CLIENT_SUPERUSER (n/a) Allow user to perform any FHIR tasks.

Defining Permissions: Regular User

We will then define permissions for a user who is authorized to be a FHIR Client, and who can read any resource of types ValueSet and CodeSystem. Aside from that, the user can only read resources within a specific Patient compartment. This means that the user can access general administrative data (ValueSet and CodeSystem do not not typically contain sensitive data).

The user is blocked from reading other data that isn't in a specific Patient compartment to which they have access. The Patient compartment belongs to a specific Patient resource (e.g. Patient/123), and the only resources that can be read are limited to that specific patient instance as well as other resources with an explicit reference to that resource from one of the reference types defined by FHIR's Patient compartment (e.g. "Observations with the given patient as the subject" and "MedicationRequests with the given patient as the subject").

To set this up, grant the user the following permissions:

Permission Argument Explanation
ROLE_FHIR_CLIENT (n/a) Allow user to be a FHIR client.
FHIR_READ_ALL_OF_TYPE ValueSet Allow user to read any ValueSet resources.
FHIR_READ_ALL_OF_TYPE CodeSystem Allow user to read any CodeSystem resources.
FHIR_READ_ALL_IN_COMPARTMENT Patient/123 Allow user to read any Patient, Observation, QuestionnaireResponse, ServiceRequest, etc. that belong to the patient Patient/123.

Calling the API

Now that you have a FHIR Endpoint that has been secured using HTTP Basic Authentication, you will need to add an Authorization header to your requests.

Here is an example request showing the Authorization header:

GET /Patient/123

Copy

The [credentials] part above is a base64 encoded string in the format username:password. For example, the username admin and password password you would result in the following header:


Copy

The base64 command can be used to base64 encode the credentials. For example:

echo -n "admin:password" | base64
YWRtaW46cGFzc3dvcmQ=

Copy

Integrating SMART Apps With a 3rd-Party OIDC Server

In this example we are authorizing data requests to Smile CDR using a 3rd-party Identity Provider, or more specifically a 3rd-party OpenID Connect server. This might be a software package such as MitreID Connect or Keycloak, or it could be a cloud service such as Okta or Ping Identity.

The proposed architecture here is shown below:

Background And Terminology

To keep things simple, we'll refer to 3 roles in the example that follows (see SMART on FHIR Participants for more details on these roles):

The OpenID Connect protocol relies heavily on cryptographically signed assertions about the user that are included in the token(s) which are passed from system to system. A simple way of thinking about these assertions is follows:

  1. The Auth Server creates a small JSON document with elements that states "The App that I gave this document to is logged in as John Smith, which corresponds to the FHIR Resource Patient/A123. John has granted the app permission to perform read operations but not write operations."
  2. The Auth Server uses a key to digitally sign the document. This same key is also distributed to the Resource Server so that it can verify the signature (this form of verification is known as "Shared Secret" verification and is an alternative to other forms such as RSA public/private keypairs which are also often used). This means that anyone who holds this token can digitally verify that the token was actually created by the Auth Server and that it was not modified.
  3. The Auth Server gives this signed document (which is in fact a JSON Web Token to the App. This signed document is now called a token. The app needs to keep the token secure since it was issued specifically for that app.
  4. The App includes the token in FHIR requests that it makes to the Resource Server.
  5. The Resource Server uses the public key from the Auth Server to verify that the token is genuine and has not been tampered with and uses the data contained within it to decide what operations should be allowed by the user.

Scopes and Claims

The token, which is a signed JWT, contains a number of specific "claims", which are actually just fields within the JSON document. These fields identify the system that issued the token, metadata about the token and the session, and often details about the user.

A very simple example of a JWT follows:

{
    "sub":"myusername",
    "azp":"my-client-id",
    "scope":"openid profile patient\/*.read",
    "iss":"http:\/\/localhost:19202",
}

Copy

When this JWT is signed for sharing, it is also base64 encoded, resulting in a string resembling the following (although the real string is generally much longer):

yJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJteXVzZXJuYW1lIi.9w3r9fy8czoy8YYUy

Copy

The OIDC protocol specifies that apps may request scopes, which are a mechanism for declaring specific permissions that the App has (in other words, they declare that the App has the ability to do specific things on the user's behalf). The SMART on FHIR specification declares a set of healthcare specific scopes, such as patient/*.read (app may read but not write any resource data about the current patient). These scopes are placed in a claim called "scope".

While scopes are used to convey App permissions, the OIDC protocol does not mandate any specific mechanism for conveying User permissions. In our example here we will use additional custom claims that are used to convey the linkage between the user and the FHIR resource they are allowed to access.

User Permissions

Conveying the details of what the user can do is probably the least intuitive part of this flow. This is mostly because the OIDC and SMART on FHIR specs aim to cover the authorizing of Apps, but leave the authorizing of Users up to the individual implementation.

In our example, we are going to add a single custom claim to the generated token called "patient" containing the resource ID that corresponds to the logged in user. In other words, the Auth Server will include a custom claim such as "patient": "A123" and this will be interpreted by the Resource Server as an assertion by the Auth Server that the logged in user is allowed to access any data belonging to the FHIR resource Patient/A123. This might include the Patient resource itself, but also Observations, Encounters, etc. that refer to the given patient.

This type of authorization is simplistic and can be expanded upon in many ways, but it serves to demonstrate the concept of authorizing patient-facing apps to allow patient users to access their own data without granting access to other users' data.

Step 1: Configure The Auth Server

For our example, the Auth Server is configured with the following settings. The mechanism for achieving these settings will vary from provider to provider but the general concepts should exist in every provider.

Key

In our example here, we will use the following HS256 shared secret key. There is no requirement to use this form of signature, and you should absolutely generate your own key for any real scenarios but this key will work for testing. The format below is known as a JSON Web Key (JWK), and the value of the "k" attribute is the actual key contents.

{
  "kty": "oct",
  "use": "sig",
  "kid": "my-app-key",
  "k": "cdl6q83k7VNc-LtSzi_VzR3BvnTWmFGuQPuscZ45IOtVIb178ZwWwc65M1yhd-3Mmp_kxoe8_r_s5TN3pLOHYHynkAgavmJJHG-YDc6ksVD1cAQMXlL90BXfFuL0ewDCqgghkTd-qCGavGbi4rwqFOQp5QxbfT1-SK9q7-IgGOANVKiPIAS-bup1f0QwPUU3k9hNP5gF9dWHk4nK9KBZkO_iGXjrc1tv5gCGi1kKk9R4BAK0bHvDfQjR1ryl_Jkt_qdodd9cuyFr_lS6Rd7VsmnnEYEpwbleYtkcjqBJRvR86Ka6BQgzdSyBp3Lel2mL23X6gAw1TVT_jERPBmqBpg",
  "alg": "HS256"
}

Copy

Issuer

Any OpenID Connect server needs an Issuer URL. This URL serves two purposes: First, it is used as a unique identifier to actually indicate to the Resource Server which Auth Server actually issued the token. Second, it can be used as an actual resolvable URL by the App or Resource Server to introspect the token, meaning to invoke service endpoints on the Auth Server to query for more information about the token or the server.

In our example, we will use the issuer URL of: https://identity.example.com:9200

Client Definition

We will need to create a definition for the client (i.e. the App) in the identity server. This definition should include the following details:

Step 2: Create an Authentication Script

The following is an example of an Authenticaton Script.

In this script, we examine the "patient" claim, and if it exists we will grant the user full read/write permissions for any data in the given patient's "compartment" (compartments are a FHIR concept indicating resources that are deemed to "belong" to the given Patient). We will use scopes to indicate whether a given app should be allowed to read and/or write, so despite giving the user full permissions here, these rights will be reduced when a token is processed based on the approved scopes.

function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {

// Just log a bit about the user logging in.
   var username = theOutcome.username;
   var remoteAddress = theContext.remoteAddress;
   Log.info('User ' + username + ' is logging in from ' + remoteAddress);

// Authorize access based on the "patient" claim.
   var patient = theContext.getClaim("patient");
   Log.info("Have access to patient: " + patient);
   theOutcome.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + patient);
   theOutcome.addAuthority('FHIR_WRITE_ALL_IN_COMPARTMENT', 'Patient/' + patient);

return theOutcome;
}

Copy

Step 3: Create a SMART Inbound Security module

If you do not already have one created, you should now create a SMART Inbound Security module in your Smile CDR module configuration. Give this module an ID, and then paste the script from step 2 into the Authentication Callback Script (Text) box. No other settings are required.

Step 4: Configure the FHIR Endpoint module

In your FHIR Endpoint module you will want to:

Step 5: Create an OIDC Server Definition

Under Config OpenID Connect Servers you should create a server definition. This definition corresponds to your Auth Server.

Grant the server an Issuer ID of https://identity.example.com:9200 and paste the JWK (the full JSON document) from step 1 above into the text box labelled Key (Text) and then hit save.

No other settings are required.

Testing This Setup

If you are able to, you can now request a real token from your Auth Server. For the purposes of demonstration, we'll go through an example that should also work with the given configuration.

The following JSON document is a JWT claim set containing the required claims for the setup described here:

{
    "iss": "https://identity.example.com:9200",
    "iat": 1573065974,
    "exp": 1604688374,
    "aud": "https://fhir.example.com:8000",
    "sub": "jsmith123",
    "scope": "openid profile patient/*.read",
    "family_name": "Smith",
    "given_name": "John",
    "patient": "A123"
}

Copy

When signed using the key above, this claim set results in the token below. Note that if you want to try modifying the claim set above and generating your own new token for testing you can do so using this tool. If you want to try parsing and verifying the signed token below, you can do so at JWT.io.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2lkZW50aXR5LmV4YW1wbGUuY29tOjkyMDAiLCJpYXQiOjE1NzMwNjU5NzQsImV4cCI6MTYwNDY4ODM3NCwiYXVkIjoiaHR0cHM6Ly9maGlyLmV4YW1wbGUuY29tOjgwMDAiLCJzdWIiOiJqc21pdGgxMjMiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIHBhdGllbnQvKi5yZWFkIiwiZmFtaWx5X25hbWUiOiJTbWl0aCIsImdpdmVuX25hbWUiOiJKb2huIiwicGF0aWVudCI6IkExMjMifQ.un-o5De6VvmCUcuA8usKYzQmIGP95PTUYs78pxxm8gs

Copy

You may now invoke your FHIR endpoint using a request such as the following:

GET Patient/123

Copy

Extra Authorization Script Features

The following snippet shows the authorization script above with a few more examples of possible interactions.

Note that method getClaim() can be used on theContext object to get any claim in the JWT, of any data type. Null will be returned, if no claim exists.

Alternatively, method getClaims() can be used to get all claims from the token as a Map object.

function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {

// Example of granting permissions based on specific claims. We'll
   // give users with the "ehr_admin" claim full superuser rights, and
   // we'll give users with the "ehr_user" claim access to all patient
   // data.
   if (theContext.getClaim("ehr_admin") === true) {
      theOutcome.addAuthority('ROLE_FHIR_CLIENT_SUPERUSER');
   }
   if (theContext.getClaim("ehr_user") === true) {
      theOutcome.addAuthority('FHIR_READ_ALL_OF_TYPE', 'Patient');
   }

// Example of adding permissions based on scopes
   if (theContext.getApprovedScopes().contains('view-claims')) {
       theOutcome.addAuthority('FHIR_READ_ALL_OF_TYPE', 'Claim');
   }

// Authorize access based on the "patient" claim
   var patient = theContext.getClaims()["patient"];
   Log.info("Have access to patient: " + patient);
   theOutcome.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + patient);

return theOutcome;
}

Copy

Consent Service: Java API

Security Recipes

Security

Authentication Protocols Authorization and Consent Inbound Security Module Local Inbound Security Module LDAP Inbound Security Module Scripted Inbound Security Module SAML Inbound Security Module Trusted Client Mode Roles and Permissions Callback Scripts Anonymous Access Consent Service Consent Service: JavaScript API Consent Service: Java API Security Recipes Two Factor Authentication Password Hashing Algorithms JWT Signature Algorithms Troubleshooting Security

Two Factor Authentication