15.10.1Authentication Callback Scripts

 

The Smile CDR Inbound and Outbound Security modules support a JavaScript based callback API that can be used to add custom logic to the authentication and authorization process. Callback scripts have the right to examine authentication requests, enhance or restrict the corresponding user session, and even reject the authentication entirely.

Callback scripts in security modules can use the documented JavaScript Execution Environment APIs. However in cases where a security module does NOT have a FHIR Storage module dependency configured, the following APIs will NOT be available in callback scripts:

15.10.2Function: onAuthenticateSuccess

 

This function applies to all security modules with the exception of non-federated SMART Outbound module implementations. In the case of a Federated SMART Outbound module implementation, this method would be implemented as part of an OIDC Server definition used with the Federated SMART Outbound module.

The onAuthenticateSuccess method is invoked after a user has successfully authenticated with an inbound security module. In other words, this is invoked after a user's credentials have been validated and a set of user details (potentially including the user's name, permissions, etc.) has been assembled.

The script has the ability to modify the user's details. This might consist of steps such as:

  • Enhancing the user demographics, such as looking up their email address in an external directory
  • Setting the SMART launch context for the session based on external factors
  • Adding to or removing permissions from the user session
  • Adding components to fhirContext, such as a reference (ex "PractitionerRole/abc") or a reference + role pair (ex "List/123", ""https://example.org/med-list-at-home"). See: fhirContext and the FHIR standard for more details:

15.10.2.1Parameters

  • theUserSession – This object contains details about the request, including the authorization and authentication tokens. This object is of type UserSessionDetails.

  • theOutcomeFactory – This object is used to create a success or failure object to be returned by the function. This object is of type ScriptAuthenticationOutcomeFactory.

  • theContext – The context object contains details about the environment in which the authorization occurred. The datatype for this object will vary depending on the specific inbound security module being used. See the individual module type documentation for more information. All module types will provide a context that provides at least the properties and functions of the AuthenticationContext. Different Inbound Security modules will provide a specific subclass of AuthenticationContext that has relevant properties and operations for that module type:

15.10.2.2Output

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

15.10.2.3Returning Success

Typically, it is sufficient to simply return the authorization outcome that was already generated by the system as shown above. It is also possible to modify the authorization. The most common use case is to add or remove permissions based on some criteria.

function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {

	if (theOutcome.username === 'admin123') {
		theOutcome.addAuthority('ROLE_FHIR_CLIENT_SUPERUSER_RO');
	}

	return theOutcome;
}

15.10.2.4Returning Failure

The following example shows a failure being returned:

// Create a failure object to return
var failure = theOutcomeFactory.newFailure();

// The following properties are optional
failure.message = 'Login failed due to invalid credentials';
failure.unknownUsername = false;
failure.incorrectPassword = true;

return failure;

Note the properties that may optionally be populated on the failure object. These properties supply additional details about the failure and can be useful for troubleshooting; however, they are not required to be populated. A failure object with no properties is still treated as a normal authentication failure.

  • Message – The message property is used to supply a human-readable description of why the authentication failed.
  • Unknown Username – The unknownUsername property may be set to true to indicate that the authentication failed due to an unknown or invalid username.
  • Incorrect Password – The incorrectPassword property may be set to true to indicate that the authentication failed due to an incorrect password for the given username.

15.10.2.5User Data

Some solutions may have custom authorization needs that do not map easily to existing authorities or roles. For these cases, the UserSessionDetails object exposes a userData key/value collection similarly to FHIR resources. For example, a SMART inbound hook can inspect custom claims and set user data:

function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {

	if (theContext.getClaim('patientRepresentative') != null) {
		theOutcome.setUserData('patientRepresentative', true);
	}

	return theOutcome;
}

This user data could then be used in the Consent Service to block or mask data:

function consentWillSeeResource(theRequestDetails, theUserSession, theContextServices, theResource, theClientSession) {
   if (theUserSession.hasUserData('patientRepresentative')) {
      // clear some sensitive data.
      theResource.clear('value');
   }
}

15.10.2.5.1Passing Data to UserData

Due to a known issue in higher versions of Java, attempts to save complex objects to UserData during onTokenGenerating will often throw an error.

As such, it's better to serialize any non-primitive, non-string values before saving them to UserData.

function onTokenGenerating(theUserSession, theAuthorizationRequestDetails, theClientDetails) {
   const myComplexObj = {
      "property": "value",
      "bool_property": true
   };
   
   // save the object to UserData by serializing first
   const serialized = JSON.stringify(myComplexObj);
   theUserSession.setUserData("userDataField", serialized);
}

NB: This can also be fixed by setting JVMARGS=\"$JVMARGS --add-opens=java.base/java.util=ALL-UNNAMED in the setenv file.

15.10.2.6Example: Extracting External Context

The following is a simple example of an onAuthenticateSuccess callback function:

function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {
   var username = theOutcome.username;
   var remoteAddress = theContext.remoteAddress;

   Log.info('User ' + username + ' is logging in from ' + remoteAddress);

   // Don't allow the admin user to log in from hosts other than localhost
   // Note that the username will be capitalized if the security module is not case sensitive
   if (username === 'ADMIN' && remoteAddress != '127.0.0.1') {
      var failure = theOutcomeFactory.newFailure();
      failure.message = 'Can not log in as admin from this address';
      return failure;
   }

   // Otherwise, just return default (successful) outcome
   return theOutcome;
}

15.10.2.7Example: Adding Permissions

The following example simply assigns a superuser permissions to the user. This might be appropriate if all users of an application should have access to all data. You could also decide which permissions to add based on other properties of the authentication.

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

15.10.2.8Example: Retrieving Access Token Claims

This Script applies to the SMART Inbound Security module.

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.

We will also extract a complex object claim and assign additional permissions based on that.

Consider the following decoded access token claimset:

{
   "id": "ABCDEFG",
   "sub": "myusername",
   "azp": "my-client-id",
   "patient": "123",
   "scope": "openid profile patient/*.read",
   "iss": "http://example.com/oidc-issuer",
   "access": {
      "role": "admin",
      "since": 32098766472
   }
}

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.getClaim('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);

   // We'll use a value extracted from a complex claim to add a superuser
   // permission if this is an admin user (this permission grants the user
   // rights to other patient data as well)
   let accessClaim = theContext.getClaim('access');
   if (accessClaim.role === 'admin') {
      theOutcome.addAuthority('ROLE_FHIR_CLIENT_SUPERUSER_RO');
   }

   return theOutcome;
}

15.10.2.9Example: Nonstandard Scope Claim

This Script applies to the SMART Inbound Security module.

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.getClaim('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;
}

15.10.2.10Example: Populating fhirContext

This script will:

  • Add resource type to resource ID pairs, each of which will result in a row in fhirContext (ex 'provenance', 'xxx' results in "Provenance/123")
  • Add a reference to "PractitionerRole/123" without a role
  • Add a reference to "List/456" with the role set to "https://example.org/med-list-at-home"
function onAuthenticateSuccess(theOutcome, theOutcomeFactory, theContext) {
    // This method will result in an access token that contains the following:
   /*
   {
       ...
       "fhirContext": [
           { "reference" : "Provenance/xxx" }
           { "reference" : "Schedule/yyy" }
           { "reference" : "Schedule/zzz" }
           { "reference" : "Patient/abc" }
           { "reference" : "Encounter/def" }
           { "reference" : "PractitionerRole/123" }
           { "reference" : "List/456", "role": "https://example.org/med-list-at-home" }
       ],
       ...
   }
    */
   theOutcome.addLaunchResourceId('provenance', 'xxx');
   theOutcome.addLaunchResourceId('schedule', 'yyy');
   theOutcome.addLaunchResourceId('schedule', 'zzz');
   theOutcome.addLaunchResourceId('patient', 'abc');
   theOutcome.addLaunchResourceId('encounter', 'def');

   theOutcome.addFhirContextReference('PractitionerRole/123');
   theOutcome.addFhirContextReference('List/456', 'https://example.org/med-list-at-home');

   return theOutcome;
}

15.10.3Function: onAuthenticateSuccessClientCredentials

 

This function applies only to the SMART Outbound security module.

If present in the Post-Authorize Script defined in the SMART Outbound Security module, the onAuthenticateSuccessClientCredentials function will be called after a client has authenticated using the Client Credentials with JWT Credential flow. It can be used to add additional authorities to the client's authenticated session.

This function has two arguments:

  • theAuthorizationRequestDetails (argument 0) – Contains details about the client credentials authentication request. This object is of type ClientCredentialsAuthenticationRequest.
  • theClientSession (argument 1) – Contains the authenticated session, which is essentially the authenticated client, but can be modified by the script. This object is of type OAuth2ClientSession.

This function should not return any value.

15.10.3.1Example: Extract JWT Claim into Authorities and UserData

The following example shows a script that provides the user with two patient context options to select from.

This example also stores a value in the client session UserData map. An example of using this value later is shown in the Add Custom Claims and JSON Responses example below.

/**
 * This is a sample authentication callback script for the
 * SMART Outbound Security module. This script is invoked when
 * a client credentials grant with JWT credentials is being
 * performed, and can manipulate the client's session to
 * add authorities.
 *
 * @param theAuthorizationRequestDetails Contains details about the client credentials
 *                                       authentication request. This object is of type
 *                                       ClientCredentialsAuthenticationRequest.
 * @param theClientSession Contains the authenticated session, which is essentially the
 *                         authenticated client, but can be modified by the script. This
 *                         object is of type OAuth2ClientSession.
 */
function onAuthenticateSuccessClientCredentials(theAuthorizationRequestDetails, theClientSession) {

    // If the JWT used for client authentication contains a claim called
    // patientId, then we will add a security authority authorizing the
    // session to be able to read in the compartment belonging to the value
    // of that claim.
    if (theAuthorizationRequestDetails.jwtClaims['patientId']) {
        const patientIdClaim = theAuthorizationRequestDetails.jwtClaims['patientId'];
        theClientSession.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', patientIdClaim);

        // We will also store the patient ID in the client session userData
        // map so that we can refer to it from the onTokenGenerating(...)
        // script.
        const userData = {
            'patientId': patientIdClaim
        };
        theClientSession.addUserData(userData);

    }

}

15.10.4Function: onSmartLoginPreContextSelection(theUserSession, theContextSelectionChoices)

 

This function applies only to the SMART Outbound security module.

If present in the Post-Authorize Script defined in the SMART Outbound Security module, the onSmartLoginPreContextSelection function will be called after the user has authenticated. It should supply the context choices that will be presented to the user. See SMART on FHIR Outbound Security Module: Context Selection for more information on this flow.

It can also elect to not supply any choices. In this case, the authorization flow will continue as normal.

This function has two arguments:

  • theUserSession (argument 0) – This object contains details about the authenticated user and their session. This includes demographics, permissions, approved scopes, etc. The object will be of type UserSessionDetailsJson.
  • theContextSelectionChoices (argument 1) – This object is a holder for the selection choices. The function should add all options that the user will be allowed to choose from. If the script does not modify this object, the user will not be presented with the context selection screen and no changes will be made to the session launch context. This object is of type OAuth2SmartContextSelectionChoices.

This function should not return any value.

15.10.4.1Example

The following example shows a script that provides the user with two patient context options to select from.

/**
 * This function is called immediately after the user authenticates with the server,
 * in order to determine whether a context selection is needed.
 *
 * It should return the allowable contexts, which the user will then be allowed
 * to choose from.
 *
 * @param theUserSession     Contains details about the logged in user and
 *                           their session.
 * @param theContextSelectionChoices This object should be manipulated in the
 *                                   function in order to provide the choices
 *                                   the user can select from.
 */
function onSmartLoginPreContextSelection(theUserSession, theContextSelectionChoices) {

   /*
    * In this example, we're just going to hard code 2 choices. These could be pulled from
    * a FHIR repository (e.g. using Consent resources) or could even be pulled from a 3rd
    * party system.
    */
   let person1 = theContextSelectionChoices.addPerson();
   person1.familyName = 'Simpson';
   person1.givenName = 'Homer';
   person1.birthDate = '1950-01-12';
   // This is the resource ID that will ultimately be put into the launch context if this
   // person is selected by the user.
   person1.associatedPatientContextResourceId = "context-patient-id-111";
   // Any authorities added to the person record will be granted to the session only
   // if this person is picked. This can be used to grant appropriate permissions
   // to access the selected entity.
   person1.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + person1.associatedPatientContextResourceId)

   /*
    * Add a second person to choose from
    */

   let person2 = theContextSelectionChoices.addPerson();
   person2.familyName = 'Simpson';
   person2.givenName = 'Marge';
   person2.birthDate = '1954-06-22';
   person2.associatedPatientContextResourceId = "context-patient-id-222";
   person2.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + person2.associatedPatientContextResourceId);
   person2.addAutoGrantScopes('openid');
   person2.addRequestedScopes('launch profile patient/*.read system/Patient.read');

   let person3 = theContextSelectionChoices.addPerson();
   person3.familyName = 'Simpson';
   person3.givenName = 'Bart';
   person3.birthDate = '1979-12-17';
   person3.associatedPatientContextResourceId = "context-patient-id-333";
   person3.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + person3.associatedPatientContextResourceId);
   person3.removeRequestedScopes('patient/*.read profile');

   let person4 = theContextSelectionChoices.addPerson();
   person4.familyName = 'Simpson';
   person4.givenName = 'Lisa';
   person4.birthDate = '1981-05-09';
   person4.associatedPatientContextResourceId = "context-patient-id-444";
   person4.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + person4.associatedPatientContextResourceId);
   person4.addAutoGrantScopes('patient/*.write system/Patient.read');
   person4.removeAutoGrantScopes('patient/*.write');

   let person5 = theContextSelectionChoices.addPerson();
   person5.familyName = 'Simpson';
   person5.givenName = 'Maggie';
   person5.birthDate = '1989-01-14';
   person5.associatedPatientContextResourceId = "context-patient-id-555";
   person5.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + person5.associatedPatientContextResourceId);
   person5.addRequestedScopes('launch profile patient/*.read system/Patient.read patient/Observation.read');
   person5.removeRequestedScopes('system/Patient.read');
   person5.addAutoGrantScopes('system/Observation.read patient/*.write');
   person5.removeAutoGrantScopes('patient/*.write');

   let person6 = theContextSelectionChoices.addPerson();
   person6.familyName = 'Simpson';
   person6.givenName = 'Herb';
   person6.birthDate = '1948-01-12';
   person6.associatedPatientContextResourceId = "context-patient-id-123";
   person6.addAuthority('FHIR_READ_ALL_IN_COMPARTMENT', 'Patient/' + person6.associatedPatientContextResourceId);
   person6.addRequestedScopes('launch profile patient/*.read system/Patient.read patient/Observation.read');

   theContextSelectionChoices.addDisplayTranslation('system/Patient.read', 'Custom Display');
}

