16.11.1Federated OAuth2/OIDC Login

 

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:

  • Support authenticating users via social logins such as Google, Twitter, and Facebook.
  • Support authenticating users against cloud identity providers such as Okta or Ping, while leveraging SMART Client Management in Smile CDR and potentially injecting additional details such as launch contexts that would not be available to the cloud provider.

16.11.2Federated Flow

 

The following diagram shows how the Federated OAuth2/OIDC Login flow works.

Federated OAuth2 Flow

16.11.3Considerations

 

When using federated flow, a few key points to consider include:

  • When using this flow, an access token is generated by the Federated OIDC Provider, but this token is only used by Smile CDR to perform introspection. To the Federated OIDC Provider, your Smile CDR installation is the client requesting access. This means that even if many SMART Applications are registered in Smile CDR and then used, a single client definition will be used by the Federated OIDC Provider.
  • All SMART Applications are authorizing against the Smile CDR SMART Outbound Security module. These applications are unaware of the Federated OIDC Provider.
  • Access tokens and refresh tokens used to access your FHIR APIs are managed by Smile CDR and not by the Federated OIDC Provider.

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.

16.11.4Setting Up Federated OAuth2/OIDC Login

 

To enable Federated OAuth2/OIDC Login, use the following steps:

16.11.4.0.11. Create a Client Definition for Smile CDR

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:

  • Support the Authorization Code flow (required)
  • Have a client secret defined (optional but recommended)
  • Have reachable endpoints for user authorization (Authorization Endpoint), token exchange (Token Endpoint), and user introspection (UserInfo Endpoint)
  • Support OpenID Connect with the openid and profile scopes (optional but recommended)
  • Support PKCE if possible (optional but recommended)
  • Support the following token signature algorithms:
    • RS256
    • RS512
    • ES256
    • ES512

16.11.4.0.22. Create/Configure a SMART Outbound Security Module

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

16.11.4.0.33. Create an OIDC Server Definition

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.

16.11.4.0.44. Create at least one OIDC Client Definition

Create a definition for the SMART Application you wish to authorize, or multiple definitions if you have multiple applications.

16.11.4.0.55. Configure your FHIR Endpoint module

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.

16.11.5Authorization Script

 

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:

  • User demographics
  • Smile CDR permissions
  • Launch context details if known

16.11.5.1Example

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'];
}

16.11.6Server Selection Interceptor

 

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

}