How to Use ForgeRock Identity Gateway to Extend User Self-Service

ForgeRock Identity Gateway (IG) is something of a Swiss-Army knife when it comes to manipulating HTTP traffic, and that even includes ForgeRock User-Self Service APIs. Learn how to peer within the encrypted content of the self-service state tokens, so you can use these services in new and creative ways.

There are two ways to implement user self-service in the ForgeRock Identity Platform, as of the 6.5 release: using ForgeRock Access Management's user self-service or using ForgeRock Identity Management's self-service. If ForgeRock Identity Management (IDM) is available, generally it's recommended that it be used for this purpose. IDM has a few more features out of the box than ForgeRock Access Management (AM) does, and IDM supports the development of custom self-service logic (called "stages") more easily than AM does. See Adding a Custom Stage to a User Self-Service Process for details on how you could do that.

You may find yourself facing a situation where adding custom stages to IDM isn't a viable option for you. Maybe you are not currently using IDM and do not want to install it just for this purpose. Maybe there is something about the execution environment of a custom stage in IDM that does not suit your needs. In any case, there is another option available using IG.

Identity Gateway is something of a Swiss Army knife when it comes to manipulating HTTP traffic. Using IG, you can modify the requests and responses as much as you need. It is designed to pull apart and chain together any piece of the request, offering a lot of power and flexibility when the default server behavior isn't quite what you need.

This power and flexibility over transforming HTTP behavior can also be applied to ForgeRock User-Self Service APIs. Regardless of the implementation (either in AM or IDM), IG can help you augment the behavior to fill in the gaps however you need them to be filled. Fortunately, both AM and IDM share a common core implementation for self-service, and as such the same IG techniques can work with either choice.

The key challenge with transforming User Self-Service requests using IG is with how data is passed around from one request to the next. Both AM and IDM represent state between self-service requests using a client-side token, called a JSON Web Token or JWT. This JWT is encrypted, which is critical to the security of the self-service process. The fact that it is encrypted makes it more difficult than normal for IG to do anything with it, however. The solution to this problem is to use the techniques below, so that you can dig into the content of those JWTs and do interesting things with them.

In order to do anything with IG, you have to position it to intercept the network requests to the backend service, in the standard way for any reverse proxy. Let's assume you've done that, and that you are using AM as the example backend for the user registration self-service endpoints. A rest call to these AM endpoints would look something like this:

curl -X POST \
--header "Accept-API-Version: resource=1.0, protocol=1.0" \
--header "Content-Type: application/json" \
--data \
'{
    "input": {
       "user": {
         "username": "DEMO",
         "givenName": "Demo User",
         "sn": "User",
         "mail":"demo@example.com",
         "userPassword": "forgerock",
         "inetUserStatus": "Active"
       }
    }
}' \
https://default.iam.example.com/am/json/realms/root/selfservice/userRegistration?_action=submitRequirements

The response to this call normally looks something like this:

{
    "type": "emailValidation",
    "tag": "validateCode",
    "requirements": {
        "$schema": "http://json-schema.org/draft-04/schema#",
        "description": "Verify emailed code",
        "type": "object",
        "required": [
            "code"
        ],
        "properties": {
            "code": {
                "description": "Enter code emailed",
                "type": "string"
            }
        }
    },
    "token": "eyJ0eXAiOiJKV1QiLCJjdHkiOiJKV1QiLCJhbGciOiJIU.....CmYo8"
}

Where "token" is the encrypted JWT. In this example it's been shortened.

An IG route configuration that can intercept this request and response (and pass it through a script) could look something like this:

{
    "name": "am",
    "baseURI": "http://am:80",
    "condition": "${matches(request.uri.path, '^/am')}",
    "handler": {
        "type": "Chain",
        "config": {
            "filters": [
                {
                  "type": "ConditionalFilter",
                  "config": {
                    "condition": "${matches(request.uri.path, '^/am/json/realms/root/selfservice/userRegistration')}",
                    "delegate": {
                        "type": "ScriptableFilter",
                        "config": {
                            "type": "application/x-groovy",
                            "file": "decryptToken.groovy",
                            "args": {
                                "keystore_location": "/usr/local/tomcat/security/keystore.jceks",
                                "storepass": "mystorepass",
                                "keypass": "changeit",
                                "signing_alias": "selfservicesigntest",
                                "encryption_alias": "selfserviceenctest"
                            }
                        }
                    }
                  }
                }
            ],
            "handler": "ClientHandler"
        }
    }
}