15.10.5Function: onSmartScopeAuthorityNarrowing

 

This function applies to the SMART Outbound and the SMART Inbound security modules.

If present in the module callback script, the onSmartScopeAuthorityNarrowing function will be called after the approved scopes have been used to reduce the session authorities.

If Enforce Approved Scopes to Restrict Permissions is enabled, this function is called immediately after the built-in logic is applied in order to automatically narrow scopes. If this setting is disabled, then the function will be called instead of the built-in logic.

This script is typically used to add or preserve authorities which can not be granted through the standard SMART framework scopes.

For example: Suppose you wanted to allow the user to invoke a custom operation called Patient/$foo, and you have therefore granted the user the FHIR_EXTENDED_OPERATION_ON_TYPE/Patient/$foo permission. The SMART framework does not provide any built-in scopes which allow a client to request access to this operation. So, a script could be used to automatically grant this permission based on an approved scope, or based on the existence of another authority.

This function has one argument:

  • theNarrowingResult (argument 0) – Contains details about the approved scopes, and the input authorities (the set of authorities granted to the session, prior to any scope based narrowing) as well as the output authorities (the set of authorities remaining after the scope based narrowing). Scripts may modify the output authorities by adding or removing from the list, and these changes will be reflected in the session. This object is of type SmartScopeAuthorityNarrowingResult.

