The SMART Inbound Security module is a security module that authorizes client requests by validating OpenID Connect Access Tokens.
Unlike the SMART on FHIR Outbound Security Module, this module is not itself an Authorization Server; rather it assumes the existence of an external Authorization Server and validates that Access Tokens were granted by that server.
This module requires the existence of an external Authorization Server that is compliant with the SMART on FHIR Authorization Guide. Specifically, this external Authorization Server must be a compliant OpenID Connect server.
The following diagram shows the flow when using the SMART Inbound Security module to perform authorization.
Setting up a SMART on FHIR architecture with an external Authorization Server requires both an inbound security module and one or more OpenID Connect server definitions.
To create a SMART Inbound Security module in the Web Admin Console:
Config
Module Config
SMART Inbound Security
module type and click Add
Save
The SMART Inbound Security module does not require much configuration, as the process of accepting a SMART Access Token is a fairly well-defined process. The module has two properties for defining endpoint URLs: Token Endpoint and Authorization Endpoint.
These properties do not actually affect the behaviour of the inbound security module, but they will affect the CapabilityStatement
exported by any FHIR endpoints that use the module.
The SMART Inbound Security module can authorize tokens in two ways:
In a deployment where Smile CDR is acting as the OIDC Provider using the SMART Outbound Security module, the outbound security module is able to act as an OpenID Connect Authentication dependency to other modules (e.g. FHIR Endpoint).
In order to achieve separation of concerns, and to scale different parts of the architecture separately, the SMART Outbound Security module can be placed on a different node from the services being secured (e.g. FHIR Endpoint). In this model, a SMART Inbound Security module should be placed on the same node as the services being secured, and the SMART Inbound Security module should have the trust_intra_cluster_tokens.modules
property set with a value pointing to the SMART Outbound Security module on its node.
When validating an external access token (a token that was issued by a third-party SMART on FHIR compliant Authorization Server), Smile CDR will validate the token using the standard OpenID Connect mechanisms. This involves parsing the signed JWT access token, verifying the signature, granting the user appropriate permissions, and finally applying scopes to the user session.
If possible, the third party server should be configured to support the JSON Web Token (JWT) Profile for OAuth2 2.0 Access Tokens. This profile specifies that Access Tokens must be signed JWTs containing at least the following claims:
iss
- Issuersub
- Subjectexp
- Expiry (optional)The server must support token introspection using a Client ID and Client Secret as defined in the OpenID Connect specification.
The following diagram shows the internal flow used by the SMART on FHIR Inbound Security module to process Access Tokens.
When accepting external tokens, Smile CDR will first validate the token to ensure that it has been digitally signed with a key that is trusted. This involves the following steps inside the SMART Inbound Security module:
See JWT Signature Algorithms for a list of supported algorithms that can be used to verify a signed token.
An OpenID Connect Server definition must be created in order to accept external access tokens. This definition describes the external Authorization Server and tells the SMART Inbound Security module that it is okay to trust the Authorization Server and the tokens it generates in order to authorize access to functions in Smile CDR.
To create an OpenID Connect Server definition:
This definition has a few properties:
The most important property when defining the server definition is the Issuer. This is a URL which points to the base URL of the OpenID Connect Authorization Server you wish to trust. Note that the value here must point to a valid OpenID Connect server, and this server must be network accessible by the SMART Inbound Security module. The issuer URL must also match the iss
(issuer) claim that is received as a part of any Access Tokens created by the Authorization Server.
Note that trailing slashes in the Issuer URL are ignored in order to avoid incompatibilities. If the Authorization Server iss
claim includes a trailing slash, this will not cause any issues.
If present, the validation key is specified in JWK/JWKS format. Any tokens accepted for this server will be verified against the given key, and authentication will fail if the token can not be verified.
In order to accept tokens from an external source, a callback script should generally be supplied. This script will process the verified access token and assign the user an appropriate set of permissions based on this token (and the scopes and claims it provides).
It is important to realize that a token with a set of scopes is not enough on its own to determine what a user may do. For example, a session that has been granted Patient/*.read
permission has read access to all resource types for some patient, but which patient(s) that is is not explicitly stated in the token. This is why a script is required. The script is configured in the SMART Inbound Security module configuration, and much contain an onAuthenticateSuccess(...) function that will be called immediately after the OIDC token is validated, but before it is turned into a Smile CDR session.
The following example simply assigns all permissions to the user. This might be appropriate if all users of an application should have access to all data.
/**
* This is a sample authentication callback script for the
* SMART Inbound Security module.
*
* @param theOutcome The outcome object. This contains details about the user that was created
* in response to the incoming token.
*
* @param theOutcomeFactory A factory object that can be used to create a new success or failure
* object
*
* @param theContext The login context. This object contains details about the authorized
* scopes and claims. Because this script will be used with the SMART Inbound Security
* module, the type for this parameter will be of type SecurityInSmartAuthenticationContext,
* which is described here:
* https://try.smilecdr.com/docs/javascript_execution_environment/callback_models.html#securityinsmartauthenticationcontext
*
* @returns {*} Either a successful outcome, or a failure outcome
*/
function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {
// We will grant the user ROLE_FHIR_CLIENT_SUPERUSER which gives them access to all
// FHIR data. Note that the list of approved scopes may cause the permissions assigned
// here to be reduced automatically when the session is created.
theOutcome.addAuthority('ROLE_FHIR_CLIENT_SUPERUSER');
return theOutcome;
}
The following example assumes that an external OIDC server has been configured to issue access tokens that will be consumed by the SMART Inbound Security module. In order to convey the identity of the authorized patient, the OIDC server in this example has been configured to include a claim called patient
, which will include the resource ID for the Patient resource corresponding to the authenticated user.
Consider the following decoded access token:
{
"sub": "myusername",
"azp": "my-client-id",
"patient": "123",
"scope": "openid profile patient/*.read",
"iss": "http://example.com/oidc-issuer"
// .... other claims omitted ....
}
A callback script which is able to apply the patient
claim is shown below:
/**
* This is a sample authentication callback script for the
* SMART Inbound Security module.
*
* @param theOutcome The outcome object. This contains details about the user that was created
* in response to the incoming token.
*
* @param theOutcomeFactory A factory object that can be used to create a new success or failure
* object
*
* @param theContext The login context. This object contains details about the authorized
* scopes and claims. Because this script will be used with the SMART Inbound Security
* module, the type for this parameter will be of type SecurityInSmartAuthenticationContext,
* which is described here:
* https://try.smilecdr.com/docs/javascript_execution_environment/callback_models.html#securityinsmartauthenticationcontext
*
* @returns {*} Either a successful outcome, or a failure outcome
*/
function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {
// We expect the access token JWT to have a claim called "patient" with
// a value such as "123". This claim is assumed to be the ID of the patient
// for whom the token was issued.
let patientClaim = theContext.getStringClaim('patient');
if (!patientClaim) {
throw 'No claim "patient" in access token';
}
let patientId = 'Patient/' + patientClaim;
// Log a bit of troubleshooting information
Log.info("User " + theOutcome.getUsername() + " has authorized for " + patientId + " with scopes: " + theContext.getApprovedScopes());
// We will grant the user the ability to perform the FHIR /metadata operation,
// as well as giving them read/write permission to the Patient record corresponding
// to their account. Note that the list of approved scopes may cause this to be
// reduced automatically. E.g. If the user denies the "patient/*.write" scope,
// the FHIR_WRITE_ALL_IN_COMPARTMENT will automatically be stripped from the
// session even though it is granted here.
theOutcome.addAuthority('FHIR_CAPABILITIES');
theOutcome.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', patientId);
theOutcome.addAuthority('FHIR_WRITE_ALL_IN_COMPARTMENT', patientId);
return theOutcome;
}
The SMART Inbound Security module expects a list of approved scopes to be included in the Access Token JWT in a claim called "scope"
, as described in the JSON Web Token (JWT) Profile for OAuth2 2.0 Access Tokens specification.
Some third-party Authorization servers use a claim of "scp"
instead. The following example shows how to account for this in your callback script.
/**
* This is a sample authentication callback script for the
* SMART Inbound Security module.
*
* @param theOutcome The outcome object. This contains details about the user that was created
* in response to the incoming token.
* @param theOutcomeFactory A factory object that can be used to create a new success or failure
* object
* @param theContext The login context. This object contains details about the authorized
* scopes and claims
* @returns {*} Either a successful outcome, or a failure outcome
*/
function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {
// If the claim contains a single string (e.g. "scp": "openid profile email")
// the following will work
let scpClaim = theContext.getStringClaim('scp');
if (scpClaim) {
scpClaim.split(" ").forEach(function (nextScope) {
theOutcome.addApprovedScope(nextScope);
});
}
// If the claim contains an array (e.g. "scp": ["openid", "profile", "email"])
// the following will work
var scpArrayClaim = theContext.getStringArrayClaim('scp');
if (scpArrayClaim) {
scpArrayClaim.forEach(function (nextScope) {
theOutcome.addApprovedScope(nextScope);
});
}
// ... add permissions to session, and other logic ...
return theOutcome;
}
If the Authentication Server issues tokens that are not signed JWTs, an authenticate()
function must be supplied instead of the onAuthenticateSuccess()
function. This function is responsible for validating the token and determining whether it is valid.
The following example simply assigns all permissions to the user. This might be appropriate if all users of an application should have access to all data.
/**
* This method is called by the SMART Inbound Security module in order to validate the
* received Access Token and either generate a successful authentication, or
* to reject it.
*
* @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) {
// Add a log line
let token = theRequest.getUsername();
// In this example, we'll look for a hardcoded token value. In a real scenario, something more
// advanced will be needed
let failed = (token === 'ABCDEFG123455');
if (failed) {
let outcome = theOutcomeFactory.newFailure();
failure.message = 'Token is invalid';
return failure;
}
// Otherwise, prepare a valid response
let outcome = theOutcomeFactory.newSuccess();
outcome.username = theRequest.getUsername();
outcome.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/123');
outcome.addApprovedScope('patient/*.read');
return outcome;
}