Custom OIDC claims in ForgeRock Identity Cloud


Custom OIDC claims in ForgeRock Identity Cloud

A guide to adding additional OIDC claims from the identity schema in ForgeRock Identity Cloud

Clients or Relying Parties consuming OpenID Connect (OIDC) tokens may require additional attributes inside the OIDC Token (id_token).

We are going to address two common use cases where we retrieve values of different attributes from the authenticated user to include in the token.

The ForgeRock identity cloud user schema provides :

  • Standard user attributes like first name, last name etc
  • General purpose extension attributes, some indexed, some not, these are baked into the schema and have set names like Generic Indexed String 1 or Generic Unindexed Multivalue 3.
  • Custom unindexed attributes which can be added to the schema, with names like custom_myAttribute.

More information about the Identity Schema can be found here. Also each of the attribute is referenced differently in IDM vs AM, the reference table for this is found here.

Referencing standard and general purpose extension attributes relatively simple as they are explicitly defined in the schema. Custom attributes are a little more complex as they are stored in a single JSON blob. The two use cases below will address each of these.

Use Case 1 — Requirements

The ID Token must include a claim called “status” and the value must be the value of the general purpose extension attribute — Generic Indexed String 1.

Use Case 2 — Requirements

The ID Token must include a claim called “preferredMail” and the value must be the value of a custom attribute called custom_preferredMail.

OIDC Claims Script

ForgeRock provides a mechanism to customise the ID Token by use of an OIDC Claims Script. The OIDC claims script is run when the ID Token is minted by the OpenID Provider (OP), a role filled by ForgeRock Access Management.

OpenID Provider

The script used by the provider can be viewed or changed by looking at the OAuth2 Provider service in the AM. To do this browse to the Access Manager Native Console, then go to Services, OAuth2 Provider, then the Plugins tab. The OIDC Claims script will be listed there, and the OIDC Claims Plugin type says to use this Script as it’s noted as SCRIPTED.

It’s possible to create your own script and set it at the provider level, this will be it’s called any time the provider is called to mint an id_token.

Important: customisations made to the Provider level affects ALL clients.

OIDC Client

The OIDC Claims script can be overridden at the client level, for example Application 1 requires certain additional claims, while Application 2 needs the basic OOTB claims.

For both our use cases we are setting at the client level so let’s setup a new client and custom claims script.

Create Application

Browse to Applications and + Custom Application.

Create an application of type OIDC.

Select Web application type.

We will call our client NewClient, you’ll also be required to set an owner then press Next.

Once Created, browse to the AM Native Console then go to Applications, OAuth 2.0, Clients and click on the NewClient

Modify the new client and add in :

Press Save

On the Advanced tab, set the Token Endpoint Authentication Method to client_secret_post

To make sure this is working lets get an id_token with a standard set of scopes i.e. openid, profile and email.

Expectation:

  1. openid scope will request an id_token
  2. profile scope will return a set of claims representing basic profile attributes i.e. name, family_name, given_name etc
  3. email is a separate claim mapped to the mail attribute

Make a REST call to the authorize endpoint:

curl - location ‘https://openam-fidc.forgeblocks.com/am/oauth2/realms/alpha/authorize?client_id=NewClient&redirect_uri=http%3A%2F%2Fwww.google.com&state=abc123&response_type=code&scope=email%20profile%20openid’ \

  • header ‘Content-Type: application/x-www-form-urlencoded’ \
  • header ‘Cookie: <session_cookie>=’

The response includes the code in the 302 redirect which we swap for the token

curl - location ‘https://openam-fidc.forgeblocks.com/am/oauth2/realms/alpha/access_token’ \

  • header ‘Content-Type: application/x-www-form-urlencoded’ \
  • data-urlencode ‘grant_type=authorization_code’ \
  • data-urlencode ‘code=SAQockGPRFrD5XhF1v_1jAi4qe4’ \
  • data-urlencode ‘client_id=NewClient’ \
  • data-urlencode ‘client_secret=password’ \
  • data-urlencode ‘redirect_uri=http://www.google.com

We have our tokens in the response including the id_token, when decoded:

{
“iss”: “https://openam-fidc.forgeblocks.com:443/am/oauth2/alpha”,
“tokenName”: “id_token”,
“given_name”: “Mark”,
“azp”: “NewClient”,
“name”: “Mark Nienaber”,
“family_name”: “Nienaber”,
“email”: “mailattribute@email.com”,
… additional standard token claims
}

Let’s customise the OIDC claims script to provide some additional information.

Create Custom OID Script

Browse to Scripts, Auth Scripts, New Script then of type OIDC Claims

Let’s call this NewClientOIDClaimScript

Set Client Overrides

In the OAuth Client we will override the provider settings and set the client specific OID Claims script we created above.