This function should not return any value.

15.10.5.1Example

The following example shows a script that modifies the output authorities based on approved scopes and permissions.

/**
 * This function is called immediately after the session permissions have been
 * narrowed in accordance with the approved SMART scopes. The function can
 * examine the approved scopes and permissions, and can modify the final
 * permission set given to the session.
 *
 * @param theNarrowingResult Contains the inputs and outputs of the scope authority narrowing, and can be
 *                           modified to override or enhance the results of the narrowing process.
 */
function onSmartScopeAuthorityNarrowing(theNarrowingResult) {

    // In this example, we look for a custom scope named "manage_search_params",
    // and if it is approved for the session, we grant the session an additional
    // authority.
    if (theNarrowingResult.hasScope('manage_search_params')) {
        theNarrowingResult.addOutputAuthority('FHIR_MODIFY_SEARCH_PARAMETERS');
    }

    // If we have a read-in-compartment permission after the narrowing process,
    // also grant the ability to invoke LiveBundle operations, and to a custom
    // operation.
    if (theNarrowingResult.hasOutputAuthority('FHIR_READ_ALL_IN_COMPARTMENT')) {
        theNarrowingResult.addOutputAuthority('FHIR_LIVEBUNDLE');
        theNarrowingResult.addOutputAuthority('FHIR_EXTENDED_OPERATION_ON_TYPE', 'Patient/$my-custom-operation');
    }

}

