On this page:

12.10Authentication 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.

12.10.1Function: onAuthenticateSuccess

 

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

Parameters

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

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.

Example

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

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

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

User 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.getStringClaim('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) {
   if (theUserSession.hasUserData('patientRepresentative')) {
      // clear some sensitive data.
      theResource.clear('value');
   }
}

12.10.2Function: onSmartLoginPreContextSelection(theUserSession, theContextSelectionChoices)

 

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.

Thie function should not return any value.

12.10.3Example Script

 

The following example shows a script that provides the user with two poatient 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 grnated 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)

}

12.10.4Function: onTokenGenerating(theUserSession, theAuthorizationRequestDetails)

 

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

Parameters

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

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

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

}

Example: 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
 */
function onTokenGenerating(theUserSession, theAuthorizationRequestDetails) {

   theAuthorizationRequestDetails.addAccessTokenClaim('claimName', 'claimValue');

}

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

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

12.10.5Function: onPostAuthorize(theDetails)

 

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.

Example

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

12.10.6Function: authenticate(theRequest, theOutcomeFactory)

 

This function applies only to the CODAP Grant type described above. It is placed in the CODAP Authorization Script setting on 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.

Example:

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