On this page:

7.14SMART on FHIR Outbound Security Module

 

The SMART Outbound Security module is a complete SMART on FHIR compliant Authorization Server. It is able to accept authorization requests from SMART Applications, grant access tokens on behalf of users, and manage these Applications.

7.14.1Setting Up This Module

 

Setting up this module requires several key configuration items.

Username/Password Authentication

This module uses an Inbound Security module such as the Local Inbound Security Module or the LDAP Inbound Security Module in order to actually validate user credentials. This module is specified as a dependency in the SMART on FHIR Outbound Security module configuration.

JSON Web KeySet (JWKS) for Signing Tokens

In order to generate OpenID Connect Access Tokens, this module requires a JSON Web Key Set (JWKS). This keyset is used to sign the tokens that are generated so that Resource Servers may verify and ultimately trust that they are legitimate.

Note that anyone who is in possession of the key used here is able to generate access tokens. For this reason it is very important that the key be safely stored.

There are a number of tools that can be used to generate JWKS files. One popular option is the MKJWK tool maintained by the MIT Kerberos and Internet Trust. The steps below show how to create a JWKS file using the online version of their tool; however, for increased security you may wish to download a copy and use it locally:

  • Navigate to https://mkjwk.org/
  • Select the RSA tab with a key size of 2048 (this is the default, which should be sufficient for most purposes)
  • Select a Key Use of Signing
  • Select an algorithm (the default of RS256 should be sufficient)
  • Enter a meaningful key ID ()e.g. my-openid-token-signature)
  • Click "Generate New Key"

Once a key is generated, copy its value from the "Keypair set" textbox and set it for use:

  • It may be stored in a file in the Smile CDR classes directory. In this case, the openid.signing.jwks_file (Signing JWKS (File)) property should be set to classpath://[filename].
  • The raw JWKS JSON document may be placed directly in the openid.signing.jwks_text (Signing JWKS (Text)) property.

Note that a JWKS file will have contents similar to the following:

{
  "keys": [
    {
      "kty": "RSA",
      "d": "cSYq2di [trimmed]",
      "e": "AQAB",
      "use": "sig",
      "kid": "test",
      "alg": "RS512",
      "n": "mVuJygm [trimmed]"
    }
  ]
}

Issuer URL

The security_out_smart.issuer.url (Issuer URL) property should be set to the outward facing URL at which this module will be accessible.

For example, if Smile CDR is deployed on a server named "myserver" and this module is configured to use port 9200 then this property might be set to https://myserver:9200.

7.14.2Configuring Clients

 

In order for a SMART Application to authenticate a user and receive an Access Token using the SMART on FHIR Outbound Security module, a client definition must be created for the application.

Client definitions may be configured using the Web Admin Console or the JSON Admin API. In the Web Admin Console, clients definitions may be found under Config -> OpenID Connect Clients

A Client definition should be created for each SMART Application.

7.14.3Client Secrets

 

A client secret is used to authenticate the Client. It is a similar concept to the user's password in that it is a secret credential used to authenticate, but it is different in that it authenticates a system and not a person.

Client secrets should only be used in situations where the client system is able to securely store the secret. For example, a Single Page Webapp (SPA) or Mobile Application should never use a client secret, since it is possible for malicious users to decompile and examine the source of the application in order to learn the secret.

Rotating Secrets

The client definition allows multiple secrets to be defined for a single client. This is done in order to facilitate secret rotation, where a new secret is used periodically. By setting the secret's Activation Date (the earliest date that a given secret will be accepted on) and the Expiration Date (the latest date that a given secret will be accepted on) it is possible to define a window of time where the old secret and the new secret are both usable.

This strategy is useful in cases where client updates need to be propagated and a grace period is required.

Requiring Client Secret Authentication

Clients may be marked as Client Secret Required to Authenticate, meaning that the client is required to authenticate itself when interacting with the authentication server.

If this property is not enabled, but a client secret is present:

  • The client will be permitted to perform the Refresh Flow using a valid Access Token instead of a client secret to authenticate.
  • The client will be permitted to perform Token Revocation without needing to authenticate the client.

Change Client Secret Via an Endpoint

Clients can change their own secret using a HTTP POST to the change-secret endpoint. Clients can change their secret with a grace period, allowing them to briefly have an overlap with the previous secret(s) and the new secret.

The following example shows a SMART Application making a request to the Authorization Server to change their client secret (for readability each parameter is shown on a new line but in a real request these would be joined together in one long line):

POST /oauth/change-secret
Accept: application/json
Content-Type: application/x-www-form-urlencoded

client_id=my-client-id
&client_secret=my-client-secret
&client_secret_new=my-client-secret-new
&grace_period_mins=10

In the above example, all parameters are required except for grace_period_mins. When a client changes their secret without grace period, expiration will be set to now.

