Understanding and Troubleshooting ForgeRock Identity Platform Integration

In the 7.0 release of the ForgeRock® Identity Platform, there are new ways to integrate some of the products at an API level. These integration patterns follow the OAuth 2.0 set of standards. In particular, IDM REST APIs are now callable using the standard HTTP bearer token header for resource servers. Accordingly, all applications that call IDM REST endpoints must operate as an OAuth 2.0 client.

The “platform-enduser”, “platform-admin”, and IDM admin UIs are all example applications which are designed to operate as OAuth 2.0 clients. So are the new “platform self-service” authentication nodes within AM. All client applications are expected to obtain an access token from AM, and present it to IDM. With this high-level pattern in mind, getting all of the components working as an integrated platform requires careful configuration of each one.

This article aims to help integrators understand some key configurations and call out common problems you might encounter as you customize them. This is not meant to be step-by-step instructions for getting the platform running. To do this, refer to the excellent product documentation available for running the platform using ForgeOps or by manually setting up it following the instructions in the ForgeRock Identity Platform Setup Guide.

Platform-based single page applications

Single page applications are static collections of HTML, JavaScript, CSS and other browser files. From a server perspective, they are all simple static resources. Serving them is a relatively trivial task; any HTTP server can do the job. A browser will load them all and use them to interact with various backend services.

For the ForgeRock Platform 7.0, there are two main types of single page apps: non-OAuth 2 applications and OAuth 2 clients. When it comes to platform integration, getting these different SPAs working properly is far-and-away the primary challenge.

Non-OAuth 2 applications

A “non-OAuth 2 application” is one which does not involve obtaining or using access tokens; instead they use cookies for authentication. There are several non-OAuth 2 single page applications shipped with 7.0. They are:

  • platform-login: a new-to-7 login interface specifically designed to support the new callbacks introduced for self-service. It replaces the “legacy” AM login UI still being shipped with AM 7.0 under /XUI.
  • legacy XUI: the above-mentioned AM login UI that is essentially unchanged from 6.5. DO NOT USE IN A PLATFORM DEPLOYMENT
  • ui-admin: AM’s “Native Console”; used for various configuration and administrative tasks in AM.
  • IDM stand-alone end-user UI: A completely different implementation of IDM end-user UI capabilities (as compared to platform-enduser) that is only usable when IDM is deployed without AM. DO NOT USE IN A PLATFORM DEPLOYMENT

IMPORTANT NOTE: Do NOT use the IDM stand-alone end-user UI if you are using the full platform. “Out of the box” with IDM 7 you will see it deployed under ui/enduser. This is NOT the platform-enduser UI. It will NOT work with the other configuration described here.

The most important one for a full platform deployment is the platform-login UI package. If you plan to use the new self-service tree capabilities of the 7.0 platform, you will need to use this login UI instead of the legacy XUI login application. See details within “Customizing the user-facing interfaces” for more on how to replace the legacy XUI. See details within “Platform self-service trees” for more on the use of platform-login with self-service trees.

Note: The platform-login UI uses the ForgeRock JavaScript SDK to help render the various login callbacks. You may find the platform-login source code to be a useful reference if you plan to build your own login UI with the SDK.

SPA OAuth 2 clients

There are three “single page app” clients that ForgeRock provides as part of the 7.0 release. A general overview for how all three clients operate is provided in my earlier article, Building an SSO Client for Your REST APIs with OIDC. The specific OAuth 2 client SPAs shipped with 7.0 are:

  • platform-enduser: example UI demonstrating profile maintenance, consent management, workflow and delegated administration capabilities. Makes REST calls to IDM (using an OAuth 2 bearer token) and AM (using an SSO cookie).
  • platform-admin: new-to-7.0 administrative application designed to handle various tasks that span AM and IDM. Only available as a docker image. Use the latest tag, available here: gcr.io/forgerock-io/platform-admin-ui:7.1.0-postcommit-latest
  • IDM Admin UI: The IDM “Native Console”; used for various configuration and administrative tasks in IDM.

OAuth 2 is designed to support clients which operate in different domains from the authorization server. This means that all three clients can be hosted under any domain name you prefer. Due to the fact that they are just static files, you can also use any HTTP server.

