Extending OpenIG’s Functionality This chapter covers extending what OpenIG can do. In this chapter, you will learn to: Write scripts to create custom filters and handlers Plug additional Java libraries into OpenIG for further customization To extend filter and handler functionality, OpenIG supports the Groovy dynamic scripting language through the use of ScriptableFilter and ScriptableHandler objects. For when you can’t achieve complex server interactions or intensive data transformations with scripts or existing handlers, filters, or expressions, OpenIG allows you to develop custom extensions in Java and provide them in additional libraries that you build into OpenIG. The libraries allow you to develop custom extensions to OpenIG. When you are writing scripts or Java extensions, never use a Promise blocking method, such as get(), getOrThrow(), or getOrThrowUninterruptibly(), to obtain the response. A promise represents the result of an asynchronous operation. Therefore, using a blocking method to wait for the result can cause deadlocks and/or race issues. About Scripting Scriptable filters and handlers are added to the configuration in the same way as standard filters and handlers. Each takes as its configuration the script’s Internet media type and either a source script included in the JSON configuration, or a file script that OpenIG reads from a file. The configuration can optionally supply arguments to the script. The following example defines a ScriptableFilter, written in the Groovy language, and stored in a file named $HOME/.openig/scripts/groovy/SimpleFormLogin.groovy (%appdata%\OpenIG\scripts\groovy\SimpleFormLogin.groovy on Windows): { "name": "SimpleFormLogin", "type": "ScriptableFilter", "config": { "type": "application/x-groovy", "file": "SimpleFormLogin.groovy" } } Relative paths in the file field depend on how OpenIG is installed. If OpenIG is installed in an application server, then paths for Groovy scripts are relative to $HOME/.openig/scripts/groovy. This base location $HOME/.openig/scripts/groovy is on the classpath when the scripts are executed. If therefore some Groovy scripts are not in the default package, but instead have their own package names, they belong in the directory corresponding to their package name. For example, a script in package com.example.groovy belongs under $HOME/.openig/scripts/groovy/com/example/groovy/. OpenIG provides several global variables to scripts at runtime. As well as having access to Groovy’s built-in functionality, scripts can access the request and the context, store variables across executions, write messages to logs, make requests to a web service or to an LDAP directory service, and access responses returned in promise callback methods. For information about scripting in OpenIG, see ScriptableFilter(5) in the Configuration Reference and ScriptableHandler(5) in the Configuration Reference. Before trying the scripts shown in this chapter, first install and configure OpenIG as described in Getting Started. When developing and debugging your scripts, consider configuring a capture decorator to log requests, responses, and context data in JSON form. You can then turn off capturing when you move to production. For details, see CaptureDecorator(5) in the Configuration Reference. Scripting Dispatch In order to route requests, especially when the conditions are complicated, you can use a ScriptableHandler instead of a DispatchHandler as described in DispatchHandler(5) in the Configuration Reference. The following script demonstrates a simple dispatch handler: /* * This simplistic dispatcher matches the path part of the HTTP request. * If the path is /mylogin, it checks Username and Password headers, * accepting bjensen:hifalutin, and returning HTTP 403 Forbidden to others. * Otherwise it returns HTTP 401 Unauthorized. */ // Rather than return a Promise of a response from an external source, // this script returns the response itself. response = new Response(); switch (request.uri.path) { case "/mylogin": if (request.headers.Username.values[0] == "bjensen" && request.headers.Password.values[0] == "hifalutin") { response.status = Status.OK response.entity = "<html><p>Welcome back, Babs!</p></html>" } else { response.status = Status.FORBIDDEN response.entity = "<html><p>Authorization required</p></html>" } break default: response.status = Status.UNAUTHORIZED response.entity = "<html><p>Please <a href='./mylogin'>log in</a>.</p></html>" break } // Return the locally created response, no need to wrap it into a Promise return response To try this handler, save the script as $HOME/.openig/scripts/groovy/DispatchHandler.groovy (%appdata%\OpenIG\scripts\groovy\DispatchHandler.groovy on Windows). Next, add the following route to your configuration as $HOME/.openig/config/routes/98-dispatch.json (%appdata%\OpenIG\config\routes\98-dispatch.json on Windows): { "heap": [ { "name": "DispatchHandler", "type": "DispatchHandler", "config": { "bindings": [{ "condition": "${matches(request.uri.path, '/mylogin')}", "handler": { "type": "Chain", "config": { "filters": [ { "type": "HeaderFilter", "config": { "messageType": "REQUEST", "add": { "Username": [ "bjensen" ], "Password": [ "hifalutin" ] } } } ], "handler": "Dispatcher" } } }, { "handler": "Dispatcher" } ] } }, { "name": "Dispatcher", "type": "ScriptableHandler", "config": { "type": "application/x-groovy", "file": "DispatchHandler.groovy" } } ], "handler": "DispatchHandler" } The route sets up the headers required by the script when the user logs in. To try it out, browse to http://openig.example.com:8080. The response from the script says, "Please log in." When you click the log in link, the HeaderFilter sets Username and Password headers in the request, and passes the request to the script. The script then responds, Welcome back, Babs! Scripting HTTP Basic Authentication HTTP Basic authentication calls for the user agent such as a browser to send a user name and password to the server in an Authorization header. HTTP Basic authentication relies on an encrypted connection to protect the user name and password credentials, which are base64-encoded in the Authorization header, not encrypted. The following script, for use in a ScriptableFilter, adds an Authorization header based on a username and password combination: /* * Perform basic authentication with the user name and password * that are supplied using a configuration like the following: * * { * "name": "BasicAuth", * "type": "ScriptableFilter", * "config": { * "type": "application/x-groovy", * "file": "BasicAuthFilter.groovy", * "args": { * "username": "bjensen", * "password": "hifalutin" * } * } * } */ def userPass = username + ":" + password def base64UserPass = userPass.getBytes().encodeBase64() request.headers.add("Authorization", "Basic ${base64UserPass}" as String) // Credentials are only base64-encoded, not encrypted: Set scheme to HTTPS. /* * When connecting over HTTPS, by default the client tries to trust the server. * If the server has no certificate * or has a self-signed certificate unknown to the client, * then the most likely result is an SSLPeerUnverifiedException. * * To avoid an SSLPeerUnverifiedException, * set up HTTPS correctly on the server. * Either use a server certificate signed by a well-known CA, * or set up the gateway to trust the server certificate. */ request.uri.scheme = "https" // Calls the next Handler and returns a Promise of the Response. // The Response can be handled with asynchronous Promise callbacks. next.handle(context, request) To try this filter, save the script as $HOME/.openig/scripts/groovy/BasicAuthFilter.groovy (%appdata%\OpenIG\scripts\groovy\BasicAuthFilter.groovy on Windows). Next, add the following route to your configuration as $HOME/.openig/config/routes/09-basic.json (%appdata%\OpenIG\config\routes\09-basic.json on Windows): { "handler": { "type": "Chain", "config": { "filters": [ { "type": "ScriptableFilter", "config": { "type": "application/x-groovy", "file": "BasicAuthFilter.groovy", "args": { "username": "bjensen", "password": "hifalutin" } }, "capture": "filtered_request" } ], "handler": { "type": "StaticResponseHandler", "config": { "status": 200, "reason": "OK", "entity": "Hello, Babs!" } } } }, "condition": "${matches(request.uri.path, '^/basic')}" } When the request path matches /basic the route calls the Chain, which runs the ScriptableFilter. The capture setting captures the request as updated by the ScriptableFilter. Finally, OpenIG returns a static page. To try it out, browse to http://openig.example.com:8080/basic. The captured request in the console log shows that the scheme is now HTTPS, and that the Authorization header is set for HTTP Basic: GET https://openig.example.com:8080/basic HTTP/1.1 Authorization: Basic YmplbnNlbjpoaWZhbHV0aW4= Scripting LDAP Authentication Many organizations use an LDAP directory service to store user profiles including authentication credentials. The LDAP directory service securely stores user passwords in a highly-available, central service capable of handling thousands of authentications per second. The following script, for use in a ScriptableFilter, performs simple authentication against an LDAP server based on request form fields username and password: import org.forgerock.opendj.ldap.* /* * Perform LDAP authentication based on user credentials from a form. * * If LDAP authentication succeeds, then return a promise to handle the response. * If there is a failure, produce an error response and return it. */ username = request.form?.username[0] password = request.form?.password[0] // For testing purposes, the LDAP host and port are provided in the context's attributes. // Edit as needed to match your directory service. host = attributes.ldapHost ?: "localhost" port = attributes.ldapPort ?: 1389 client = ldap.connect(host, port as Integer) try { // Assume the username is an exact match of either // the user ID, the email address, or the user's full name. filter = "(|(uid=%s)(mail=%s)(cn=%s))" user = client.searchSingleEntry( "ou=people,dc=example,dc=com", ldap.scope.sub, ldap.filter(filter, username, username, username)) client.bind(user.name as String, password?.toCharArray()) // Authentication succeeded. // Set a header (or whatever else you want to do here). request.headers.add("Ldap-User-Dn", user.name.toString()) // Most LDAP attributes are multi-valued. // When you read multi-valued attributes, use the parse() method, // with an AttributeParser method // that specifies the type of object to return. attributes.cn = user.cn?.parse().asSetOfString() // When you write attribute values, set them directly. user.description = "New description set by my script" // Here is how you might read a single value of a multi-valued attribute: attributes.description = user.description?.parse().asString() // Call the next handler. This returns when the request has been handled. return next.handle(context, request) } catch (AuthenticationException e) { // LDAP authentication failed, so fail the response with // HTTP status code 403 Forbidden. response = new Response() response.status = Status.FORBIDDEN response.entity = "<html><p>Authentication failed: " + e.message + "</p></html>" } catch (Exception e) { // Something other than authentication failed on the server side, // so fail the response with HTTP 500 Internal Server Error. response = new Response() response.status = Status.INTERNAL_SERVER_ERROR response.entity = "<html><p>Server error: " + e.message + "</p></html>" } finally { client.close() } // Return the locally created response, no need to wrap it into a Promise return response For the list of methods to specify which type of objects to return, see the OpenDJ LDAP SDK Javadoc for AttributeParser. To try the LDAP authentication script, follow these steps: Install an LDAP directory server such as ForgeRock Directory Services or OpenDJ directory server. Either import some sample users who can authenticate over LDAP, or generate sample users at installation time. Save the script as $HOME/.openig/scripts/groovy/LdapAuthFilter.groovy (%appdata%\OpenIG\scripts\groovy\LdapAuthFilter.groovy on Windows). If the directory server installation does not match the assumptions made in the script, adjust the script to use the correct settings for your installation. Add the following route to your configuration as $HOME/.openig/config/routes/10-ldap.json (%appdata%\OpenIG\config\routes\10-ldap.json on Windows): { "handler": { "type": "Chain", "config": { "filters": [ { "type": "ScriptableFilter", "config": { "type": "application/x-groovy", "file": "LdapAuthFilter.groovy" } } ], "handler": { "type": "ScriptableHandler", "config": { "type": "application/x-groovy", "source": "import org.forgerock.http.protocol.Response; import org.forgerock.http.protocol.Status; dn = request.headers['Ldap-User-Dn'].values[0]; entity = '<html><p>Ldap-User-Dn: ' + dn + '</p></html>'; response = new Response(Status.OK); response.entity = entity; return response" } } } }, "condition": "${matches(request.uri.path, '^/ldap')}" } The route calls the LdapAuthFilter.groovy script to authenticate the user over LDAP. On successful authentication, it responds with the the bind DN. To test the configuration, browse to a URL where query string parameters specify a valid username and password, such as http://openig.example.com:8080/ldap?username=user.0&password=password. The response from the script shows the DN: Ldap-User-Dn: uid=user.0,ou=People,dc=example,dc=com. Scripting SQL Queries You can use a ScriptableFilter to look up information in a relational database and include the results in the request context. The following filter looks up user credentials in a database given the user’s email address, which is found in the form data of the request. The script then sets the credentials in headers, making sure the scheme is HTTPS to protect the request when it leaves OpenIG: /* * Look up user credentials in a relational database * based on the user's email address provided in the request form data, * and set the credentials in the request headers for the next handler. */ def client = new SqlClient() def credentials = client.getCredentials(request.form?.mail[0]) request.headers.add("Username", credentials.Username) request.headers.add("Password", credentials.Password) // The credentials are not protected in the headers, so use HTTPS. request.uri.scheme = "https" // Calls the next Handler and returns a Promise of the Response. // The Response can be handled with asynchronous Promise callbacks. next.handle(context, request) The previous script demonstrates a ScriptableFilter that uses a SqlClient class defined in another script. The following code listing shows the SqlClient class: import groovy.sql.Sql import javax.naming.InitialContext import javax.sql.DataSource /** * Access a database with a well-known structure, * in particular to get credentials given an email address. */ class SqlClient { // Get a DataSource from the container. InitialContext context = new InitialContext() DataSource dataSource = context.lookup("jdbc/forgerock") as DataSource def sql = new Sql(dataSource) // The expected table is laid out like the following. // Table USERS // ---------------------------------------- // | USERNAME | PASSWORD | EMAIL |...| // ---------------------------------------- // | <username>| <passwd> | <mail@...>|...| // ---------------------------------------- String tableName = "USERS" String usernameColumn = "USERNAME" String passwordColumn = "PASSWORD" String mailColumn = "EMAIL" /** * Get the Username and Password given an email address. * * @param mail Email address used to look up the credentials * @return Username and Password from the database */ def getCredentials(mail) { def credentials = [:] def query = "SELECT " + usernameColumn + ", " + passwordColumn + " FROM " + tableName + " WHERE " + mailColumn + "='$mail';" sql.eachRow(query) { credentials.put("Username", it."$usernameColumn") credentials.put("Password", it."$passwordColumn") } return credentials } } To try the script, follow these steps: Follow the tutorial in Log in With Credentials From a Database. When everything in that tutorial works, you know that OpenIG can connect to the database, look up users by email address, and successfully authenticate to the sample application. Save the scripts as $HOME/.openig/scripts/groovy/SqlAccessFilter.groovy (%appdata%\OpenIG\scripts\groovy\SqlAccessFilter.groovy on Windows), and as $HOME/.openig/scripts/groovy/SqlClient.groovy (%appdata%\OpenIG\scripts\groovy\SqlClient.groovy on Windows). Add the following route to your configuration as $HOME/.openig/config/routes/11-db.json (%appdata%\OpenIG\config\routes\11-db.json on Windows): { "handler": { "type": "Chain", "config": { "filters": [ { "type": "ScriptableFilter", "config": { "type": "application/x-groovy", "file": "SqlAccessFilter.groovy" } }, { "type": "StaticRequestFilter", "config": { "method": "POST", "uri": "http://app.example.com:8081", "form": { "username": [ "${request.headers['Username'][0]}" ], "password": [ "${request.headers['Password'][0]}" ] } } } ], "handler": "ClientHandler" } }, "condition": "${matches(request.uri.path, '^/db')}" } The route calls the ScriptableFilter to look up credentials over SQL. It then uses calls a StaticRequestFilter to build a login request. Although the script sets the scheme to HTTPS, the StaticRequestFilter ignores that and resets the URI. This makes it easier to try the script without additional steps to set up HTTPS. To try the configuration, browse to a URL where a query string parameter specifies a valid email address, such as http://openig.example.com:8080/db?mail=george@example.com. If the lookup and authentication are successful, you see the profile page of the sample application. Developing Custom Extensions OpenIG includes a complete Java application programming interface to allow you to customize OpenIG to perform complex server interactions or intensive data transformations that you cannot achieve with scripts or the existing handlers, filters, and expressions described in Expressions(5) in the Configuration Reference. Key Extension Points Interface Stability: Evolving (For information, see Product Interface Stability in the Configuration Reference.) The following interfaces are available: Decorator A Decorator adds new behavior to another object without changing the base type of the object. When suggesting custom Decorator names, know that OpenIG reserves all field names that use only alphanumeric characters. To avoid clashes, use dots or dashes in your field names, such as my-decorator. ExpressionPlugin An ExpressionPlugin adds a node to the Expression context tree, alongside env (for environment variables), and system (for system properties). For example, the expression ${system['user.home']} yields the home directory of the user running the application server for OpenIG. In your ExpressionPlugin, the getKey() method returns the name of the node, and the getObject() method returns the unified expression language context object that contains the values needed to resolve the expression. The plugins for env and system return Map objects, for example. When you add your own ExpressionPlugin, you must make it discoverable within your custom library. You do this by adding a services file named after the plugin interface, where the file contains the fully qualified class name of your plugin, under META-INF/services/org.forgerock.openig.el.ExpressionPlugin in the .jar file for your customizations. When you have more than one plugin, add one fully qualified class name per line. For details, see the reference documentation for the Java class ServiceLoader. If you build your project using Maven, then you can add this under the src/main/resources directory. As described in Embedding the Customization in OpenIG, you must add your custom libraries to the WEB-INF/lib/ directory of the OpenIG .war file that you deploy. Be sure to provide some documentation for OpenIG administrators on how your plugin extends expressions. Filter A Filter serves to process a request before handing it off to the next element in the chain, in a similar way to an interceptor programming model. The Filter interface exposes a filter() method, which takes a Context, a Request, and the Handler, which is the next filter or handler to dispatch to. The filter() method returns a Promise that provides access to the Response with methods for dealing with both success and failure conditions. A filter can elect not to pass the request to the next filter or handler, and instead handle the request itself. It can achieve this by merely avoiding a call to next.handle(context, request), creating its own response object and returning that in the promise. The filter is also at liberty to replace a response with another of its own. A filter can exist in more than one chain, therefore should make no assumptions or correlations using the chain it is supplied. The only valid use of a chain by a filter is to call its handle() method to dispatch the request to the rest of the chain. OpenIG also provides the convenience class, GenericHeapObject, to help with configuration. Handler A Handler generates a response for a request. The Handler interface exposes a handle() method, which takes a Context, and a Request. It processes the request and returns a Promise that provides access to the Response with methods for dealing with both success and failure conditions. A handler can elect to dispatch the request to another handler or chain. OpenIG also provides the convenience class, GenericHeapObject, to help with configuration. Implementing a Customized Sample Filter The SampleFilter class implements the Filter interface to set a header in the incoming request and in the outgoing response. The following sample filter adds an arbitrary header: package org.forgerock.openig.doc; import static org.forgerock.openig.util.JsonValues.evaluated; import org.forgerock.http.Filter; import org.forgerock.http.Handler; import org.forgerock.http.protocol.Request; import org.forgerock.http.protocol.Response; import org.forgerock.openig.heap.GenericHeapObject; import org.forgerock.openig.heap.GenericHeaplet; import org.forgerock.openig.heap.HeapException; import org.forgerock.services.context.Context; import org.forgerock.util.promise.NeverThrowsException; import org.forgerock.util.promise.Promise; import org.forgerock.util.promise.ResultHandler; /** * Filter to set a header in the incoming request and in the outgoing response. */ public class SampleFilter extends GenericHeapObject implements Filter { /** Header name. */ String name; /** Header value. */ String value; /** * Set a header in the incoming request and in the outgoing response. * A configuration example looks something like the following. * * <pre> * { * "name": "SampleFilter", * "type": "SampleFilter", * "config": { * "name": "X-Greeting", * "value": "Hello world" * } * } * </pre> * * @param context Execution context. * @param request HTTP Request. * @param next Next filter or handler in the chain. * @return A {@code Promise} representing the response to be returned to the client. */ @Override public Promise<Response, NeverThrowsException> filter(final Context context, final Request request, final Handler next) { // Set header in the request. request.getHeaders().put(name, value); // Pass to the next filter or handler in the chain. return next.handle(context, request) // When it has been successfully executed, execute the following callback .thenOnResult(new ResultHandler<Response>() { @Override public void handleResult(final Response response) { // Set header in the response. response.getHeaders().put(name, value); } }); } /** * Create and initialize the filter, based on the configuration. * The filter object is stored in the heap. */ public static class Heaplet extends GenericHeaplet { /** * Create the filter object in the heap, * setting the header name and value for the filter, * based on the configuration. * * @return The filter object. * @throws HeapException Failed to create the object. */ @Override public Object create() throws HeapException { SampleFilter filter = new SampleFilter(); filter.name = config.get("name").as(evaluated()).required().asString(); filter.value = config.get("value").as(evaluated()).required().asString(); return filter; } } } When you set the sample filter type in the configuration, you need to provide the fully qualified class name, as in "type": "org.forgerock.openig.doc.SampleFilter". You can however implement a class alias resolver to make it possible to use a short name instead, as in "type": "SampleFilter": package org.forgerock.openig.doc; import org.forgerock.openig.alias.ClassAliasResolver; import java.util.HashMap; import java.util.Map; /** * Allow use of short name aliases in configuration object types. * * This allows a configuration with {@code "type": "SampleFilter"} * instead of {@code "type": "org.forgerock.openig.doc.SampleFilter"}. */ public class SampleClassAliasResolver implements ClassAliasResolver { private static final Map<String, Class<?>> ALIASES = new HashMap<>(); static { ALIASES.put("SampleFilter", SampleFilter.class); } /** * Get the class for a short name alias. * * @param alias Short name alias. * @return The class, or null if the alias is not defined. */ @Override public Class<?> resolve(String alias) { return ALIASES.get(alias); } } When you add your own resolver, you must make it discoverable within your custom library. You do this by adding a services file named after the class resolver interface, where the file contains the fully qualified class name of your resolver, under META-INF/services/org.forgerock.openig.alias.ClassAliasResolver in the .jar file for your customizations. When you have more than one resolver, add one fully qualified class name per line. If you build your project using Maven, then you can add this under the src/main/resources directory. The content of the file in this example is one line: org.forgerock.openig.doc.SampleClassAliasResolver The corresponding heap object configuration then looks as follows: { "name": "SampleFilter", "type": "SampleFilter", "config": { "name": "X-Greeting", "value": "Hello world" } } Configuring the Heap Object for the Customization Objects are added to the heap and supplied with configuration artifacts at initialization time. To be integrated with the configuration, a class must have an accompanying implementation of the Heaplet interface. The easiest and most common way of exposing the heaplet is to extend the GenericHeaplet class in a nested class of the class you want to create and initialize, overriding the heaplet’s create() method. Within the create() method, you can access the object’s configuration through the config field. Building the Customization You can use Apache Maven to manage dependencies on OpenIG. The dependencies are found in the Central Maven repository. <dependencies> <dependency> <groupId>org.openidentityplatform.openig</groupId> <artifactId>openig-core</artifactId> <version>5.3.0</version> </dependency> </dependencies> You can then build your customizations into a .jar file and install them in your local Maven repository by using the mvn install command: $ mvn install ... [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ sample-filter --- [INFO] Building jar: .../sample-filter/target/sample-filter-1.0.0-SNAPSHOT.jar [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.478s [INFO] Finished at: Fri Nov 07 16:57:18 CET 2014 [INFO] Final Memory: 18M/309M [INFO] ------------------------------------------------------------------------ Embedding the Customization in OpenIG After building your customizations into a .jar file, you can include them in the OpenIG .war file for deployment. You do this by unpacking OpenIG-4.5.0.war, including your .jar library in WEB-INF/lib, and then creating a new .war file. For example, if your .jar file is in a project named sample-filter, and the development version is 1.0.0-SNAPSHOT, you might include the file as in the following example: $ mkdir root && cd root $ jar -xf ~/Downloads/OpenIG-5.3.0.war $ cp ~/Documents/sample-filter/target/sample-filter-1.0.0-SNAPSHOT.jar WEB-INF/lib $ jar -cf ../custom.war * In this example, the resulting custom.war contains the custom sample filter. You can deploy the custom .war file as you would deploy OpenIG-5.3.0.war. Configuration Templates Auditing and Monitoring OpenIG