15.10.6Function: onTokenGenerating(theUserSession, theAuthorizationRequestDetails, theClientDetails)

 

This function applies to the SMART Outbound and SMART Inbound security module.

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

15.10.6.1Parameters

  • 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. Note that only the key value pair that has the key listed in the Authorization Request Details property will be available.

  • theClientDetails – This parameter contains details about the client used for the authentication. This parameter will be of type OAuth2Client for interactive flows (e.g. Authorization Code), and will be of type OAuth2ClientSession for the Client Credentials with JWT Credential grant.

15.10.6.2Example: 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, theClientDetails) {

   // 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');

   theUserSession.addLaunchContextParameter('smart_style_url', 'http://acme.org/styles/smart_v1.json');
   
}

15.10.6.3Example: 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
 * @param theClientDetails The details about the OIDC client used in the auth flow
 */
function onTokenGenerating(theUserSession, theAuthorizationRequestDetails, theClientDetails) {

   // Add a simple string claim
   theAuthorizationRequestDetails.addAccessTokenClaim('claimName', 'claimValue');

   // Add a simple numeric claim
   theAuthorizationRequestDetails.addAccessTokenClaim('numericClaimName', 123);

   // Add a complex JSON object claim
   const claimValue = {
      someArray: [1, 2, 3.3, null, true],
      someNestedObject: {
         family: 'Simpson',
         given: 'Homer'
      }
   };
   theAuthorizationRequestDetails.addAccessTokenClaim('objectClaim', claimValue);
}

