Protecting IDM with IG

Instructions for configuring IG to protect IDM. Updated for the ForgeRock 6.5 platform.

Introduction

IDM includes a rich set of REST APIs capable of performing many actions. Given the broadness of this set, you might want to limit the REST APIs available to public networks. To do this, use an API gateway to act as a reverse proxy, so only the requests the gateway is configured to allow are sent to IDM.

API gateways offer many powerful options for authentication and authorization that are not directly available within IDM. One important example of this is when OAuth 2.0 clients request IDM REST APIs using an access token obtained from AM. Using a gateway, IDM endpoints can be treated as standard OAuth 2.0 resource server endpoints. The gateway can validate the access token and introspect it to ensure there are appropriate scopes for the request. The ability to rely on an access token as the means by which clients work with the IDM REST API offers greater security and interoperability than IDM 6.5 can offer by itself.

In this article, we show you how to configure ForgeRock Identity Gateway (IG) to accomplish these goals. You can use this configuration as a pattern for deploying IDM functionality in a secure and standards-based way, so it is easily integrated with your OAuth 2 client applications. Some basic knowledge about installation and typical use for each product is assumed. Review the product documentation if you are unfamiliar with either product.

Securing the connection between IDM and IG

When IG is responsible for securing the requests to IDM, we need to configure IDM to only accept connections that originate from IG. One way to do this is to configure your network topology and firewall rules to prevent anything besides IG from connecting. Because this is specific to your environment, you will need to consult with your network administrator.

You also need to establish a trust relationship between the IG server and IDM - IG should be the only “client” capable of directly interacting with IDM. Essentially, IG needs to authenticate itself to IDM. There are multiple ways to do this; for example, you could use a client certificate in IG that is trusted by IDM. You could also use IG-specific username and password credentials. Using a client certificate (mTLS) is probably better from a management perspective, but it is more complicated to initially deploy. Regardless of the choice of authentication, the key is that IG is the only client capable of connecting to IDM, both from a network and an authentication perspective. This combined pattern provides the most robust security.

Static users

IG is not a user in the sense that IDM normally expects. It is more of a service, and as such, will probably not have a corresponding record available on the IDM router as users normally do. Because authentication in IDM requires a route for every authenticated subject, we need a way to supply one for IG. One simple way to do this is to create a custom endpoint that provides a pseudo-route.

For example, you can create a file named conf/endpoint-staticuser.json with this content. This handy endpoint works with the authentication service by reflecting back the ID provided, either as a query parameter or from the security context. Using this, we can define authentication modules for “users” that don’t have any other router entry, as is the case with IG:

{
    "type" : "text/javascript",
    "context" : "endpoint/static/user/*",
    "source" : "request.method === 'query' ? ([{_id: request.additionalParameters._id}]) : ({_id: context.security.authenticationId})"
}

Configuring authentication in IDM

The next step is to configure the authentication module IDM will use for identifying IG.

Client Certificate Authentication

If you want to use mTLS, you can use the CLIENT_CERT authentication module and follow a similar process to the one described in Configuring Client Certification Authentication. Example openssl commands and background detail can be found there. This module requires that the client (in this case, IG) has a particular SSL certificate it presents to IDM when it connects using HTTPS. Here is the most basic example JSON configuration using our static/user endpoint:

{
    "name": "CLIENT_CERT",
    "properties": {
       "queryOnResource": "endpoint/static/user",
        "defaultUserRoles": [
           "openidm-cert"
        ],
       "allowedAuthenticationIdPatterns": [
            "CN=ig, O=forgerock"
]
"enabled": true
}

This declaration authenticates any request that has supplied a client certificate that meets two conditions:

  1. It is trusted in terms of SSL, either directly imported in the IDM truststore or signed by a CA, which is within the IDM trust store.
  2. It has a “subject” which matches one of the patterns listed under allowedAuthenticationIdPatterns. In this example, the subject needs to match CN=ig, O=forgerock exactly.

If both of those conditions are met, then the request will be authenticated, and it will be recognized as an endpoint/static/user type of entity with the openidm-cert role. The intent of this configuration is that requests from IG are recognized within this context.

Basic HTTP Auth

A simpler form of authentication can be done using basic HTTP authentication. In this case, the password will have to be shared between the IG instance and the IDM instance. The configuration for the authentication module in IDM looks like this:

{
    "name" : "STATIC_USER",
    "properties" : {
        "queryOnResource" : "endpoint/static/user",
        "username" : "rsFilterClient",
        "password" : "myPassword",
        "defaultUserRoles" : [
            "internal/role/openidm-admin"
        ]
    },
    "enabled" : true
}

In this case, the particular “username” and “password” values entered here are expected to be supplied by the client (IG), either in the form of a standard basic Authorization header ( Auhorization: Basic ${base64Encoded(“rsFilterClient:myPassword”)} ) or using the IDM authentication headers (X-OpenIDM-Username: rsFilterClient and X-OpenIDM-Password: myPassword). See the “STATIC_USER” section within Supported Authentication Modules for more detail.

The RunAs header

However you choose to authenticate IG, you also need to configure IDM to allow IG to make requests on behalf of the access token subject. There is a feature available for every authentication module called RunAs Authentication. This feature lets privileged clients supply an X-OpenIDM-RunAs header to specify the name of the user they would like to operate on behalf of. This is obviously a highly sensitive type of operation, and only the most trusted clients (such as IG) should be allowed to operate under the guise of other users. As an example for how to configure this, you can expand the CLIENT_CERT authentication module configuration like so:

{
    "name": "CLIENT_CERT",
    "properties": {
        "queryOnResource": "endpoint/static/user",
        "defaultUserRoles": [
            "openidm-cert"
        ],
        "allowedAuthenticationIdPatterns": [
            "CN=ig, O=forgerock"
        ],
        "runAsProperties": {
            "adminRoles": [
                "openidm-cert"
            ],
            "disallowedRunAsRoles": [ ],
            "queryOnResource": "managed/user",
            "propertyMapping": {
                "authenticationId" : "userName",
                "userRoles": "authzRoles"
            },
            "defaultUserRoles" : [
                "openidm-authorized"
            ]
        }
    },
    "enabled": true
}

And example STATIC_USER configuration would look something like this:

{
    "name" : "STATIC_USER",
    "properties" : {
        "queryOnResource" : "endpoint/static/user",
        "username" : "rsFilterClient",
        "password" : "myPassword",
        "defaultUserRoles" : [
            "internal/role/openidm-admin"
        ],
        "runAsProperties": {
            "adminRoles": [
                "internal/role/openidm-admin"
            ],
            "disallowedRunAsRoles": [ ],
            "queryOnResource": "managed/user",
            "propertyMapping": {
                "authenticationId" : "userName",
                "userRoles": "authzRoles"
            },
            "defaultUserRoles" : [ ]
        }
    },
    "enabled" : true
}

Notice that both examples use the same “runAsProperties” definition. Regardess of the particular auth module used, this additional “runAsProperties” configuration allows any authenticated request from IG to include an X-OpenIDM-RunAs header that identifies the userName value for the associated managed/user record. IG will be the only client capable of making this sort of request, because it is the only entity that can authenticate using the particular auth module.

Configuring IG to make connections to IDM

Now that IDM is prepared to accept connections, IG needs to be configured to make them correctly.

Trusting IDM

If you are using CLIENT_CERT, you will need to review the chapter in the IG configuration guide called “Configuring IG for HTTPS, (client-side)". If IDM is using a self-signed certificate, you will need to import that into IG’s truststore as described there.

Supplying a client certificate

All requests made by IG to a backend are via a ClientHandler. By default, there is no special behavior associated with a ClientHandler; it simply acts as a generic HTTP/S client. The best option is to define a new ClientHandler on the IG heap that is configured to perform client certificate authentication. The most important declaration for this use is the keyManager; this tells the ClientHandler where to get the keys to use when prompted for client certificates. Here is an example of a configured client that is ready to trust IDM’s certificate and supply its own:

{
    "name": "IDMClient",
    "type": "ClientHandler",
    "config": {
        "sslContextAlgorithm": "TLSv1.2",
        "keyManager": {
            "type": "KeyManager",
            "config": {
                "keystore": {
                    "type": "KeyStore",
                    "config": {
                        "url": "file:///var/openig/keystore.jks",
                        "password": "changeit"
                    }
                },
                "password": "changeit"
            }
        },
        "trustManager": {
            "type": "TrustManager",
            "config": {
                "keystore": {
                    "type": "KeyStore",
                    "config": {
                        "url": "file:///var/openig/keystore.jks",
                        "password": "changeit"
                    }
                }
            }
        }
    }
}


In this example, the /var/openig/keystore.jks file contains the public certificate needed to trust IDM and the private key needed to authenticate IG as a client. Make sure all IG routes that forward requests to IDM use this ClientHandler.

Supplying Basic Auth Headers

The configuration is easier for basic auth. In this case, your ClientHandler simply needs to include the particular request headers that identify IG. For example:

{
    "name": "IDMClient",
    "type": "Chain",
    "config": {
        "filters": [
            {
                "type": "HeaderFilter",
                "config": {
                    "messageType": "REQUEST",
                    "add": {
                        "X-OpenIDM-Username": ["rsFilterClient"],
                        "X-OpenIDM-Password": ["myPassword"]
                    }
                }
            }
        ],
        "handler": "ClientHandler"
    }
}

Including the RunAs Header

Before the request is forwarded to IDM via the IDMClient, it needs to be modified to include the X-OpenIDM-RunAs header. This lets IDM find the user making the request via IG. There are several ways to modify the request before it is sent to IDM, and all of them involve a filter on the route. The simplest example is to use the HeaderFilter, like so:

{
     "type": "HeaderFilter",
     "config": {
         "messageType": "REQUEST",
         "add": {
             "X-OpenIDM-RunAs": [ "${contexts.oauth2.accessToken.info.sub}" ]
         }
     }
}

If you have complex logic around your user behavior, you might want to use a ScriptableFilter, within which you could set the header with code like so:

String sub = contexts.oauth2.accessToken.info.sub
request.getHeaders().add('X-OpenIDM-RunAs', sub)
return next.handle(context, request)

Note that both of these examples set the RunAs header to a dynmic value; this value comes from the access token introspection request, as described in the next section.

Operating as an OAuth 2.0 resource server

While there are many potential benefits provided by IG, the main one explored here is to operate as an OAuth 2.0 resource server. The core feature is well-documented; see the IG Gateway Guide and the IG Configuration Reference. The main detail to consider in this specific context is how you want to manage scopes. In order for requests to successfully pass through the OAuth2ResourceServerFilter, the token must have the correct scopes associated with it. You decide what constitutes a correct scope value for any given request. You need to consider which IDM endpoints and methods you want to make available and how to express that availability in terms of a scope. For example, if you want to allow OAuth2 clients to be able to make calls to the /openidm/endpoint/usernotifications endpoint, you could define a scope called “notifications”, and require it with a route like so:

{
    "name": "notificationsRoute",
    "baseURI": "https://idm.example.com:8444",
    "condition": "${matches(request.uri.path, '^/openidm/endpoint/usernotifications')}",
    "handler": {
        "type": "Chain",
        "config": {
            "filters": [
                {
                    "type": "OAuth2ResourceServerFilter",
                    "config": {
                        "scopes": [
                            "notifications"
                        ],
                        "accessTokenResolver": "AccessTokenResolver"
                    }
                },
            ],
            "handler": "IDMClient"
        }
    }
}

Scope design can be a complicated subject. It is up to you to decide what level of consent you want your users to be able to grant to the clients you will be supporting. That will determine what scopes you define and how they map to various REST requests.

Complete configuration example

There is a full sample configuration available (forgeops/config/6.5/oauth2 at release/6.5-kustomize · ForgeRock/forgeops · GitHub) as part of the ForgeOps project which follows these patterns. This sample brings up AM configured to operate as an OAuth2 AS, providing token creation and introspection. It configures IG to act as an OAuth2 RS, and configures IDM to accept connections exclusively from IG (using the STATIC_USER auth option). Review ig/config and idm subfolders included within that sample:

{
    "name": "AccessTokenResolver",
    "type": "TokenIntrospectionAccessTokenResolver",
    "config": {
        "endpoint": "http://am:80/am/oauth2/introspect",
        "providerHandler": {
            "type": "Chain",
            "config": {
                "filters": [
                    {
                        "type": "HeaderFilter",
                        "config": {
                            "messageType": "request",
                            "add": {
                                "Authorization": [
                                    "Basic ${encodeBase64('igClient:myPassword')}"
                                ]
                            }
                        }
                    }
                ]
            }
        }
    }
},
{
    "name": "RSClient",
    "type": "Chain",
    "config": {
        "filters": [
            {
                "type": "HeaderFilter",
                "config": {
                    "messageType": "REQUEST",
                    "add": {
                        "X-Requested-With": ["IG"],
                        "X-OpenIDM-NoSession": ["true"],
                        "X-OpenIDM-Username": ["rsFilterClient"],
                        "X-OpenIDM-Password": ["myPassword}"],
                        "X-OpenIDM-RunAs": ["${contexts.oauth2.accessToken.info.sub}"]
                    }
                }
            }
        ]
    }
}

The details about how to validate the access token are shown here via the AccessTokenResolver heap object. There are many legitimate configuration options worth exploring for the OAuth2ResourceServerFilter filter. See the Reference documentation for the full range.

In this sample, it calls to a standard OAuth 2.0 token introspection endpoint provided by AM, authenticated with a registered client called rsFilterClient. See the documentation for information on configuring AM to operate as a token introspection service. Be sure that the client you register for the IG resource server filter has the am-introspect-all-tokens scope included within it.

The best practice for IG is to keep the common parts of each route (such as the RSClient and AccessTokenResolver heap objects) in the global IG config.json file, rather than repeating them in each route. This sample follows that same pattern. You can see the shared “heap” objects declared within config.json.

This sample takes a very simplistic approach to scope usage. There is just one catch-all route for all of IDM. Here’s how it is defined:

{
    "name": "openid",
    "baseURI": "http://idm:80",
    "condition": "${matches(request.uri.path, '^/openidm')}",
    "handler": {
        "type": "Chain",
        "config": {
            "filters": [
                {
                    "type": "ConditionEnforcementFilter",
                    "config": {
                        "condition": "${not empty request.headers['authorization'] and indexOf(request.headers['authorization'][0], 'Bearer ') == 0}",
                        "failureHandler": "NonRSClient"
                    }
                },
                {
                    "type": "OAuth2ResourceServerFilter",
                    "config": {
                        "scopes": [
                            "openid"
                        ],
                        "requireHttps": false,
                        "accessTokenResolver": "AccessTokenResolver",
                        "cacheExpiration": "2 minutes"
                    }
                }
            ],
            "handler": "RSClient"
        }
    }
}

This example also includes a ConditionEnforcementFilter; this allows IG to selectively enforce the OAuth2ResourceServerFilter. Requests which do not have an Authorization: Bearer header will revert to other authentication options. See the IG documentation for more details about route definitions.

Running this sample

Follow the Platform DevOps Guides for detailed steps on running this sample. The main details that are specific to running this sample, however, are included here for clarity:

  1. Initialize your docker environment with the sample configuration: bin/config.sh init -v 6.5 -p oauth2
  2. Deploy the kubernetes manifests to your cluster: skaffold run -f skaffold-6.5.yaml -p oauth2

Hopefully this helps make it clear how you can build your secure, standards-based applications, integrated with the ForgeRock Identity Platform!