Writing scripts¶
Running custom JavaScript scripts is the core functionality of the Onegini Extension Engine. This topic guide will explain how to implement and configure such scripts.
Overview¶
Each script is executed using Oracle Nashorn. By default, access to Java classes is disabled. See Configuring access to Java classes for more information.
Script format¶
A script should consist of an execute
function, along with any number of helper functions. The execute
function must meet the following requirements:
- Name:
execute
- Arguments:
requestPayload
: String with the payloaduserIdentifier
: Unique identifier for the user on the device (String, can benull
)registrationData
: Registration data that was returned in a previous execution (String, can benull
)hookContextCustomParams
: A map of custom Web Hooks context parameters (Map>, can be null
)
- Returns: An object with the following properties:
status
: Integer that indicates whether the operation was successful (required)user
: Map of data containing user information likeid
(required) oramr
(optional). Theuser
object may also contain any other attributes describing the authenticated user.responsePayload
: String with a payload that will be interpreted by the caller (optional)registrationData
: String with registration data that should be stored by the caller for future requests (optional)
The execute
function is responsible for:
- Parsing the
requestPayload
- Performing the communication with the API of the authenticator
- Interpreting the response of that API as successful or otherwise
- Converting the response from the API into a
responsePayload
string that is understood by the caller - Returning a
status
that is understood by the caller to indicate whether the execution was successful
Accessing configuration properties¶
The Onegini Extension Engine includes the option of configuring properties in its database. This has several benefits. For one, sensitive values like credentials used to communicate with an authenticator API are stored encrypted. It also makes it easier to update these values without changing the scripts, for example when moving from the test to the production environment.
These properties are available to the JS functions through the configuration
object, and can be accessed using several methods on this object.
Accessing a single property:
var serverUrl = configuration.get('authenticators_myApi_serverUrl');
Accessing all properties:
var configMap = configuration.getAll();
var serverUrl = configMap['authenticators_myApi_serverUrl'];
In this case, authenticators_myApi_serverUrl
is the unique identifier of the config property. The last method is available in order to minimize the number of
database queries.
Logging¶
Console logging is available to the JavaScript functions through the global LOG
object. This object is an instance of
the SLF4J Logger class. It can be used in JavaScript in the same way as in Java classes.
LOG.debug("Test value {}", value);
LOG.error("Could not reach the server", exception);
Using the Cache¶
Cache is available within a script if you need to store data that will be utilized at a future script execution. A global CACHE
object is available to store
and fetch data via a key/value pair
CACHE.store("sampleKey", "sampleValue");
CACHE.store("sampleKey", "sampleValue", 100); //sets TTL to 100 seconds for this key.
var value = CACHE.fetch("sampleKey");
CACHE.delete("sampleKey");
Using the RestTemplate¶
The Spring Framework RestTemplate
class is also exposed by default to make REST calls easier. A global REST_TEMPLATE
object is available to make REST calls.
//GET
var xml = REST_TEMPLATE.getForEntity("http://example.com", java.lang.String.class).getBody();
//POST
var headers = new org.springframework.http.HttpHeaders();
headers.add("headerName", "headerValue");
var requestMap = new org.springframework.util.LinkedMultiValueMap();
requestMap.add("paramName", "paramValue");
var entity = new org.springframework.http.HttpEntity(requestMap, headers);
var response = REST_TEMPLATE.postForEntity("http://example.com", entity, java.lang.String.class);
These are just a few sample usages. Any other usages you can normally do with a restTemplate Java object is also possible.
Using the password encryptor¶
Onegini CIM's APIs require that account passwords are sent encrypted. A global PASSWORD_ENCRYPTOR
object is available to encrypt passwords.
The code snippet below shows a usage example. The example contains a bit of boilerplate code to customize error handling in case password encryption fails.
try {
var passwordEncryptionKey = configuration.get('idp_password_encryption_key');
var currentPassword = PASSWORD_ENCRYPTOR.encrypt("currentPassword", passwordEncryptionKey);
var iv = currentPassword.getIv();
var newPassword = PASSWORD_ENCRYPTOR.encrypt("newPassword", passwordEncryptionKey, iv);
var encryptedCurrentPassword = currentPassword.getPassword();
var encryptedNewPassword = newPassword.getPassword();
// Interaction with CIM API...
} catch (e) {
// Error handling: in case the encrypt method throws an exception
var errorMessage = e.toString();
if (errorMessage.contains("IllegalArgumentException")) {
LOG.debug(e);
// Either the key or plaintext password are empty
} else if (errorMessage.contains("InvalidAlgorithmParameterException")) {
LOG.debug(e);
// password encryption key has an invalid size.
} else {
LOG.debug(e)
// encryption failed
}
// Return error response
}
NOTE: You must NEVER store the encryption key in the script. Always use a configuration property!
As the code snippet above shows, the encrypt
method can throw Java exceptions. The following exceptions are the most important:
- IllegalArgumentException: thrown in case either the key or plaintext password are empty
- InvalidAlgorithmParameterException: thrown in case the password encryption key has an invalid length.
Example script¶
function execute(requestPayload, userIdentifier, registrationData, hookContextCustomParams) {
var requestImage = getImage(requestPayload);
var registrationImage = getImage(registrationData);
var verificationResult = verifyUser(userIdentifier, requestImage, registrationImage);
var status = verificationSucceeded(verificationResult) ? 2000 : 4000;
return {
status: status,
responsePayload: resultToString(verificationResult),
registrationData: getRegistrationData(userIdentifier, registrationImage, requestImage),
user: {
id: "user123",
amr: ["mfa", "pwd"],
firstName: "John",
lastName: "Doe",
email: "[email protected]"
}
};
}
function getImage(imageData) {
LOG.debug('Stub to extract the image.');
var body = JSON.parse(imageData);
return body['image'];
}
function verifyUser(userIdentifier, requestImage, registrationImage) {
var responseInputStream = postImages(userIdentifier, requestImage, registrationImage);
return readResponse(responseInputStream);
}
function postImages(userIdentifier, requestImage, registrationImage) {
var serverUrl = configuration.get('authenticators_myApi_serverUrl');
LOG.debug('TODO: Create HTTP request to verify the image for user {} to server {}', userIdentifier, serverUrl);
// Stubbing a response
var response;
if (requestImage === registrationImage) {
response = "success!";
} else {
response = "failure!";
}
return new java.io.ByteArrayInputStream(response.getBytes());
}
function readResponse(responseInputStream) {
var sb = new java.lang.StringBuilder();
var inputStreamReader = new java.io.InputStreamReader(responseInputStream);
var reader = new java.io.BufferedReader(inputStreamReader);
var inputLine;
while ((inputLine = reader.readLine()) !== null) {
sb.append(inputLine);
}
reader.close();
inputStreamReader.close();
return sb.toString();
}
function verificationSucceeded(verificationResult) {
LOG.debug('Stub to interpret the verification result');
return verificationResult === 'success!';
}
function resultToString(verificationResult) {
LOG.debug('Stub to return the result as String');
return JSON.stringify({result: verificationResult});
}
function getRegistrationData(userIdentifier, image, requestImage) {
LOG.debug("An example of registration data.");
return JSON.stringify({
userIdentifier: userIdentifier,
image: image,
latestImage: requestImage
});
}
General Troubleshooting¶
If you receive an error like this:
java.lang.ClassCastException: Cannot cast jdk.nashorn.internal.runtime.NativeJavaPackage to java.lang.Class
You are likely attempting to use a class within your script that you have not exposed properly via the configuration. See configure access to Java classes for more information.