Note that claims prefixed with smile_ are reserved. Adding claims prefixed with smile_ to the access token is verboten.

15.10.6.4Example: 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, theClientDetails) {

   // 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');
    
}

15.10.6.5Example: Working with OIDC Client ID, audience, and other initial request parameters

Once the onTokenGenerating callback executes, it is now possible to access to the OIDC client ID from the UserSession and audience from the AuthorizationRequestDetails, as well as other initial OAuth2 request parameters.

/**
* 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
* @param theClientDetails The details about the OIDC client used in the auth flow
 */
  function onTokenGenerating(theUserSession, theAuthorizationRequestDetails, theClientDetails) {
     let oidcClientId = theUserSession.getOidcClientId()
     let audience = theAuthorizationRequestDetails.getAudience()
     let requestParameters = theAuthorizationRequestDetails.getRequestParameters()
   
      Log.info("oidcClientId: " + oidcClientId);
      Log.info("audience: " + audience);
      Log.info("requestParameters: " + requestParameters);
}

15.10.6.6Example: Add Custom Claims and JSON Responses

This example adds a custom claim to the Access Token and a custom value to the Token Response. See the Extract JWT Claim into Authorities and UserData example above to see an example of populating the client session UserData value that is used by this script.

/**
 * 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)
 * @param theAuthorizationRequestDetails Contains details about the authorization request
 * @param theClientDetails The details about the OIDC client used in the auth flow
 */
function onTokenGenerating(theUserSession, theAuthorizationRequestDetails, theClientDetails) {
    const patientId = theClientDetails.getUserData('patientId');

    // Make sure we had previously stored a patientId in the client session userData
    if (!patientId) {
        throw 'No patientId stored in userData';
    }

    // Add the patient ID as a claim to the generated access token
    theAuthorizationRequestDetails.addAccessTokenClaim('patientId', patientId);

    // Add the patient ID to the JSON token response object
    theAuthorizationRequestDetails.addTokenResponseValue('patientId', patientId);
}

15.10.7Function: onPostAuthorize(theDetails)

 

This function applies only to the SMART Outbound security module.

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.

15.10.7.1Example

/**
 * 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.

15.10.8Function: authenticate(theRequest, theOutcomeFactory)

 

This function applies to the Scripted Inbound, SAML Inbound, and SMART Inbound security modules as well as the CODAP Grant type in the SMART Outbound Security module.

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

Inputs:

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

Output:

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

15.10.8.1Example

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

15.10.9Function: getUserName(theOidcUserInfoMap, theServerInfo)

 

This function applies only to the SMART Inbound and SMART Outbound security modules.

By default, Smile CDR generates local usernames by appending issuer and subject. This behavior can be customized by supplying a custom username mapping. This mapping receives Access token claims and server info.

15.10.9.1Example

/**
 * This is a sample username mapping callback script
 *
 * @param theOidcUserInfoMap OIDC claims from the Access 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 username for the external user.  
 */
function getUserName(theOidcUserInfoMap, theServerInfo) {
   return "EXT_USER:" + theOidcUserInfoMap['preferred_username'];
}