On this page:

13.6SMART 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.

13.6.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.

13.6.2SMART 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: onTokenGenerating(theUserSession, theAuthorizationRequestDetails)

The onTokenGenerating function is called immediately before an access token is generated following a successful authentication. It is primarily used in order to customize the SMART launch context(s) associated with a particular session (ie. because the launch context is maintained in a third-party application and needs to be looked up during the auth flow).


  • theUserSession – This parameter is of type UserSessionDetails. This will contain details about the user and their session, as supplied by the connected Inbound Security module. Scripts may modify this object. Note that if the authentication is happening for a client as opposed to a user (i.e. using the Client Credentials Grant type) this parameter will be null, but must still exist in the method signature.

  • theAuthorizationRequestDetails – This parameter is of type OAuth2AuthorizationRequestDetails. It will contain details about the OAuth2 authorization request.

Example: Adding Launch Contexts

 * This function is called just prior to the creation and issuing of a new
 * access token.
 * @param theUserSession The authenticated user session (can be modifued by the script)
 * @param theAuthorizationRequestDetails Contains details about the authorization request
function onTokenGenerating(theUserSession, theAuthorizationRequestDetails) {

   // Here we will just log the launch parameter and hardcode the launch contexts
   // to use. It would be possible however to use the launch parameter as input
   // to an algorithm/lookup/etc in order to set the launch context.
   Log.info("Generating token for launch: " + theAuthorizationRequestDetails.launch)

   theUserSession.addLaunchResourceId('patient', '555');
   theUserSession.addLaunchResourceId('encounter', '666');
   theUserSession.addLaunchResourceId('location', '777');


Example: Adding Custom Claims

The following example shows how to add custom claims to the generated access token.

 * This function is called just prior to the creation and issuing of a new
 * access token.
 * @param theUserSession The authenticated user session (can be modified by the script) or null if a client is authenticating as opposed to a user
 * @param theAuthorizationRequestDetails Contains details about the authorization request
function onTokenGenerating(theUserSession, theAuthorizationRequestDetails) {

   theAuthorizationRequestDetails.addAccessTokenClaim('claimName', 'claimValue');


Example: Working With Approved Scopes

The onTokenGenerating callback executes after the user has approved any scopes, but before the user's session is actually created. The callback can examine the approved scope list, as well as modifying it. The following example shows several scope manipulation methods.

 * This function is called just prior to the creation and issuing of a new
 * access token.
 * @param theUserSession The authenticated user session (can be modified by the script) 
 *        or null if a client is authenticating as opposed to a user
 * @param theAuthorizationRequestDetails Contains details about the authorization request
function onTokenGenerating(theUserSession, theAuthorizationRequestDetails) {

   // Log the list of approved scopes
   Log.info("The following scopes have been approved for the session: " + theUserSession.approvedScopes);

   // Remove an approved scope and add a different one 

Function: onPostAuthorize(theDetails)

The onPostAuthorize 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.


 * 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);

See OAuth2 Exceptions for details on how to throw exceptions within this method.

13.6.3Client 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.

13.6.4Cross-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: authenticate(theRequest, theOutcomeFactory)

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


  • 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.


  • 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.


 * 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;

13.6.5Skinning the Server


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

Login Page

This page asks the user for credentials.


Federated OAuth2/OIDC Login Page

If you are using federated OAuth2/OIDC with multiple providers to choose from, the user will not see the interactive login page shown above. Instead they will see this page which allows them to select which provider they wish to use to log in.

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.

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.

Two Factor Authentication Page

On servers where Two Factor Authentication is enabled, the page used to request a TFA code can be skinned as well. This page will display a text box that a user may use to enter their TFA code.

Revocation Page

This page allows the user to revoke any previously approved scopes and any active tokens. See Revocation Page for more information.

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:


Note the following things about the files above:

  • Within the directory structure, the directory must start with /META-INF/resources/webjars/, but the [artifactId] and [versionId] portion should be replaced with the actual artifactId and versionId 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/.
  • Your webjar will also contain a file called pom.properties as shown above. The contents of this file must include the following:

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)

Federated OAuth2/OIDC 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)

In addition, the variable ${servers} will hold an array of OAuth2Server objects.

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)

The following variable may be used to display individual scopes:

  • ${scopeMap} - contains info about the scopes allowed:
    • ${scopeMap.scope} - The type of scope.
    • ${scopeMap.name} - the name of the scope.
    • ${scopeMap.hidden} - a boolean value to declare whether the scope is to be hidden or not.

Note the following HTML:

<input type="hidden" \th:name="${_csrf.parameterName}" \th:value="${_csrf.token}" />
<input type="hidden" name="client_id" \th:value="${client_id}"/>

These hidden input fields must always be supplied in order to provide CSRF protection to the login page, and to supply the client ID being authenticated to.

Revoke Page

On this page, there is one useful variable to consider:

  • ${clients} - This variable contains a list of all clients given permission to the server. For each ${client} in ${clients}, there exists a:
    • ${client.clientName} - The name of the client.
    • ${client.clientID} - The id of the client in the system.
    • ${client.approvedScopes} - a list scopes that the client has access to.

POSTing to the /oauth/revoke/${client.clientID} endpoint will revoke all approved permissions in ${client.approvedScopes}

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:

cdr-security-out-smart-demoskin-1.0.zip cdr-security-out-smart-demoskin-1.0.tar.gz

To use this sample:

  • Download the example project using either the .zip or the .tar.gz link and extract it to your local filesystem.
  • Modify the file pom.xml to replace the groupId, artifactId, and version with your values.
  • A sample set of skin files can be found in the path src/main/resources. You can modify these files to your liking, or replace them entirely with new files at the same path.
  • Create a webjar file by executing the following command:
mvn clean install
  • When this command is completed, your webjar file will be found in the target/ directory.