This particular route refers to a script called "decryptToken.groovy", passing in various arguments. As the name implies, the purpose of this script is to decrypt the JWT token that it intercepts from the above user-registration request. What it does with the content of the request is up to you to decide; this example code merely shows you how to get it. The code for decryptToken.groovy looks like this:

import javax.crypto.SecretKey;
import java.io.File;
import java.security.KeyStore;
import java.security.KeyPair;

import org.forgerock.security.keystore.KeyStoreBuilder;
import org.forgerock.security.keystore.KeyStoreType;
import org.forgerock.util.Strings;

import org.forgerock.json.jose.jwe.EncryptionMethod;
import org.forgerock.json.jose.jwe.JweAlgorithm;
import org.forgerock.json.jose.jws.JwsAlgorithm;
import org.forgerock.json.jose.jws.SigningManager;
import org.forgerock.json.jose.jws.handlers.SigningHandler;

import org.forgerock.json.jose.tokenhandler.JwtTokenHandler;
import org.forgerock.json.JsonValue;

return http.send(request).thenAsync( new AsyncFunction() {
    Promise apply (response) {
        if (response.getStatus() == Status.OK) {
            def responseObj = response.entity.json
            if (responseObj.token) {

                KeyStore store = new KeyStoreBuilder()
                    .withKeyStoreFile(keystore_location)
                    .withKeyStoreType(Strings.asEnum("JCEKS", KeyStoreType.class))
                    .withPassword(storepass)
                    .build();

                SecretKey secretKey = (SecretKey) store.getKey(signing_alias, keypass.toCharArray());
                SigningManager signingManager = new SigningManager();
                SigningHandler signingHandler = signingManager.newHmacSigningHandler(secretKey);

                JwtTokenHandler handler = new JwtTokenHandler(
                    JweAlgorithm.RSAES_PKCS1_V1_5,
                    EncryptionMethod.A128CBC_HS256,
                    new KeyPair(
                        store.getCertificate(encryption_alias).getPublicKey(),
                        store.getEntry(encryption_alias, new KeyStore.PasswordProtection(keypass.toCharArray())).getPrivateKey()
                    ),
                    JwsAlgorithm.HS256,
                    signingHandler);

                attributes.token_state = handler.validateAndExtractState(responseObj.token);

                //println "Content of token:" + attributes.token_state.toString()
                /*
                if (attributes.token_state.get("processState").isDefined("code")) {
                    println "Found code: " + attributes.token_state.get("processState").get("code").asString();
                } else {
                    println "No code found in token"
                }
                */
            }
        }

        return Response.newResponsePromise(response);
    }
});

Disclaimer: This example code is provided as-is, with no implied support by ForgeRock or anyone else. There are no restrictions on its use; however, any problems with it will be yours to solve, just as if you wrote it yourself.

This script will make the request to the back-end, and look for an OK (or 200) response. If found, it looks for a "token" value in the response body. If found, it will try to use the "args" values passed into the script to decrypt the token: load the keystore with the provided path and store password, extract the signing key and encryption keys, and then use those to exact the content of the token into a raw map. The content of the token will be stored in the "attributes" scope (here called "token_state"); this makes it easy to access with subsequent scripts, for you to refer to when building your custom behavior.

It should go without saying that in order to do this, IG has to have access to the same java keystore that the back-end is using to encrypt the JWTs, as well as the storepass and keypass values needed to work with it.

You can find details for all of the various Java classes used by this script in the API Docs provided by IG.

Hopefully, this example helps you implement whatever extra functionality you want to add to ForgeRock User-Self Service!