The only one of these three meant to be customized is the platform-enduser. See details within “Customizing the user-facing interfaces” on how to do so.

Customizing the user-facing interfaces

There are two platform-oriented single page applications available for 7.0 which are meant to be built uniquely for each deployment. These are the “platform-login” and “platform-enduser” applications.

There is also a “platform-shared” package which contains components and code that is used by both of them. All three packages are built with the Vue.js framework.

Every deployment will involve first making a clone of the platform-ui source code. It is recommended that you track a particular release branch of the git repository, if possible. From your clone you can make all desired changes to the UI packages.

Follow the instructions in the README.md to build each package.

Before you run yarn build for any package, be sure to adjust the variables found within .env.production. The values you provide will be compiled into the final product (found within dist); it is critical that the values you provide match your operating environment.

Replace the Legacy XUI with Platform-Login

In order to start using platform-login instead of the legacy XUI, there are two steps you’ll need to take:

  1. Get a build of platform-login:
  2. Clone the git reposistory.
  3. Carefully review the README.
  4. Make your customizations (theming / behavioral / etc…).
  5. Adjust the variables found within .env.production, in particular VUE_APP_AM_URL and VUE_APP_AM_ADMIN_URL.
  6. Build using yarn build; final product will be available within dist/.
  7. Direct authentication requests to the platform-login build files using one of these options:
  • The simplest way to do this is to copy dist/* into the expanded webapps/am/XUI folder, over the top of the legacy XUI files.
  • If you have a reverse proxy in front of AM, another option is to run a separate HTTP server to host the dist files. You can use the reverse proxy to intercept requests to XUI/* and direct them to these files instead of the legacy XUI files. If you choose to go down this route, it is critical that you make available the legacy XUI files under a different route (a few of the JavaScript files from the legacy XUI are still needed in order to properly render various OAuth 2-related UI screens). Within the expanded AM webapps folder, mv XUI OAuth2_XUI. In order for AM to know where this folder is now located, specify the path as a Java property like so: -Dorg.forgerock.am.oauth2.consent.xui_path=/OAuth2_XUI. Note that this method is the one taken by ForgeOps when deploying the platform in Kubernetes.
  • If you only need to support OAuth 2 clients, you can configure the OAuth 2 Provider service to use an external login URL. With this option you can directly expose the separate HTTP server you are using to host the dist files. Note that this will only work for OAuth 2.0-based authentication grants; others (such as SAML or same-domain policy agents) will not benefit from this setting.

Tip: if you’re using IG as your reverse proxy, you can make use of the new-to-7.0 feature of IG that allows you to serve static files directly from IG; this would avoid the need to run another HTTP server. More details on this in the documentation for ResourceHandler.

Replace the IDM stand-alone end-user UI with Platform-Enduser

In order to start using platform-enduser instead of the IDM stand-alone end-user UI, there are two steps you’ll need to take:

  1. Get a build of platform-enduser.
  2. Clone the git reposistory.
  3. Carefully review the README.
  4. Make your customizations (theming / behavioral / etc…).
  5. Adjust all variables found within .env.production.
  6. Build using yarn build; final product will be available within dist/.
  7. Make the platform-enduser build files available using one of these options:
  • The simplest way to do this is to use dist/* to replace the contents of your ui/enduser folder within your IDM installation. This means it will be available within the same domain as your IDM endpoints, available under whatever folder path is specified within ui.context-enduser.json.
  • Run a separate HTTP server to host the files. Whatever host name you choose to use to return these will need to be configured as part of the redirect_uri for the OAuth 2 client registration in AM (see “AM as an OAuth 2 authorization server” for more details).

Note: Always use HTTPS for all web traffic.

Support necessary for the IDM Admin UI

Client settings

The IDM Admin UI is capable of operating as both an OAuth 2 client and as a non-OAuth 2 application, as appropriate for whichever “mode” IDM has been configured (platform or stand-alone). In order to discover which mode the Admin UI needs to use, it will issue a GET request to /openidm/config/ui/configuration when it first starts. It is looking for a configuration entry called platformSettings, which would look something like this:

{
    "configuration" : {
        "platformSettings" : {
            "adminOauthClient" : "idm-admin-ui",
            "adminOauthClientScopes" : "fr:idm:*",
            "amUrl" : "https://am.example.com/am"
        }
    }
}
Frame Options

The Admin UI will use these details to attempt a silent auth code grant within a hidden iframe (see below for more troubleshooting tips related to auth code grants). In order for this hidden iframe request to succeed, be sure you have configured ui.context-admin.json with X-Frame-Options like so:

{
    "enabled" : true,
    "urlContextRoot" : "/admin",
    "defaultDir" : "&{idm.install.dir}/ui/admin/default",
    "extensionDir" : "&{idm.install.dir}/ui/admin/extension",
    "responseHeaders" : {
        "X-Frame-Options" : "SAMEORIGIN"
    }
}

Otherwise you will see an error in your browser like this:

Refused to display 'https://idm.example.com/admin/appAuthHelperRedirect.html' in a frame because it set 'X-Frame-Options' to 'deny'.
Alternate Servlet Aliases

If you have configured the boot.properties value openidm.servlet.alias to be something other than the default (openidm), you will need to edit the Admin UI in order to account for this new path. Replace the value “openidm” in these locations:

ui/admin/default/index.html#53
ui/admin/default/index.html#95
ui/admin/default/org/forgerock/openidm/ui/common/util/Constants.js#12
Realm support

The IDM Admin UI only supports users from the AM root realm out-of-the-box. If you need to support users from other realms, you will have to adjust these /oauth2/ references found within ui/admin/default/index.html:

commonSettings.authorizationEndpoint = calculatedAMUriLink.href + '/oauth2/authorize';

AppAuthHelper.init({
    clientId: commonSettings.clientId,
    authorizationEndpoint: commonSettings.authorizationEndpoint,
    tokenEndpoint: calculatedAMUriLink.href + '/oauth2/access_token',
    revocationEndpoint: calculatedAMUriLink.href + '/oauth2/token/revoke',
    endSessionEndpoint: calculatedAMUriLink.href + '/oauth2/connect/endSession',

For example, if your realm is named “SUBREALM”, replace /oauth2/ with /oauth2/realms/root/realms/SUBREALM/.

AM as an OAuth 2 authorization server

The OAuth 2 clients need AM to provide access tokens. Carefully follow the steps described in the "Configure AM" chapter of the Setup Guide in order to get it configured properly. Here are some additional notes worth reviewing on this topic:

  • Global CORS Configuration
    • Edit Global services → CORS → Secondary Configurations → cors → Accepted Origins
    • List only those origins which you know will be hosting SPA OAuth 2 clients (such as the platform-enduser and IDM Admin UIs).

Note: if you’re seeing CORS errors in your browser, the problem might not be due to CORS; instead, it could be due to some underlying error that is preventing AM from sending the normal CORS headers. To troubleshoot CORS errors, first attempt to make the same request outside of a CORS context. For example, use your browser’s “Copy As cURL” feature to copy the problematic request and try running it using curl instead.

The top-level realm needs the idm-resource-server registration. Note that this isn’t really an OAuth 2 “client” per se; it’s more of an OAuth 2 Resource Server account, exclusively used for token introspection. As such, you don’t need things like a redirect_uri or any particular grant type. Be sure to include the special scope am-introspect-all-tokens - this is what gives this entity the right to introspect tokens issued to other clients. Also, if you expect to accept access tokens issued from sub realms, be sure to also include the scope am-introspect-all-tokens-any-realm.

For each realm in use:

  • Register the clients (idm-admin-ui, end-user-ui, and idm-provisioning if you’re using that).
    • They have to be registered identically in each realm they are expected to be used within.
    • Be sure NOT to accidentally enter a value in the “Client Secret” field. Your browser may auto-fill a value here, thinking it is a login form. If your browser does this, be sure to clear out the value before you hit “Save Changes”. Accidentally changing the client secret has been the source of many hours of frustration.
    • Under “Advanced”, be sure that “Subject Type” is set to “public” and “Token Endpoint Authentication Method” is set to “none”.
  • OAuth 2 Provider service
    • Within “Consent”, enable “Allow Clients to Skip Consent”. You may also make use of the “Saved Consent Attribute Name” option, if you want clients to be prompted with the consent page. One or the other must be used to allow transparent token renewal.
    • Within “Advanced”, be sure id_token|org.forgerock.openidconnect.IdTokenResponseTypeHandler and code|org.forgerock.oauth2.core.AuthorizationCodeResponseTypeHandler are listed as “Response Type Plugins”.
  • Identity Stores->OpenDJ->User Configuration->LDAP Users Search Attribute
    • Attribute used to specify the “sub” value in access_token, id_token and the URL used to make REST calls into AM for that user. Refer to the below section “IDM uses the token introspection response to construct a security context” to see how this value impacts integration. The default value is “uid”; whatever value you use here needs to be able to be found by IDM when it searches for a corresponding user.
  • Validation Service
    • Be sure you add a validation service with this value: ${FQDN}/*?*. For example: https://am.example.com/*?*
    • Platform Login UI needs this for goto redirection
  • Self Service Trees
    • Platform-enduser UI needs this to find important AM services
    • There is a bug in the UI for the 7.0 AM release; instead, create it with the curl command:
curl -X POST \
  https://am.example.com/am/json/realms/root/realm-config/services/selfServiceTrees?_action=create \
  -H "Cache-Control: no-cache" \
  -H "accept-api-version: protocol=1.0,resource=1.0" \
  -H "content-type: application/json" \
  -H "x-requested-with: XMLHttpRequest" \
  -H "iPlanetDirectoryPro: $SSO_TOKEN" \
  -d '{
    "treeMapping":{
        "resetPassword":"PlatformResetPassword",
        "updatePassword":"PlatformUpdatePassword",
        "forgottenUsername":"PlatformForgottenUsername",
        "registration":"PlatformRegistration",
        "login":"PlatformLogin"
    },
    "_id":"",
    "_type":{
        "_id":"selfServiceTrees",
        "name":"Self Service Trees",
        "collection":false
    }
}'

Troubleshooting OAuth 2 client failures

So, now you have deployed SPA clients and have configured AM to support them. You open them up in your browser and something goes wrong - maybe all you see is a white screen. Or maybe after you login, your browser loops continuously back and forth between the client and the login page. What to do?

For all types of client failures, the most valuable means at your disposal is your browser’s developer tools. Open the developer tools and review the calls made in the network tab. Be sure you aren’t filtering any requests - there are XHR and non-XHR calls being made. Also be sure to use the “Preserve Log” feature, so requests will be saved even as you transition from one site (the client) to another (AM).

Authorization request failures

The first place failures could occur is in the initial call to AM to try to obtain access tokens silently (in a hidden iframe). This involves making a request to the authorize endpoint. If this request fails with a 400 error, the client will fail to render anything; you’ll just see a white screen. Here’s how you can troubleshoot that issue.

Within the developer tools network traffic log, look for a non-XHR request to AM’s authorize endpoint. It should look something like this:

GET /am/oauth2/authorize?redirect_uri=https...

If the developer tools show this request in red, there is a problem. Check the status code for this request.

If the status is 400, it’s likely the client is mis-configured for the client in AM. Review the response body - there is probably some JavaScript in the HTML response that will reveal the issue. Here are some examples you might see:

pageData = {
    realm : "\/",
    baseUrl: "https://am.forgeops.com/am/OAuth2_XUI/",
    error: {
        description: "Client authentication failed",
        message: "invalid_client"
    }
}

To fix this, review the OAuth 2 clients you have registered in the realm you are trying to use. There must be one with the same client_id that you are passing in the request.

Another response you might see could be this:

pageData = {
    realm : "\/",
    baseUrl: "https://am.forgeops.com/am/OAuth2_XUI/",
    error: {
        description: "The redirection URI provided does not match a pre-registered value.",
        message: "redirect_uri_mismatch"
    }
}

To fix this, make sure the exact (decoded!) redirect_uri query string value provided in the authorization request is registered in the client in AM.

pageData = {
    realm : "\/",
    baseUrl: "https://jake.iam.forgeops.com/am/OAuth2_XUI/",
    error: {
        description: "Server does not support this client\'s subject type.",
        message: "invalid_client"
    }
}

This occurs when the subject type is set incorrectly. Be sure to set the Subject Type to public in the client registration details, under “Advanced”.

You might also not be getting stopped with a 400 failure; instead, you might find the client is stuck in an infinite redirect loop. If so, you still need to check the response to the authorize request. In this case, look for the Location response header; the error details are likely available there. For example (decoded for readability):

error_description=Unknown/invalid scope(s)&error=invalid_scope

In this case, the scope being asked for by the authorization request is not available to the client. Once again, edit your client registration to make sure it has the proper scope listed.

You might also see this in your Location header (decoded for readability):

error_description=The authenticated client is not authorized to use this authorization grant type.&error=unauthorized_client

Here you need to make sure your client has “Authorization Code” listed within “Grant Types”.

Token request failures

You have fixed the authorization request failures, so now you are successfully redirected to AM to login. You might find that after you are redirected back to your client app other failures occur. Once again, use your browser developer tools network traffic log to look for a request like this:

POST /am/oauth2/access_token

If this returns a 400 error, review these settings in your AM client configuration:

  • Make sure your Client type is “public”
  • Make sure your Token Endpoint Authentication Method is “none”

You might also see CORS-related errors in your developer tools console, which correspond with this request. If that’s the case, the issue is that the client’s origin has not been white-listed by AM. Edit Global services → CORS → Secondary Configurations → cors → Accepted Origins and add the origin for your client.

Session check failures

After the client has successfully gotten an access token, it will periodically check back with AM to make sure the original session that was used to sign in is still valid. The client does this by making an OIDC implicit grant within a hidden iframe (using prompt=none). You can find out more implementation details for how this works by reviewing the OIDC Session Check library.

To find the session check call, open the developer tools network traffic log and look for a non-XHR request to AM’s authorize endpoint; be sure the request has response_type=id_token&scope=openid&prompt=none query string parameters.

If there is an problem with the session check request, it will probably show up within the Location header in the 302 response. Here are some example errors (decoded for readability):

error_description=The authenticated client is not authorized to use this authorization grant type.&error=unauthorized_client

This error is because Implicit is not declared as a Grant Type for the client. Add it in the AM client settings, under “Advanced->Grant Types”.

It could be that you are getting unexpectedly returned to the login UI, after successfully logging in. In this case, it’s likely you would find this response to your session check call:

error_description=The request requires consent.&error=consent_required

This is because consent wasn’t saved for the user. Saved consent is required for silent OAuth 2 grant usage. Configure this in AM by opening Realm->Services->OAuth 2 Provider->Consent; make sure either “Allow Clients to Skip Consent” is enabled or “Saved Consent Attribute Name” is provided. If you are using “Allow Clients to Skip Consent”, be sure your client also has “Implied Consent” enabled.

It is possible to also see this error:

error_description=The request requires some interaction that is not allowed.&error=interaction_required

This is a normal error to see when your session has expired. However, if you have a valid session, you might still see this error. This could be due to an issue with your AM session cookie. Check to see if the request includes the AM session cookie; if it doesn’t, it’s probably due to a browser behavior known as “SameSite” cookies. This is a new security behavior implemented in recent browsers that is meant to prevent cross-site request forgery errors. Unfortunately it also can block the cookie from the session check call, which prevents it from working properly. To fix this, be sure your AM session cookie is set with both the Secure flag as well as SameSite=none. Secure is set within “Server Defaults->Security->Cookie”. Specify SameSite=none by opening “Server Defaults->Advanced” and adding the property name com.sun.identity.cookie.samesite with a value of NONE.

IDM as an OAuth 2 resource server

Once you have a valid access token within some OAuth 2.0 client application, you are now ready to try to provide it to IDM as a standard HTTP bearer token header. Maybe IDM is not responding with an HTTP 200 like you’d expect. What could be happening?

CORS errors

If you have deployed your SPA OAuth 2 clients to a separate domain from the one IDM is hosted within, they may experience CORS errors when they make REST calls to IDM. To fix this, be sure servletfilter-cors.json has these specific values set within it:

    "allowCredentials" : false,
    "allowedOrigins" : "*",
    "allowedHeaders" : "authorization,accept,content-type,origin,x-requested-with,cache-control,accept-api-version",

authentication.json rsFilter

IDM calls AM’s token introspection URL to get details about the provided access token

There are two phases for IDM’s behavior within rsFilter. This is the first phase.

  • Make sure you’ve configured IDM and AM properly so that introspect call succeeds.
  • Simulate the introspection call using cURL:
export CLIENT_ID=your resource server client id value from authentication.json
export CLIENT_SECRET=your resource server client secret value from authentication.json
export ACCESS_TOKEN=token you received with a separate client (such as the end-user UI)
export TOKEN_INTROSPECTION_URL=a value like https://am.example.com/am/oauth2/introspect
curl -u $CLIENT_ID:$CLIENT_SECRET --data "token=$ACCESS_TOKEN" $TOKEN_INTROSPECTION_URL

A note on realms

If your introspection URL looks like “am/oauth2/introspect”, then you’re using the root realm. Otherwise, if it looks something like “am/oauth2/realms/root/realms/somethingelse/introspect”, then you’re using “somethingelse”.

Possible responses from the above curl request:

  • HTTPS errors. Is your AM server using a trusted CA? Be sure IDM has either (1) the CA certs that signed AM’s HTTPS cert or (2) the AM HTTPS cert added in the truststore. Prefer option (1) for ease of replacement as needed. Bypass HTTPS cert errors with curl using -k.

BE SURE THAT YOU DO NOT ACCIDENTALLY OVERRIDE YOUR CLIENT SECRET WITH A SAVED VALUE FROM YOUR BROWSER WHEN EDITING OAUTH 2 CLIENTS

  • {"error_description":"Client authentication failed","error":"invalid_client"} - For some reason, the client_id and/or the client_secret you are using is invalid. Things to check:
    • Use the AM Native console and open the realm for the introspection endpoint you’re using.
    • Find the IDM resource server client you have configured within Applications->OAuth 2.0->Clients
    • Change the password to the value you are trying to authenticate with
  • {"active": false} - The resource server client isn’t given details about this token. Things to check:
    • Is the token still valid? You’re sure it’s not expired and that it was issued by this AM server?
    • Use the AM Native console and open the realm for the introspection endpoint you’re using.
    • Find the IDM resource server client you have configured within Applications->OAuth 2.0->Clients
    • Make sure this client has am-introspect-all-tokens listed as a scope.
    • If the access token was issued to a client that is registered in a different realm and you want to support access tokens from other realms, then you need to also add the scope am-introspect-all-tokens-any-realm to your resource server client.
  • {"active":true,"sub":"amadmin","scope":"fr:idm:*","realm":"/",.....} - this is good. IDM has enough information about the token to try to move onto the next phase.

IDM uses the token introspection response to construct a security context

Consider a token introspection response that looks like so:

{
  "active": true,
  "scope": "fr:idm:*",
  "realm": "/sub1",
  "client_id": "testclient",
  "user_id": "bjensen",
  "exp": 1601070296,
  "sub": "bjensen",
  "iss": "https://am.example.com/am/oauth2/realms/root/realms/sub1"
}
Scope validation

The first thing IDM checks is the scope value. The scope listed in the introspection response must include all values within the scopes array within rsFilter. If the token does not have all of the required scopes, then you will see this HTTP error:

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="IDM",error_description="The request requires higher privileges than provided by the access token.",scope="fr:idm:*",error="insufficient_scope"

Assuming there are sufficient scopes provided, IDM will use the rest of the introspection response to find a particular resource to use as the basis for the security context. There are three ways IDM will try to do this - using staticUserMapping, subjectMapping and anonymousUserMapping.

For a much more in-depth review of scope usage in IDM, please see my article Client and Subject Authorization in IDM.

Static user mapping

The first thing IDM does is look for an entry within staticUserMapping that matches the sub value. For example, given this configuration:

"staticUserMapping" : [
    {
        "subject" : "amadmin",
        "localUser" : "internal/user/openidm-admin",
        "roles" : [
            "internal/role/openidm-authorized",
            "internal/role/openidm-admin"
        ]
    },
    {
        "subject": "idm-provisioning",
        "localUser": "internal/user/idm-provisioning",
        "roles" : [
            "internal/role/platform-provisioning"
        ]
    }
]

If the sub value from the introspection response is either “amadmin” or “idm-provisioning”, then the security context will be constructed with the corresponding localUser and roles values. Note that if you are using a client credential grant, the client_id is the sub value. Any client using this grant will most likely need an entry within staticUserMapping.

Subject mapping

Assuming the sub value doesn’t match a static user mapping, IDM will then search for a user with the subjectMapping configuration. Consider this block:

"subjectMapping" : [
  {
    "realm": "/sub1",
    "queryOnResource": "managed/subOneUsers",
    "propertyMapping": {
      "sub": "userName"
    },
    "userRoles": "authzRoles/*",
    "defaultRoles" : [
        "internal/role/openidm-authorized"
    ]
  },
  {
    "queryOnResource": "managed/user",
    "propertyMapping": {
      "sub": "_id"
    },
    "userRoles": "authzRoles/*",
    "defaultRoles" : [
        "internal/role/openidm-authorized"
    ]
  }
]

IDM will search through the array of mapping objects, looking for one that is suitable for the access token introspection response. For example, in the above case there is a “realm” value of “/sub1”. IDM will use that detail to select the first subjectMapping object. Using this information, IDM will construct a query that is equivalent to this HTTP request:

GET /openidm/managed/subOneUsers?_queryFilter=userName eq 'bjensen'

Note that you cannot have more than one possible matching subjectMapping declaration; doing so will result in an IDM configuration error.

If there is a single result found from this query, that result will be the basis for the security context. If you are unsure about what records IDM is finding for your token, try querying IDM yourself with a queryFilter similar to the one shown above.

Anonymous mapping

If you have a valid access token and scope, but IDM is unable to find a resource which matches the sub using either of the above two methods, IDM will revert to the anonymousUserMapping configuration.

"anonymousUserMapping" : {
    "localUser" : "internal/user/anonymous",
    "roles" : [
        "internal/role/openidm-reg"
    ]
}

The anonymousUserMapping typically maps to a very low-security resource. Note that this will result in the same security context that would be produced if you made a REST call with no access token at all.

Note that this behavior may change after IDM 7. Future releases may reject an access token which includes an unmatched subject.

Resulting security context

Assuming IDM was able to find the bjensen record based on our above token, the security context IDM would use for the remainder of the request would look something like this:

{
    "authorization": {
        "id": "73b0c6cb-bc16-45d5-8b0e-e7cab4fb7966",
        "roles": [
            "internal/role/openidm-authorized"
        ],
        "component": "managed/subOneUsers"
    },
    "authenticationId": "bjensen"
}

These details will be used by the authorization filter (see router.json) to determine if this user is authorized to perform the specific request being made. You can get back these details for your particular access token by making a GET request to /openidm/info/login. For example:

curl -H "Authorization: Bearer $ACCESS_TOKEN" -i https://idm.example.com/openidm/info/login

If you are getting a 403 error with some other REST call (and it doesn’t mention insufficient_scope), it is either due to an authorization failure or a policy failure. In either case, those are not “platform integration” issues. You might need to adjust your authorization rules in that case.

Logging out

You have successfully configured the system to allow you to operate your SPA client with the IDM resource server endpoints. Now it’s time to log off, and you can’t! Every time you try to click on “Sign out” from your client, you just end up right back where you started. What’s going on?

The answer is “Ops Tokens”. Standard OpenID Connect session termination involves the client passing the original id_token to the end_session_endpoint. In order for AM to use this id_token to end the session, there has to be a link stored in the CTS between the AM session and the given id_token. This link is called an “Ops Token”. Be sure you have “Store Ops Tokens” enabled within “Realm->Services->OAuth 2 Provider->Advanced OpenID Connect”.

Platform self-service trees

At this point we have covered the ways in which the SPA clients operate. However, there is some more behind-the-scenes detail to share regarding the behavior of the new platform-login UI, specifically with regard to platform self-service. If this is feature you plan on using, be sure to carefully follow the official platform documentation for setting up self-service.

This new (to 7.0) implementation of self-service operates within the same platform context that was just covered - OAuth 2.0 interaction between AM, a client, and IDM (as a resource server). The key difference here is the client. Remember, the platform-login UI is NOT an OAuth 2.0 client. However, it does trigger some client behavior indirectly, through its use of the AM authentication tree API.

Some of the AM tree nodes that the platform-login UI calls are actually OAuth 2.0 clients. These tree nodes perform a client credentials grant in order to obtain an access token. The node then makes a standard REST call to IDM, passing the access token as a header just like the SPA clients do.

Note: Since this is a client credentials grant, these access tokens have the client_id as the subject.

The details used by the nodes for this client credential grant are found in two places:

  • Configure->Global Services->IdmIntegrationService: important values for these purposes are idmDeploymentUrl, idmDeploymentPath, idmProvisioningClient. Ignore the others; they are used in older deployments.
  • Realm->Applications->OAuth 2.0->Clients: find the client_id identified by the global service (IDM Provisioning Client). The client secret and scopes defined here will be used to make the client credential grant. Be sure this client has “Client Credentials” registered within “Grant Types”.

You can simulate the behavior of the platform tree nodes by creating your own access token the same way they do, like so:

export CLIENT_ID=your IDM Provisioning Client id, e.g. idm-provisioning
export CLIENT_SECRET=secret registered within your client
export SCOPES=space-delimited list of scopes registered within your client
export TOKEN_ENDPOINT_URL=a value like https://am.example.com/am/oauth2/access_token
curl -u $CLIENT_ID:$CLIENT_SECRET --data "grant_type=client_credentials&scope=$SCOPES" $TOKEN_ENDPOINT_URL

This will return a JSON response with an “access_token” entry. You can use that token to call IDM:

curl -H "Authorization: Bearer $ACCESS_TOKEN" -i https://idm.example.com/openidm/info/login

If you don’t get a successful response from this call, review the above “IDM as an OAuth 2 resource server” section again (but this time, with this token). Keep in mind that the client_id will probably need to be mapped with an anonymousUserMapping. Ensure the roles associated with this mapping have sufficient authorization to make the necessary changes in IDM data. Here is the full set of authorization rules required within access.json (assuming the role you have granted the client is “internal/role/platform-provisioning”):

{
  "pattern"   : "managed/*",
  "roles"     : "internal/role/platform-provisioning",
  "methods"   : "create,read,query,patch"
},
{
  "pattern"   : "internal/role/*",
  "roles"     : "internal/role/platform-provisioning",
  "methods"   : "read,query"
},
{
  "pattern"   : "profile/*",
  "roles"     : "internal/role/platform-provisioning",
  "methods"   : "create,read,action,update",
  "actions"   : "*"
},
{
  "pattern"   : "policy/*",
  "roles"     : "internal/role/platform-provisioning",
  "methods"   : "read,action",
  "actions"   : "*"
},
{
  "pattern"    : "schema/*",
  "roles"      : "internal/role/platform-provisioning",
  "methods"    : "read"
},
{
  "pattern"    : "consent",
  "roles"      : "internal/role/platform-provisioning",
  "methods"    : "action,query",
  "actions"    : "*"
},
{
  "pattern"    : "selfservice/kba",
  "roles"      : "internal/role/platform-provisioning",
  "methods"    : "read"
},
{
  "pattern"    : "selfservice/terms",
  "roles"      : "internal/role/platform-provisioning",
  "methods"    : "read"
},
{
  "pattern"    : "identityProviders",
  "roles"      : "internal/role/platform-provisioning",
  "methods"    : "read"
},
{
  "pattern"   : "external/email",
  "roles"     : "internal/role/platform-provisioning",
  "methods"   : "action",
  "actions"   : "sendTemplate"
},

The specific requests being made to IDM will vary by node. Review the documentation for Identity Management Authentication nodes for more details.

Note: For the UpdatePassword tree, you might notice the sample tree provided in AM 7.0 uses a PatchObjectNode that has the value patchAsObject set to true. You do NOT want to set this to true. Be sure it is set to false. The access token sent to IDM as part of the PATCH request must be made using the client_id as the subject. Relatedly, be sure that the user can’t update their own password directly - edit IDM’s managed.json and set `userEditable to false for the password field. This is needed to prevent other clients from updating a user’s password directly.

All done!

There are many failure modes possible when setting up such a powerful system. Understanding what went wrong can be frustrating. Manually tracking down each issue can be a hassle. But it can also be a very good learning exercise! The end result of all of this effort should be a well-oiled, standards-based platform to build upon.

Also, now that you’ve done all of this work it’s worth pointing out that it was all pre-configured this same way within the default platform configuration available within ForgeOps and the ForgeRock Identity Cloud. Be sure to check those out too!

3 Likes