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.
Setting up this module requires several key configuration items.
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.
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:
2048
(this is the default, which should be sufficient for most purposes)Signing
RS256
should be sufficient)my-openid-token-signature
)Once a key is generated, copy its value from the "Keypair set" textbox and set it for use:
classes
directory. In this case, the openid.signing.jwks_file
(Signing JWKS (File)) property should be set to classpath://[filename]
.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]"
}
]
}
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
.
The Terms of Service feature is activated by configuring the effective date. When active, the login flow will include a separate page to present Terms of Service that the user must accept. Once accepted, the user will not be asked again on subsequent logins. The system records these agreements per-client for each user along with a timestamp. They are also recorded in the audit log. A user's Terms of Service agreement is revoked along with the access token on the Revocation Page.
The user must have accepted the terms after the effective date to complete a login, so do not set a future effective date as the users will not be able to log in at all until after that time is reached.
If needed, a callback script may be invoked during the authorization process. This script should be placed in the Post-Authorize Script property.
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.
/**
* 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');
}
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');
}
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
theUserSession.removeApprovedScope('patient/*.read');
theUserSession.addApprovedScope('patient/Observation.read');
}
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.
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.
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.
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.
The authorization script for CODAP uses the function authenticate(...).
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.
This page asks the user for credentials.
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.
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.
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:
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.
On servers where Terms of Service is enabled, the page used to present the TOS should be skinned to present the agreement. This page will display agree and reject buttons.
This page allows the user to revoke any previously approved scopes and any active tokens. See Revocation Page for more information.
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]/[versionId]/userlogin.html
/META-INF/resources/webjars/[artifactId]/[versionId]/userapprove.html
/META-INF/resources/webjars/[artifactId]/[versionId]/usererror.html
/META-INF/resources/webjars/[artifactId]/[versionId]/resources/css/mycss.css
/META-INF/resources/webjars/[artifactId]/[versionId]/resources/css/bootstrap.min.css
/META-INF/resources/webjars/[artifactId]/[versionId]/resources/js/loginscript.js
/META-INF/maven/[groupId]/[artifactId]/pom.properties
Note the following things about the files above:
/META-INF/resources/webjars/
, but the [artifactId]
and [versionId]
portion should be replaced with the actual artifactId and versionId of your skin project.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.resources/
.pom.properties
as shown above. The contents of this file must include the following:groupId=[groupId]
artifactId=[artifactId]
version=[versionId]
To install your skin files, this JAR should be placed in the customerlib/ directory of the Smile CDR installation, and Smile CDR should be restarted. Then configure the SMART Login Skin section of the SMART on FHIR Outbound Security Module in the console.
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:
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)${client_scopes}
- Provides the list of client scopes${client_auto_grant_scopes}
- Provides the list client auto-granted scopes${client_attestation_accepted}
- Boolean value indicating whether the client has accepted the attestation to the policyThe 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)${client_scopes}
- Provides the list of client scopes${client_auto_grant_scopes}
- Provides the list client auto-granted scopes${client_attestation_accepted}
- Boolean value indicating whether the client has accepted the attestation to the policyIn addition, the variable ${servers}
will hold an array of OAuth2Server objects.
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) nameThe 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)${client_scopes}
- Provides the list of client scopes${client_auto_grant_scopes}
- Provides the list client auto-granted scopes${client_attestation_accepted}
- Boolean value indicating whether the client has accepted the attestation to the policyThe 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.
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}
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)${client_scopes}
- Provides the list of client scopes${client_auto_grant_scopes}
- Provides the list client auto-granted scopes${client_attestation_accepted}
- Boolean value indicating whether the client has accepted the attestation to the policyOn 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.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:
pom.xml
to replace the groupId, artifactId, and version with your values.src/main/resources
. You can modify these files to your liking, or replace them entirely with new files at the same path.mvn clean install
target/
directory.If needed, a callback script may be invoked during the authorization process.
Function | Context for Use | When Called | Location |
---|---|---|---|
onAuthenticateSuccess(..) (learn more) |
Federated OAuth2/OIDC Mode Only | Called immediately after a token has been issued by the federated provider but before a local (to Smile CDR) user and session has been created. | Federation Auth Script option in the OIDC Server Definition. |
authenticate(..) (learn more) |
CODAP Mode Only | Called immediately after a token has been received from the CODAP token issuer. | CODAP Authorization Script setting on SMART Outbound Security module. |
onTokenGenerating(..) (learn more) |
Any | Called after authentication has succeeded and before a token is issued. | Post-Authorize Script setting on SMART Outbound Security module. |
onPostAuthorize(..) (learn more) |
Any | Called after a token has been issued and stored but before it is returned to the client. | Post-Authorize Script setting on SMART Outbound Security module. |