IP Address in openID/OAuth id_token Claims and in SAML response

Is it possible to retrieve User’s IP Address from openID/OAuth Claims and in SAML response. Do we have anywhere to add this or tune up something in configuration.

1 Like

Welcome, @rohipati !

Fair warning: What you are asking is a very common ask but requires a few steps depending on your setup. The steps apply in layers and order and only after all the required steps are complete will you end up where you want to be. So bear with me:

A ForgeRock session contains a Host field by default, which holds the client’s IP address.

If you operate AM behind a load balancer or reverse proxy (the majority of production deployments do) you need to make sure that layer in front of AM forwards the original IP address so that AM can extract it (typically from the X-Forwarded-For header) or you will only get the the load balancer’s or proxy’s IP address and not the user’s. You need to tell AM where to get the address from using an advanced server property com.sun.identity.authentication.client.ipAddressHeader:

Example: com.sun.identity.authentication.client.ipAddressHeader=X-Forwarded-For

In the Admin UI you get to the Advanced Server Properties by selecting Deployment > Servers > Your Server > Advanced.

If your AM sits alone without any hops between it and its clients, then the above does not apply.

The next step is to allow the Host session property to be read. AM treats session properties as confidential and if you want to read them, you must whitelist them in the Session Property Whitelist Service (Admin UI > Services > Session Property Whitelist Service). Add Host to the list in Allowlisted Session Property Names. Usually you’ll see AMCtxId as the initial value in that list:
image

Once you have that all figured out, you can test if AM reflects the correct IP address in its sessions using the session service endpoint:

curl --location --request POST 'https://your.am.server.com/am/json/sessions/?_action=getSessionProperties&_fields=Host' \
--header 'Content-Type: application/json' \
--header 'Accept-API-Version: resource=3.1' \
--header 'Cookie: iPlanetDirectoryPro=<your session token>; amlbcookie=01' \
--data-raw '{}'
{
    "Host": "94.71.25.132"
}

If all went well, you get your own IP address returned by that call.

Once you have validated that the session contains the right data, you can then focus on putting it where you need it:

SAML: Super simple. The default IDP Attribute Mapper allows you to retrieve any profile attribute or session property by name. So you define something like this:
image

That will put the IP address into a SAML attribute with the name ipaddress

OAuth2/OIDC: Similarly simple but this time you are in for the fun with modifying the default Access Token Modification Script and OIDC Claims Script. To obtain the IP address from the session in either script you would use:

session.getProperty("Host")

The session binding contains a reference to the user’s session and from there it’s simple.

Access Token:
The default script contains exactly your use case as sample code, which you just have to uncomment to activate:

(function () {
    // Adds new fields containing the session property values.
    // NOTE: session may not be available for non-interactive authorization grants.
    if (session) {
        try {
            accessToken.setField('ip_address', session.getProperty('Host'));
        } catch (e) {
            logger.error('Unable to retrieve session property value. ' + e);
        }
    }
}());

With the above change in place, your next access token will look something like this:

{
  "sub": "0bf0d4fc-6273-4e8e-a2ce-899df2ee96d4",
  "cts": "OAUTH2_STATELESS_GRANT",
  "auth_level": 0,
  "auditTrackingId": "4390a2ed-952b-4560-ba09-b45fd59d97c7-574624",
  "subname": "0bf0d4fc-6273-4e8e-a2ce-899df2ee96d4",
  "iss": "https://openam-volker-dev.forgeblocks.com:443/am/oauth2/alpha",
  "tokenName": "access_token",
  "token_type": "Bearer",
  "authGrantId": "stiTzeXys0kDwpUAabD9BMcMXAc",
  "aud": "postman",
  "nbf": 1660873057,
  "grant_type": "authorization_code",
  "scope": [
    "openid",
    "profile",
    "fr:idm:*",
    "email"
  ],
  "auth_time": 1660872599,
  "realm": "/alpha",
  "exp": 1660876657,
  "iat": 1660873057,
  "expires_in": 3600,
  "jti": "lPFRvKaQgj5t8jjqWZFqBswalyg",
  "patient": "17288",
  "ipAddress": "99.72.28.182",
  "may_act": {
    "client_id": [
      "postman"
    ],
    "sub": [
      "0bf0d4fc-6273-4e8e-a2ce-899df2ee96d4",
      "7a5a0194-770f-4718-b795-a56dd4f1f1b7"
    ]
  }
}