In the AM Native Console, browse to Applications, OAuth 2.0, Clients, and select the new client — NewClient.

Browse to the OAuth2 Provider Overrides tab.

Check the Enable OAuth2 Provider Overrides checkbox to enable overrides, now the client will use the settings on this page.

Important: Ensure all the relevant options for this client are selected on this page i.e. Allow Clients to Skip Consent, Use Client-Side Access & Refresh Tokens etc

Set OIDC Claims Plugin Type is set to SCRIPTED, set the OIDC Claims Script to be the new script, NewClientOIDClaimScript then press Save.

We have now set up our client, so let’s look at the use cases and customise the script.

To see the sample script, browse here.

Use Case 1 — include status

Modify setScopeClaimsMap

Modify the NewClientOIDClaimScript and add in a status claim to the setScopeClaimsMap.

This says there is a claim called status, and the value will come from the ‘status’ resolver.

utils.setScopeClaimsMap({
profile: [
‘name’,
‘family_name’,
‘given_name’,
‘zoneinfo’,
‘locale’
],
email: [‘email’],
address: [‘address’],
phone: [‘phone_number’],
status:[‘status’] // ← new claim
});

Add Claim Resolver

Add the claim resolver for ‘status’ and use the already existing getUserProfileClaimResolver to get the available attribute fr-attr-istr1.

