Skip to content

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:
    1. requestPayload: String with the payload
    2. userIdentifier: Unique identifier for the user on the device (String, can be null)
    3. registrationData: Registration data that was returned in a previous execution (String, can be null)
    4. 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 like id (required) or amr (optional). The user 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.