How to map nested JSON object with OpenID Connect

Hi,

My customer reported that Pega Platform is unable to map nested JSON object with OpenID Connect based Single Sign On. In this post, I will share how we can achieve this requirement.

  • Issue

In my experience, most of OpenID Provider send attributes as top level in JSON object. However, some software sends these as nested JSON object. For example, below are the sample claims that are returned from Okta’s UserInfo endpoint. You can find that “street_address”, “locality”, “region”, and “postal_code” are nested under “address” attribute.

Customer created corresponding address related properties under Data-Admin-Operator-ID class and tried to map returned nested attributes to Pega properties using mapping tab in Authentication Service rule as below. However, this approach is not valid.

  • Resolution

As of today, current Pega Platform can’t handle nested JSON object in the standard Authentication Service rule and I’ve submitted an enhancement request (FDBK-94722). As a workaround, create a post-authentication activity in which you can access JSON claims directly by Java. You must include below snippet.

tools.getRequestor().getRequestorPage().putString("pyAuthenticationPolicyResult", "true");

Sample Code:

  1. Create a post-authentication activity

  1. In the first step, write Java code as below to retrieve address claims.
Object dp = tools.findPage("D_pzSSOAttributes").getProperty("pyAttrList").getObjectValue();
com.fasterxml.jackson.databind.ObjectMapper om = new com.fasterxml.jackson.databind.ObjectMapper();

try{
  Map<String,Object> props = om.convertValue(dp, Map.class);
  Map<String,Object> address = (Map<String, Object>) props.get("address");

  if(address!=null){
    street_address = address.get("street_address").toString();
    locality = address.get("locality").toString();
    region = address.get("region").toString();
    postal_code = address.get("postal_code").toString();
    //oLog.infoForced("street_address: " + street_address);
    //oLog.infoForced("locality: " + locality);
    //oLog.infoForced("region: " + region);
    //oLog.infoForced("postal_code: " + postal_code);

    ClipboardPage opr = tools.findPage("OperatorID");
    //oLog.infoForced("OperatorID.StreetAddress: " + opr.getString("StreetAddress"));
    //oLog.infoForced("OperatorID.City: " + opr.getString("City"));
    //oLog.infoForced("OperatorID.Prefecture: " + opr.getString("Prefecture"));
    //oLog.infoForced("OperatorID.PostalCode: " + opr.getString("PostalCode"));

    String[][] param = {
      {street_address,opr.getString("StreetAddress")},
      {locality,opr.getString("City")},
      {region,opr.getString("Prefecture")},
      {postal_code,opr.getString("PostalCode")}
    };

    for(int i=0; i<param.length; i++){
    //oLog.infoForced(param[i][0] + " " + param[i][1]);
      if(param[i][0].equals(param[i][1])) {
        isUpdated = false;
      } else {
        isUpdated = true;
        break;
      }
    }
  } else {
    oLog.infoForced("No address found");
  }
} catch (Exception e) {
  e.printStackTrace();
}

tools.getRequestor().getRequestorPage().putString("pyAuthenticationPolicyResult", "true");
  1. In the following steps, complete the activity such way that if there are any changes in attributes, update the operator ID with new values. If none, exit the activity.

Steps:

Pages & Classes

  1. Next, add ARO (Access of Role to Object) to the Access Role for the logging in user. Specify CanAccessSelf Access When rule for Read instances and Write instances.

  1. That’s it. Address information is now properly stored in Operator ID after SSO process is completed.

  • For other top level JSON objects, you can still use Mapping tab in Authentication Service rule (see below screenshot). Only nested JSON object should be handled by post-authentication activity.

  • Note

I have also uploaded a document about how to build OpenID Connect SSO with Okta in a separate post. Please see https://support.pega.com/discussion/how-build-sso-oidc-openid-connect.

Hope this helps.

Thanks,

Hi @KenshoTsuchihashi,

We are trying to implement OIDC for SSO and I’ve come across few issues. Can you help with this?

We get Acess groups related information from the OIDC claims (roles) and have to create/update the operator’s Access groups in Pega based on the info received from claims. Can we map the roles claim directly on the Mapping tab in Auth service rule ?
Or have to follow similar approach as mentioned here ?

Also, Post authentication activity has to be in users default access group as the authentication already happens by then. We have multiple applications hosted on a single db. So should we just put this post authentication activity at enterprise layer so that it works irrespective of what the default access group of the operator is ?

Also, I’m not able to see all these claims on the page D_pzSSOAttributes.pyAttrList on clipboard. I can only see few of these claims on the data page.
So even if I iterate through the attrlist, I won’t be able to get all the claims I see when I decode ID token.

Thanks,

Pavan.

Hi @pavankumarp6690

I am also having the exact same problem,
Did you find a solution for this?

Thanks,
Gustavo

@GustavoJannuzzi Created an activity that retrieves the ID token during authentication, processes the token, extracts all the claims and copies them to a clipboard page.

This is executed on “Unauthenticated Context”. (Called in a data page that is used in “Operator Identification” section of Auth service rule.)

In the activity, retrieving the ID token using this java step.

  1. String idToken=tools.findPage(“AccessTokenPage”).getString(“idToken”);

  2. Create a token profile rule and map all the claims returned as part of ID token.

  3. Pass the ID token retrieved in step 1 to activity “pxProcessJWT” and copy the required details from parsed Token to a temporary page to use wherever required.

@KenshoTsuchihashi when using Java code, how does that relate do local variables?

The piece of code below:

Map<String,Object> address = (Map<String, Object>) props.get(“address”);

How does that work in the activity context? Will Pega understand this as local.address variable automatically? Do you need to declare the local variable in the Parameters tab?

Thanks in advance!