Creating Custom Compiled Java Filters For Ping Gateway

Originally posted on keithdaly-identity.medium.com

Introduction

When implementing functionality in Ping Gateway, you will create routes that will direct system calls to back-end systems (or handle the calls itself).

Routes will be configured to contain handlers, which are either a chain containing filter objects, a component that forwards a request onto another system, or a component that handles the request itself.

Filters are components that take some sort of action on the requests and pass its results up and down to other filters and handlers.

When at all possible, use the Out-Of-The-Box components that are shipped with the product. When these components do not meet your needs, you can implement additional functionality with scripts. For certain cases, such as integrating other jar files that do not implement the filter interface themselves, you may be required to use compiled jar files.

While experienced developers should be able to pick up the standard documentation and adapt it to their specific needs, this post is intended to provide a complete walk-through of the process, filling in some missing pieces and accelerating the learning process for all.

Pre-Requisites

The following components are required for following along with this blog post.

  • Ping Gateway
    (previously named ForgeRock Identity Gateway and openig)
    I am using the 2024.9.0 version. You will need to change your jars and references in your pom file if you use another version.

  • Java JDK
    I am using openjdk version 17.0.13 (2024–10–15). The version number need to be adjusted in your pom file, as indicated below.

  • Maven
    I am using version 3.9.9. It should be safe to use the latest version (“brew install maven” on a mac).

  • Visual Studio Code
    I am using 1.95.2. The latest version available should be fine.

  • Ping Advanced Identity Cloud Tenant
    This is not required for following along with this blog post, but the next step after this “hello world” activity will likely be integrating with AIC.

Create Maven Project in Visual Studio Code

In Visual Studio Code when a new window is shown, click on the explorer icon or press Shift-Command-E on a Mac.


 

There is no Gateway Filter archetype available, so we will start by creating our projetc using a basic maven archetype.

  • Create Java Project → Maven archetype → maven-archetype-quickstart

  • Select 1.4

  • Enter your group id (org.example is the default)

  • Enter an artifact id (demo is default)

  • Select a disk location.

  • In the console, you will need to specify a version flag (1.0-SNAPSHOT is default) and confirm everything.

  • Choose to open the new project.

The project is now created. The first step is to adjust the compiler java version in the pom.xml file. In my case, I am using openjdk 17.0.13 2024–10–15, so I have changed from 1.7 to 17.

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <maven.compiler.source>17</maven.compiler.source>
  <maven.compiler.target>17</maven.compiler.target>
</properties>

Once this is done, you should be able to compile the project. From a command line, go to your project directory and compile using maven:

mvn clean install

At this point, you now have a functional generic Maven project. The next step is to add the Gateway dependencies.

Add Gateway Library Files

There is no maven archetype or repository, so we will need to use the local filesystem to host the required libraries.

Start by creating a /lib folder in your project at the same level as your existing /src folder

Copy the following files from the lib directory of your downloaded software (identity-gateway-2024.9.0/lib) to the /lib directory of your project:

chf-http-core-27.0.0–20240917002912–4c4891bf2fe1d5f4f517e628c4e5133a33bbf570.jar
forgerock-util-27.0.0–20240917002912–4c4891bf2fe1d5f4f517e628c4e5133a33bbf570.jar
openig-core-2024.9.0.jar
openig-model-2024.9.0.jar

Once that is completed, modify your pom file by adding these dependencies:

<dependencies>
    <dependency>
      <groupId>org.forgerock</groupId>
      <artifactId>http</artifactId>
      <version>2024.9.0</version>
      <scope>system</scope>
      <systemPath>${project.basedir}/lib/chf-http-core-27.0.0-20240917002912-4c4891bf2fe1d5f4f517e628c4e5133a33bbf570.jar</systemPath>
    </dependency>
    <dependency>
      <groupId>org.forgerock.openig</groupId>
      <artifactId>heap</artifactId>
      <version>2024.9.0</version>
      <scope>system</scope>
      <systemPath>${project.basedir}/lib/openig-core-2024.9.0.jar</systemPath>
    </dependency>
    <dependency>
      <groupId>org.forgerock.openig</groupId>
      <artifactId>model</artifactId>
      <version>2024.9.0</version>
      <scope>system</scope>
      <systemPath>${project.basedir}/lib/openig-model-2024.9.0.jar</systemPath>
    </dependency>
    <dependency>
      <groupId>org.forgerock.util</groupId>
      <artifactId>promise</artifactId>
      <version>2024.9.0</version>
      <scope>system</scope>
      <systemPath>${project.basedir}/lib/forgerock-util-27.0.0-20240917002912-4c4891bf2fe1d5f4f517e628c4e5133a33bbf570.jar</systemPath>
    </dependency>
    ...
  </dependencies>

Compile your project again to ensure that the dependencies have been included correctly (mvn clean install).

Create Filter Class

Now we are finally ready to implement the custom filter. In your /src directory, create a new class.

Copy the code from the official documentation or from the box below. (If you are using other package and/or class names, make the necessary adjustments…)

https://backstage.forgerock.com/docs/ig/2024.9/configure/extending.html#custom-sample-filter

package org.forgerock.openig.doc.examples;

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.GenericHeaplet;
import org.forgerock.openig.heap.HeapException;
import org.forgerock.openig.model.type.service.NoTypeInfo;
import org.forgerock.services.context.Context;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;

public class SampleFilter implements Filter {

    String name;

    String value;

    @Override
    public Promise<Response, NeverThrowsException> filter(final Context context,
                                                          final Request request,
                                                          final Handler next) {

        request.getHeaders().put(name, value);

        return next.handle(context, request)
                   .thenOnResult(response -> {
                       response.getHeaders().put(name, value);
                   });
    }

    @NoTypeInfo(reason = "documentation sample")
    public static class Heaplet extends GenericHeaplet {

        @Override
        public Object create() throws HeapException {

            SampleFilter filter = new SampleFilter();
            filter.name  = config.get("name").as(evaluatedWithHeapProperties()).required().asString();
            filter.value = config.get("value").as(evaluatedWithHeapProperties()).required().asString();

            return filter;
        }
    }
}

Note: This is a copy of the code (minus comments) located in the product documentation. Imports should all work correctly now that the jars have been included in the pom file.

The resulting filter object will insert a header value according to the filter configuration.

Copy the Compiled Jar to Gateway

Once the jar has been compiled (mvn clean install), it needs to be copied into your .openig/extra directory. You will need to create this folder if it does not already exist.

After copying the file, you will need to restart IG.

Create the IG Route

At this point, you should have a functional custom filter. To use it, we will need to add it to a route.

Normally, routes can be defined in Studio by dragging widgets onto the workspace and then supplying the required configuration. There is obviously no dedicated widget in the palette for the new filter. This would normally mean that we would use the “other filter type” and supply the complete filter configuration JSON to the filter. However, for custom compiled filters, there is no placeholder object and this must be defined in the JSON for the route.

Define your filter on the heap. Note that name and value are config values that are passed into the filter, which are useed to costruct a header:

{
  "name": "SampleFilter",
  "type": "org.forgerock.openig.examples.SampleFilter",
  "config": {
    "name": "X-Greeting",
    "value": "Hello world"
  }
}

Then, modify your chain to include the new filter:

{
  "name": "Chain-1",
  "type": "Chain",
  "config": {
    "handler": "ReverseProxyHandler",
    "filters": [
      "CrossDomainSingleSignOnFilter-1",
      "SampleFilter"
    ]
  }
}

TIP: When I need to do this, I usually define the full route in Studio, then select the display option from the “hamburger”. This will show the route in JSON. From here, I can simply add my additional configuration and place the resulting file into .openig/config/routes.

Note: After files are place in openig/config/routes, you may have to restart IG for the routes to be loaded correctly.

Test the Route

Test the route by pointing your browser to wherever your route is defined. In my case, I followed the CDSSO tutorial and placed my filter in that route (https://ig.example.com:8081/home/cdsso). After authentication (if required in your route), you should be able see my new header values echoed back. Huzzah!