utils.setClaimResolvers({

name: utils.getUserProfileClaimResolver(‘cn’),
family_name: utils.getUserProfileClaimResolver(‘sn’),
given_name: utils.getUserProfileClaimResolver(‘givenname’),
zoneinfo: utils.getUserProfileClaimResolver(‘preferredtimezone’),
locale: utils.getUserProfileClaimResolver(‘preferredlocale’),
status: utils.getUserProfileClaimResolver(‘fr-attr-istr1’), // ← new claimResolver
email: utils.getUserProfileClaimResolver(‘mail’),

Test

If we look at a test user in Identity Cloud we can see the the value for frIndexedString1 = Enabled. Remember that frIndexedString1 is the IDM attribute for fr-addr-istr1.

{
“custom_initials”: “mn”,
“custom_otherGivenNames”: “smithy”,
“country”: null,
“frUnindexedString1”: null,
“frIndexedString1”: “Enabled”, // <— we want this value in the token
“givenName”: “Mark”,
“accountStatus”: “Active”,
“sn”: “Nienaber”
}

Request token with additional status scope

curl --location ‘https://openam-fidc.forgeblocks.com/am/oauth2/realms/alpha/authorize?response_type=code&client_id=NewClient&redirect_uri=http%3A%2F%2Fwww.google.com&state=abc123&scope=email%20profile%20openid%20status
–header ‘Content-Type: application/x-www-form-urlencoded’
–header ‘Cookie: =’

After swapping the code for a token, we get the id_token, which when decoded contains a new claim named status and the value is Enabled as expected.

{
“tokenName”: “id_token”,
“given_name”: “Mark”,
“aud”: “NewClient”,
“acr”: “0”,
“name”: “Mark Nienaber”,
“realm”: “/alpha”,
“tokenType”: “JWTToken”,
“family_name”: “Nienaber”,
“email”: “mailattribute@email.com”,
“status”: “Enabled”, // <— new value included
… additional claims
}

Use Case 2 — include custom attribute

This use case is a little more involved because custom attributes like custom_preferredMail are stored in a JSON blob named fr-idm-custom-attrs. We will create a custom claim resolver to get this value.

Modify setScopeClaimsMap

Modify the NewClientOIDClaimScript and add in a customMail claim to the setScopeClaimsMap.

utils.setScopeClaimsMap({
profile: [
‘name’,
‘family_name’,
‘given_name’,
‘zoneinfo’,
‘locale’
],
email: [‘email’],
address: [‘address’],
phone: [‘phone_number’],
status:[‘status’],
customMail:[‘customMail’] // <— new claim

});

Modify claimResolvers

Update setClaimResolvers to include customMail, and for that resolver to use getCustomClaimResolver which we will create below.

utils.setClaimResolvers({
name: utils.getUserProfileClaimResolver(‘cn’),
family_name: utils.getUserProfileClaimResolver(‘sn’),
given_name: utils.getUserProfileClaimResolver(‘givenname’),
zoneinfo: utils.getUserProfileClaimResolver(‘preferredtimezone’),
locale: utils.getUserProfileClaimResolver(‘preferredlocale’),
status: utils.getUserProfileClaimResolver(‘fr-attr-istr1’),
email: utils.getUserProfileClaimResolver(‘mail’),
customMail: utils.getCustomClaimResolver( // <— new resolver
/**

  • Pass in the full JSON blob of all custom attributes
    */
    utils.getUserProfileClaimResolver(‘fr-idm-custom-attrs’)
    ),

Create custom claim resolver

We will create a new custom claim resolver named getCustomClaimResolver. This will take the full fr-idm-custom-attrs JSON string and parse the value to return a specific value. The value in this case is a string, but this could be any supported type.

// new custom claim resolver
function getCustomClaimResolver(resolveClaim) {
function resolveCustomClaim(claim) {
var claimValue = resolveClaim(claim);
var customClaim;
var customEmailAttr = “custom_preferredMail”;
if (isClaimValueValid(claimValue)) {
customClaim = JSON.parse(claimValue);
return customClaim[customEmailAttr];
}
}
return resolveCustomClaim;
}

Make method public

Add the new claim resolver to the list of public methods so it can be called above.

// PUBLIC METHODS

return {
setScopeClaimsMap: setScopeClaimsMap,
setClaimResolvers: setClaimResolvers,
getUserProfileClaimResolver: getUserProfileClaimResolver,
getAddressClaimResolver: getAddressClaimResolver,
getEssentialClaimResolver: getEssentialClaimResolver,
getUserInfoClaims: getUserInfoClaims,
getCustomClaimResolver: getCustomClaimResolver // ← custom resolver
};

Test

Looking at the the JSON of the user in question, we note the value for custom_preferredMail.

{
“custom_initials”: “mn”,
“custom_otherGivenNames”: “smithy”,
“country”: null,
“frUnindexedString1”: null,
“frIndexedString1”: “Enabled”,
“givenName”: “Mark”,
“accountStatus”: “Active”,
“sn”: “Nienaber”,
“custom_preferredMail”: “mypreferredMail@mail.com”,
}

Lets request a token and include the customMail scope.

curl --location ‘https://openam-fidc.forgeblocks.com/am/oauth2/realms/alpha/authorize?response_type=code&client_id=NewClient&redirect_uri=http%3A%2F%2Fwww.google.com&state=abc123&scope=email%20profile%20openid%20status%20customMail
–header ‘Content-Type: application/x-www-form-urlencoded’
–header ‘Cookie: =’

After swapping the code for a token, we receive the id_token with the correct value.

{
“tokenName”: “id_token”,
“email”: “mailattribute@email.com”,
“subname”: “174a4f9c-b6b1-4d18-a1c4-c5597e987979”,
“given_name”: “Mark”,
“aud”: “NewClient”,
“name”: “Mark Nienaber”,
“realm”: “/alpha”,
“tokenType”: “JWTToken”,
“family_name”: “Nienaber”,
“customMail”: “mypreferredMail@mail.com”, <—New Claim added
“status”: “Enabled”
}

The sample script used in this article can be found here.

And that’s it folks.

By Mark Nienaber on May 19, 2023.

Canonical link

Exported from Medium on May 22, 2023.

1 Like

Great blog!

2 Likes

@mark.nienaber - Thank you for documenting this in such a detailed and clear manner.

1 Like

@mark.nienaber - In Identity cloud, I need to pass Roles assigned to a user in claim. Could you please help me to know what should be the getCustomClaimResolver for roles. I have followed your doc and done as below:

Modify setScopeClaimsMap

utils.setScopeClaimsMap({
profile: [
‘name’,
‘family_name’,
‘given_name’,
‘zoneinfo’,
‘locale’,
‘roles’ ← added here
],

Modify claimResolvers

roles: utils.getCustomClaimResolver( // <— new resolver
/**

  • Pass in the full JSON blob of all custom attributes
    */
    utils.getUserProfileClaimResolver(‘roles???’).
    ),
    ===============

Create custom claim resolver

function getCustomClaimResolver(resolveClaim) {
function resolveCustomClaim(claim) {
…How should I fetch roles here. <----
}
return resolveCustomClaim;
}

Make method public

getCustomClaimResolver: getCustomClaimResolver // ← custom resolver
};

=====

I’m following the same steps for adding roles in claims as other attributes assuming its similar.

Hi @tanu.garg,

Check this: User identity attributes and properties reference :: ForgeRock Identity Cloud Docs

You’ll see in the table that effectiveRoles in IDM are mapped to fr-idm-effectiveRole in the user profile.

Regards
Patrick

3 Likes

Hi @tanu.garg exactly what @patrickdiligent said above, also if you want to get “groups” rather than roles you can get this by looking at the memberOf attribute i.e.

    isMemberOf: utils.getUserProfileClaimResolver('isMemberOf'),

If you want to get a nicer format you can build a resolver to do that.

2 Likes

Thank You @patrickdiligent and @mark.nienaber for your advise and it works.Below is claims in id_token for ‘roles’.

Do you have any idea on a easy way to send just ‘name’ of role instead of _refResourceCollection, _refResourceId, _ref in claim.

“roles”: [
“{"_refResourceCollection":"managed/alpha_role","_refResourceId":"88932ab5-e9fd-417f-b88a-a13314077f19","_ref":"managed/alpha_role/88932ab5-e9fd-417f-b88a-a13314077f19"}”,
“{"_refResourceCollection":"managed/alpha_role","_refResourceId":"418199c7-88b1-44b1-8880-25eaba1db9c4","_ref":"managed/alpha_role/418199c7-88b1-44b1-8880-25eaba1db9c4"}”
],

simple output like this

“roles”: [
“name1”,
“name2”
],

Hi @tanu.garg,

Under your code extract you’ve marked with …How should I fetch roles here. <----, rather than returning the array as is, you’ll have to process it using javascript native array and string manipulation function and iteration, so that to produce the correct value. However, what do you mean by “name”, are you referring to the role _id as “managed/alpha_role/_id”?

Regards
Patrick

by name I mean is name value of a role because _id is a non recognizable value for OIDC Target application. Below is the RAW json of a Alpha realms - Roles

{
“_id”: “88932ab5-e9fd-417f-b88a-a13314077f19”,
“name”: “EAS role”,
“description”: "Role for EAS access "
}

Hi @tanu.garg,

The name is not included in the relationship value unfortunately. The options is to either:

Regards
Patrick

2 Likes

Thank You Patrick for sharing the valuable idea

1 Like

@patrickdiligent @mark.nienaber

I have been trying to get Authorization Roles in claims. I followed all above steps and even tried creating custom resolver.

Authorization Roles authzRoles fr-idm-managed-user-authzroles-internal-role

But it looks like it is not part of User RAW JSON. hence, its not returned in claim.

Is there any alternative way to send Authorization Role / Internal Role id_token claims.

From IDC documentation:Roles :: ForgeRock Identity Cloud Docs

" * Authorization roles : used to specify the authorization rights of a managed object internally, within IDM.
Authorization roles are created as internal roles, at the context path openidm/internal/role/role-name, and are granted to managed users as values of the user’s authzRoles property."

Hi @tanu.garg,

Yes you are correct, this attribute (fr-idm-managed-user-authzroles-internal-role) is not present in the user profile (it’s not included in the response to the AM user profile REST endoint). You might have to issue a support ticket to get at the bottom of this.

Meanwhile, as a workaround, you could use an RDVP again in that case. However, unfortunately, the virtual value would not be updated until authzRoles is updated - which is not convenient - only newly created objects will have a consistent value. To get around this, create a dummy internal role, then use a scheduler to update the user entries - adding then removing the dummy role. Use paged queries to iterate over the identities for a very large number of identities (100k+).

Regards
Patrick

1 Like

@patrickdiligent - I have used the rdvp property (effectiveroles) now which is a OOTB RDVP property for provisioning roles. And the result is as below.
But whatever code I write in Javascript ‘for loop’ for parsing Array of JSON objects. I’m unable to fetch result as [“direct_pending”,“direct_active”]

fr-idm-effectiveRole=
[{"name":"direct_pending","_id":"e6d4c521-7154-4489-b24d-c175a0e3b415","_rev":"3d5b8826-cde7-46f3-a104-853927787305-239434","_refResourceCollection":"managed/alpha_role","_refResourceId":"e6d4c521-7154-4489-b24d-c175a0e3b415","_ref":"managed/alpha_role/e6d4c521-7154-4489-b24d-c175a0e3b415"},

{"name":"direct_active","_id":"9f1999cd-a24f-44ae-bb6a-237cb9369fb8","_rev":"92664f3f-8430-44a6-98f3-3830a52d0807-82797","_refResourceCollection":"managed/alpha_role","_refResourceId":"9f1999cd-a24f-44ae-bb6a-237cb9369fb8","_ref":"managed/alpha_role/9f1999cd-a24f-44ae-bb6a-237cb9369fb8"}]

Sample tried code Snippets and similarly few more variations of for loop. but it look like there is something tricky here related to product which I’m unable to do because the same code works well in normal javascript but not when I use in forgerock cloud scripting engine.

const names = [];
const parsedResult = JSON.parse(identity.getAttributeValues(“fr-idm-effectiveRole”));
for (var i = 0; i < 2; i++) {
//const names = JSON.parse(identity.getAttributeValues(“fr-idm-effectiveRole”)[i]).map(obj => obj.name);
names.push(parsedResult[i].name);}
logger.error(“DEBUG_direct fr-idm-effectiveNamesA”+ names);

You don’t need to parse JSON here, just something like this:

var roles = idRepository.getAttribute(id, "fr-idm-effectiveRole");
if (roles) {
   roles.toArray().forEach(function (role) {
            // Do stuff here
            var name = role['name'];
   });
}

or yes, map instead of forEach should work here as well.