ID Token Claims:
Just a little bit more complex due to the nature of how OIDC claims work. The easiest is to modify the following two functions in the default script:

  /**
   * OAuth 2.0 scope values (scopes) can be used by the Client to request OIDC claims.
   *
   * Call this configuration method, and pass in as the first argument
   * an object that maps a scope value to an array of claim names
   * to specify which claims need to be processed and returned for the requested scopes.
   * @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims}
   * for the scope values that could be used to request claims as defined in the OIDC specification.
   *
   * Below, find a default configuration that is expected to work in the current environment.
   *
   * CUSTOMIZATION
   * You can choose the claim names returned for a scope.
   */
  utils.setScopeClaimsMap({
    profile: ['name', 'family_name', 'given_name', 'zoneinfo', 'locale'],
    email: ['email'],
    address: ['address'],
    phone: ['phone_number'],
    'fr:idm:*': ['post_logout_url', 'ip_address'],
  });

In the above example, I have mapped the claim ip_address to the scope fr:idm:*. You could equally map it to the profile scope or add its own ipaddress scope.
If you use a scope mapping to activate a claim, the claim will only be added if the application requests the appropriate scope. This is often preferable over always there claims.

Now the other half is to instruct the script how to resolve the ip_address claim by adding a custom claims resolver, modify the following function further down in the script:

  utils.setClaimResolvers({
    /*
      // An example of a simple claim resolver function that is defined for a claim
      // directly in the configuration object:
      custom-claim-name: function (requestedClaim) {
          // In this case, initially, the claim value comes straight from a user profile attribute value:
          var claimValue = identity.getAttribute('custom-attribute-name').toArray()[0]

          // Optionally, provide additional logic for processing (filtering, formatting, etc.) the claim value.
          // You can use:
          // requestedClaim.getName()
          // requestedClaim.getValues()
          // requestedClaim.getLocale()
          // requestedClaim.isEssential()

          return claimValue
      },
      */
    /**
     * The use of utils.getUserProfileClaimResolver shows how
     * an argument passed to a function that returns a claim resolver
     * becomes available to the resolver function (via its lexical context).
     */
    name: utils.getUserProfileClaimResolver('cn'),
    family_name: utils.getUserProfileClaimResolver('sn'),
    given_name: utils.getUserProfileClaimResolver('givenname'),
    zoneinfo: utils.getUserProfileClaimResolver('preferredtimezone'),
    locale: utils.getUserProfileClaimResolver('preferredlocale'),
    email: utils.getUserProfileClaimResolver('mail'),
    address: utils.getAddressClaimResolver(
      /**
       * The passed in user profile claim resolver function
       * can be used by the address claim resolver function
       * to obtain the claim value to be formatted as per the OIDC specification:
       * @see https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim.
       */
      utils.getUserProfileClaimResolver('postaladdress')
    ),
    phone_number: utils.getUserProfileClaimResolver('telephonenumber'),
    post_logout_url: function () {
      return 'https://idc.scheuber.io/login/?realm=/alpha#/service/Launcher'
    },
    ip_address: function () {
      return session.getProperty("Host")
    },
  });

And that’s it! Now you have the user’s IP in 3 places:

  • SAML attribute in the assertion
  • OAuth2 Access Token field
  • OIDC ID Token claim

Please let me know if this allows you to address your use case.

Cheers.
Volker

3 Likes