Thursday 17 November 2016

Calling external REST endpoints in OpenAMv13.5 scripted authorization policies

Summary

It is often useful to be able to call external services as part of an authorisation policy in OpenAM.  One such example is a policy that does a check to see if the IP address of the calling user is located in the same country as the registered address for the user.  Now, there's an out of the box scripted policy condition that does just this that relies on external services it calls using 'GET' requests.  I thought it might be nice to add some functionality to this policy that sent me a text message (SMS) when the policy determined that it was being evaluated from an IP address from a country other than my own.  This could act as a warning to me that my account has been compromised and is being used by someone else, somewhere else in the world.  A colleague had also been doing a little bit of integration work with Twilio who happen to provide RESTful endpoints for sending SMS so I decided to adopt Twilio for this purpose.  That Twilio is the endpoint here is of little consequence as this approach will work for any service provider, but it gave me a real service for SMS notifications.

The solution

Well, that's easy isn't it... there's a Developers Guide that explains the scripting API for OpenAM: https://backstage.forgerock.com/#!/docs/openam/13.5/dev-guide#scripting-api
We just use that, looking at how the existing external calls work in the policy, and we're done, right?
Err, no, wrong, as it turns out!
Tried that and it didn't work :(

The problem

The Twilio endpoint requires a POST of data including an Authorization and Content-Type header.  This should be fine.  But the httpClient.post method as described in the guide simply wouldn't send the required HTTP headers.
It turns out the 'post' and 'get' methods uses RESTlet under the covers which has very specific methods for including standardised HTTP headers.  Unfortunately these methods aren't exposed by the httpClient object available in the script.  The OpenAM developer documentation suggests that you should just be able to set these as headers in the 'requestdata' parameter but as the underlying code does not use the specific RESTlet methods for adding these then the RESTlet framework discards them.

As an example, from the guide, you might try to write code like this:
var response = httpClient.post("http://example.com:8080/openam/json/users/" + username, "", { cookies:[ { "domain": ".example.com", "field": "iPlanetDirectoryPro", "value": "E8cDkvlad83kd....KDodkIEIx*DLEDLK...JKD09d" } ], headers:[ { "field": "Content-Type", "value": "application/json" } ] });

If you do, then you'll see that the Content-Type header is not sent because it is considered standard.  However, if the header was a custom header then it would be passed to the destination.

The other thing you might notice in the logs is that the methods indicated by the developer guide are now marked as deprecated.  They still work - to a fashion - but an alternative method 'send' is recommended.  Unfortunately the guides don't describe this method...hence this blog post!

The real solution

So the real solution is to use the new 'send' method of the httpClient object.  This accepts one parameter 'request' which is defined as the following type:

    org.forgerock.http.protocol.Request

So within our script we should define a Request object and set the appropriate parameters before passing it as a parameter to the httpClient.send method.

Great, easy right? Well, err, that depends...

As I was using a copy of the default policy authorization script this was defined as Javascript.  So I needed to import the necessary class using Javascript.  And, as I discovered, I was using Java8 which changed the engine from Rhino to Nashorn which recommends the 'JavaImporter' mechanism for importing packages

So, with this is at the top of my script:
    var fr = new JavaImporter(org.forgerock.http.protocol)

I can now instantiate a new Request object like this:
    with (fr) {
      var request = new fr.Request();
    } 
Note the use of the 'with' block.

Now I can set the necessary properties of the request object so that I can call the Twilio API.  This API requires the HTTP headers specified, to be POSTed, with url-encoded body contents that describe the various details of the message Twilio will issue.  All this needs to be done within the 'with' block highlighted above:
    request.method = 'POST';
    request.setUri("https://twilio-url/path/resource");
    request.getHeaders().add('Content-Type', 'application/x-www-form-urlencoded');
    request.getHeaders().add('Authorization', 'Basic abcde12345');
    request.setEntity("url%20encoded%20body%20contents");

Ok, so now I can send my request parameter?
Well, yes, but I also need to handle the response, which for the 'send' method is a Promise (defined as org.forgerock.util.promise.Promise) for a Response object (defined as org.forgerock.http.protocol.Response)

So this is another package I need to import into my Javascript file in order to access the Promise class.  JavaImporter takes multiple parameters to make them all available to the assigned variable so you can use them all within the same 'with' block.  Therefore my import line now looks like:
 var fr = new JavaImporter(org.forgerock.http.protocol, org.forgerock.util.promise)

And, within the 'with' block, I now include:
    promise = httpClient.send(request);
    var response = promise.get();

Which will execute the desired external call and return the response into the response variable.

So now I can use the Response properties and methods to check the call was successful e.g:
     response.getStatus();
     response.getCause();

So my script for calling an external service looks something like this:
var fr = new JavaImporter(org.forgerock.http.protocol, org.forgerock.util.promise)
function postSMS() {
  with (fr) {
    var request = new Request();
    request.method = 'POST';
    request.setUri("https://twilio-url/path/resource");
    request.getHeaders().add('Content-Type', 'application/x-www-form-urlencoded');
    request.getHeaders().add('Authorization', 'Basic abcde12345');
    request.setEntity("url%20encoded%20body%20contents");
    promise = httpClient.send(request);
    var response = promise.get();
    logger.message("Twilio Call. Status: " + response.getStatus() + ", Cause: " + response.getCause());
  }
postSMS();

Great, so we're done now?

Well, almost!

The various classes defined by the imported packages are not all 'whitelisted'. This is an OpenAM feature that controls which classes can be used within scripts. We therefore need to add the necessary classes to the 'whitelist' for Policy scripts which can be in the Global Services page of the admin console. As you run the script you'll see the log files will produce an error if the necessary class is not whitelisted. You can use this approach to see what is required then add the highlighted class to the list.

And, now we're done... happy RESTing!