7.14.4SMART Callback Script

 

If needed, a callback script may be invoked during the authorization process. This script should be placed in the Post-Authorize Script property.

Function: onPostAuthorize(theDetails)

The onPostAuthorize(theDetails) function is invoked any time that an authorization has succeeded. This function is invoked after the token is generated and should not be used to cancel or modify the authorization.

This function takes one argument that is named theDetails by convention. This argument is of type SmartOnPostAuthorizeDetails.

Example

/**
 * This function is invoked after a successful OAuth2 authorization flow. It
 * will be passed details about the access token and the request. It may be used
 * to log theDetails or call third-party systems. It should not try to abort the
 * grant as a token has already been issued at the time that this method is
 * called.
 *
 * @param theDetails This object contains information about the grant
 */
function onPostAuthorize(theDetails) {

	/*
	 * Here we will just log relevant details
	 */

	// Log the grant type, e.g. "implicit" or "refresh"
	Log.info("Successful completion of grant type: " + theDetails.grantType);

	// Log the granted scopes
	Log.info(" * Authorized scopes: " + theDetails.grantedScopes);

	// For a Cross-Organization Data Access Profile request, the "requesting
	// practitioner" and "requested record" (typically a FHIR Practitioner and
	// Patient resource, respectively) will be populated. These will be null
	// for other grant types.
	Log.info(" * Requesting practitioner: " + theDetails.requestingPractitioner.identifier.value);
	Log.info(" * Requested record: " + theDetails.requestedRecord.id);
}

7.14.5Client Credentials Grant

 

When using the Client Credentials Grant, the client will be authenticated and authorized without any user being involved.

This grant type may be used to authorize FHIR operations and other Smile CDR functions as well.

Note that when using this grant type, clients will automatically be granted any scopes listed in the Scopes sectiopn of the client definition. These scopes will be combined with the permissions granted directly to the client in order to determine which operations the client should be permitted to perform.

For example, if the client is given the ROLE_FHIR_CLIENT_SUPERUSER permission and the patient/*.read scope, the client will be permitted to perform read operations but not write operations.

7.14.6Cross-Organization Data Access Profile Grant

 

The SMART Cross-Organization Data Access Profile (CODAP) is a security specification that allows third-party systems such as EHRs and remote systems to authenticate a user implicitly, meaning that their identity and permission is asserted and trusted by the local authorization server.

This flow would typically be used when a local Smile CDR instance is providing access to clinical data, and you wish to allow users using systems at other organizations to make direct data access (i.e. FHIR API) requests without the need for these users to have an account already created in the local Smile CDR instance.

See CODAP Authorization Flow for a description of how this flow works.

Authorization Script

When performing a CODAP authorization flow, an Authorization script must be supplied to the module configuration. This authorization script is responsible for examining the supplied Authentication and Authorization tokens, and for determining the appropriate user permissions that should be granted. This permission process is critical to the implementation of the CODAP process: The CODAP specification supplies a mechanism for accepting tokens and the format that these tokens should take, but it does not spell out how an implementor might use these tokens to decide what the user should be allowed to do. This is always going to be implementation specific, which is why a script is used.

Function: authorize(theRequest, theOutcomeFactory)

This function is used to process the user request and return either a successful or a failed authorization outcome.

Inputs:

  • theRequest – This object contains details about the request, including the authorization and authentication tokens. This object is of type SmartCodapAuthorizationRequest.

  • theOutcomeFactory – This object is used to create a success or failure object to be returned by the function. This object is of type ScriptAuthenticationOutcomeFactory.

Output:

  • Returns an authorization outcome object. This outcome may indicate a successful authorization. It may alternately indicate a failed authorization, in which case no access token will be generated and the client will receive an error.

Example:

/**
 * This method is called when an authorization is requested, BEFORE the
 * token is created and access is granted
 * @param theRequest The incoming theRequest
 * @param theOutcomeFactory This object is a factory for a successful
 *      response or a failure response
 * @returns {*}
 */
function authenticate(theRequest, theOutcomeFactory) {
	var outcome = theOutcomeFactory.newSuccess();

	// Grab the identity of the requesting user
	var practitioner = theRequest.requestingPractitioner;

	// We need to set the username in the outcome object. This username should uniquely
	// identify the user. In this example, we'll use the FHIR Practitioner.identifier.value
	// but this could be something else.
	if (!practitioner.identifier.value) {
		throw 'Practitioner.identifier.value did not have a value!';
	}
	outcome.username = practitioner.identifier.value;

	// Just to show how this is done, we'll ban the user with the
	// given username by rejecting their authorization
	if (outcome.username == 'BADUSER') {
		var failure = theOutcomeFactory.newFailure();
		failure.message = 'Banned username';
		failure.incorrectPassword = true;
		return failure;
	}

	// The following contains the patient that was requested
	var patient = theRequest.requestedRecord;

	// Grant the user access to read and write any resources belonging to
	// the requested patient
	outcome.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', patient.id);
	outcome.addAuthority('FHIR_WRITE_ALL_IN_COMPARTMENT', patient.id);

	return outcome;
}

