By default, when a SMART Application requests a new authorization from the SMART Outbound Security module, the module will prompt the user for credentials in order to authenticate them, and will use an Inbound Security Module to verify the credentials directly.
Instead of prompting the user for credentials however, it is also possible to redirect the user to a third-party "Federated" OAUth2/OIDC provider. This allows SMART authentication against a standards compliant OIDC provider that is able to complete the OAuth2 Authorization Code flow, but is not able to support the SMART on FHIR extensions to the base OIDC specification.
For example, this method could be used to:
The following diagram shows how the Federated OAuth2/OIDC Login flow works.
When using federated flow, a few key points to consider include:
Using Federated OAuth2/OIDC Login with the SMART Outbound Security module is not the only way to integrate with a third-party OIDC provider however. See Models for Using External OIDC Servers for a discussion on available options.
To enable Federated OAuth2/OIDC Login, use the following steps:
In the federated OAuth2 provider, create a client definition for Smile CDR. Note that even if you will be registering multiple client (SMART App) definitions in Smile CDR, you will only need to create one definition in the federated provider.
This provider should:
openid
and profile
scopes (optional but recommended)If one hasn't already been created, create a new module of type SMART Outbound Security.
In the module definition, enable the Federated OAuth2/OIDC Login option. Note that if this is enabled, the module will no longer support direct user authentication (i.e. prompting a user for username and password from within Smile CDR and verifying the credentials directly against a database/LDAP/etc).
In the Web Admin Console, navigate to Config OpenID Connect Servers, select your SMART Outbound Security module from the dropdown and click Create. The Server definition refers to the Federated OAuth2/OIDC Provider itself.
You can also create your definition programatically at runtime using the OpenID Connect Servers Endpoint, or automatically at startup using Pre-Seeding OpenID Connect Servers.
This definition should include all of the relevant endpoint URLs for the provider, and also needs to include an Authorization Script as described below.
At least one definition must be created, but you can also create more as necessary. If only one federated definition is created, users will automatically be directed to this provider when a new authorization is initiated. If multiple definitions are created, the user will first be prompted for which provider they wish to authorize against.
Create a definition for the SMART Application you wish to authorize, or multiple definitions if you have multiple applications.
In your FHIR Endpoint module configuration, enable OpenID Connect Security and add an OIDC module dependency from your FHIR Endpoint module to your SMART Outbound Security module.
During the Authorization Code exchange between Smile CDR and the federated OAuth2 provider, Smile CDR may need to authenticate itself. The authentication mechanism is specified using the Client Authentication Mode property of the OIDC Server Definition.
The following mode options are supported:
Smile CDR will provide PKCE parameters to the federated provider when initiating the token request and when performing code exchange. These parameters may be ignored by the federated provider if it does not support PKCE.
Each server definition used for federated login must include an Authentication Callback Script that implements the onAuthenticateSuccess
function. This function is described here.
The purpose of this function is to take the user session created by the federated provider, and enhance it by adding any necessary session information including:
The following example shows an authentication callback script for Federated OAuth2/OIDC Login.
/**
* This is a sample authentication callback script for the
* SMART Outbound Security module, showing federated OAuth2/OIDC Login
*
* @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) {
// In this example we are demonstrating a patient-facing app, where the user corresponds to a FHIR Patient, and the
// ID of that patient is passed back from the federated provider via a claim in the ID Token. Instead
// the ID might be fetched using an HTTP call, or derived from something else.
var patientId = theContext.getClaim('patientId');
// Add a log line for troubleshooting
Log.info("User " + theOutcome.getUsername() + " has authorized for " + patientId + " with scopes: " + theContext.getApprovedScopes());
// All users can use the FHIR CapabilityStatement operation
theOutcome.addAuthority('FHIR_CAPABILITIES');
// Assign appropriate Smile CDR permissions
theOutcome.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + patientId);
theOutcome.addAuthority('FHIR_WRITE_ALL_IN_COMPARTMENT', 'Patient/' + patientId);
// Set the launch context (in case the application has requested a SMART launch context scope). This should only
// be set if the patient referenced by the ID is actually in context for this launch.
theOutcome.addLaunchResourceId('patient', patientId);
return theOutcome;
}
By default Smile CDR generates local user names by appending issuer and subject. This behavior can be customized by supplying custom user name mapping. This mapping receives token claims and server info.
/**
* This is a sample user name mapping callback script
*
* @param theOidcUserInfoMap OIDC claims from the token as a map
*
* @param theServerInfo JSON mapping of the OAuth server definition (backed by ca.cdr.api.model.json.OAuth2ServerJson)
*
* @returns Local unique Smile CDR user name for the enternal user.
*/
function getUserName(theOidcUserInfoMap, theServerInfo) {
return "EXT_USER:" + theOidcUserInfoMap['preferred_username'];
}
If more than one server definition is available, by default the user will be presented with a screen allowing them to pick a server to authenticate with.
An interceptor can be used to preempt this and tell the SMART Outbound Security module which server to proceed with authenticating against. This is done using the SMART_FEDERATED_OIDC_PRE_PROVIDER_SELECTION pointcut.
The following example shows a simple interceptor which can be used for this purpose. This interceptor is also available in the Interceptor Starter Project.
/**
* This interceptor decides which server to select for Federated OIDC
* mode. In this example, we are using the "launch" request parameter
* sent to us by the client to determine which server to select.
* The launch parameter is provided by the EHR to the Client with
* the intent that the Client will return it unmodified. We assume
* a format of "[serverRegistrationId]-[other information]" and parse the
* value as such.
* <p>
* The "launch" parameter is intended to be opaque to the client,
* so this scheme can work. However, any scheme which allows the
* interceptor to determine which Server Name to return is fine. Other
* parameters such as "client_id" may be useful here.
* See <A href="https://smilecdr.com/docs/smart/smart_on_fhir_authorization_flows.html#authorization_code_flow">Authorization Code Flow</A>
* for an example of parameters which should be available during the
* initial authorization request by the client.
*/
@Interceptor
public class ServerSelectionInterceptor {
@CdrHook(CdrPointcut.SMART_FEDERATED_OIDC_PRE_PROVIDER_SELECTION)
public String selectServer(OidcAuthRequestDetails theAuthRequestDetails) {
String launchParameter = theAuthRequestDetails.getLaunch();
if (StringUtils.isNotBlank(launchParameter)) {
int dashIndex = launchParameter.indexOf('-');
if (dashIndex != -1) {
/*
* We'll treat the first portion of the launch code as being
* a server definition "registration ID". Note that this is not
* the same as the server name. Also note that this particular logic
* assumes that your registration ID does not have any "-" characters
* in it.
*/
return launchParameter.substring(0, dashIndex);
}
}
return null;
}
}