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:
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:
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: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:
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;
}
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
property is used to supply a human-readable description of why the authentication failed.unknownUsername
property may be set to true
to indicate that the authentication failed due to an unknown or invalid username.incorrectPassword
property may be set to true
to indicate that the authentication failed due to an incorrect password for the given username.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');
}
}
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.
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;
}
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;
}
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;
}
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;
}
This script will:
fhirContext
(ex 'provenance', 'xxx' results in "Provenance/123")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;
}
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.
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);
}
}
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.
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');
}
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.
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');
}
}
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).
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.
/**
* 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');
}
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.
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');
}
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);
}
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);
}
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.
/**
* 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.
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:
/**
* 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;
}
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.
/**
* 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'];
}