7.14.7Approving Scopes

 

When a client requests authorization using an Interactive Flow, the user may or may not be presented with an approval page after they successfully enter their credentials.

The following diagram shows the scope approval steps:

Scope Approval

7.14.8Skinning the Server

 

By default, the SMART Outbound Security module will display login and approval screens which are branded as Smile CDR. The server has two pages that display branding, shown below.

Login Page

This page asks the user for credentials.

Login

Approval Page

This page asks the user to confirm that they wish to sign into the application, and requests confirmation of the OAuth2/SMART scopes being requested by the application.

Approval

Error Page

An error page can also be skinned if desired. This page will rarely be shown to the user, as most expected error flows do not result in the user actually being directed to the error page. Several scenarios are shown below:

  • User enters invalid credentials: In this case, the user is redirected back to the login page, and an error message is shown directly on the login page.
  • User rejects approval for the Client Application: This will result in the browser session being redirected back to the Client Application with an indication in the redirect URL of what the failure is. In this case, the Client Application is expected to display an error.
  • User navigates to invalid link: If the user ends up on an invalid page request on the authorization server (which is typically due to misconfiguration) they will be shown the error page.
  • CSRF Failure: If the user experiences a CSRF failure (which is typically due to the user's session timing out before they enter credentials) they will be shown the error page.

Creating a Skin

Skins for the SMART Outbound Security module are created using a format called WebJars. A WebJar is essentially a JAR (zip) file containing your web resources (HTML, CSS, JS, etc.) in a specific structure.

Your Skin JAR file will have the following properties:

  • groupId: This is a Java-style package name, often just your company domain name backwards (e.g. com.example). This is simply an identifier, you can choose anything you like here.
  • artifactId: This is an identifier for your skin. It should not have spaces but can be any string you like (e.g. my-custom-skin).
  • versionId: A version number for your skin. Generally you should simply use 1.0 here (future versions of Smile CDR may introduce live updating or other features that use the version ID but for now it does not matter what version you pick).

The contents of the JAR will be as follows:

/META-INF/resources/webjars/artifactId/userlogin.html
/META-INF/resources/webjars/artifactId/userapprove.html
/META-INF/resources/webjars/artifactId/usererror.html
/META-INF/resources/webjars/artifactId/resources/css/mycss.css
/META-INF/resources/webjars/artifactId/resources/css/bootstrap.min.css
/META-INF/resources/webjars/artifactId/resources/js/loginscript.js
/META-INF/maven/groupId/artifactId/pom.properties

Note the following things about the files above:

  • Within the directory structure, the directory must start with /META-INF/resources/webjars/, but the artifactId portion should be replaced with the actual artifactId of your skin project.
  • The userlogin.html and userapprove.html pages are the actual HTML pages to use for login and approval respectively, and the usererror.html page is shown in the event of an error (such as a 404 Not Found or a CSRF token verification failure). See HTML Template Files below for information on the format for these files.
  • All other static web resources must be in a subdirectory called resources/

HTML Template Files

The HTML template files use the Thymeleaf templating language. Thymeleaf is a developer-friendly format that allows templates to render directly in a browser without a backend server during development, but that allows a set of custom tags and attributes to be added.

It is not our aim to completely document Thyemleaf here, as the Thymeleaf website has excellent documentation.

There are however several key points around specific page templates:

Login Page

The following variables may be used on this page to display the client details:

  • ${client_id} - Provides the ID of the client being authorized
  • ${client_name} - Provides the name of the client being authorized (or the ID if no name is specified)

Approval Page

On this page, access to the logged-on user is available via the Thymeleaf #authentication.principal object. This object can be used to fetch several properties of the logged in user:

  • ${#authentication.principal.username} - This will contain the logged in user's username
  • ${#authentication.principal.familyName} - This will contain the logged in user's family (last) name
  • ${#authentication.principal.givenName} - This will contain the logged in user's given (first) name

The following variables may be used on this page to display the client details:

  • ${client_id} - Provides the ID of the client being authorized
  • ${client_name} - Provides the name of the client being authorized (or the ID if no name is specified)

Error Page

On the error page, there are several variables available during template execution:

  • ${statusCode} - This will contain the HTTP status code associated with the error, e.g. 404 or 403.
  • ${statusTitle} - This will contain the description associated with the HTTP status code, e.g. Not Found.
  • ${uri} - This will contain the path being requested, e.g. "/" or "/oauth/token".
  • ${message} - This will contain a message about the error.

Sample Skin Project

A sample SMART Outbound Security module skin is available at the following links: