Use plurals or objects in custom claims

Custom claims provide a way for you to ask for user profile information in your Hosted Login authorization requests. More specifically, custom claims enable you to request user profile information that isn’t returned by the standard OpenID Connect scopes and claims. For example, suppose you have a custom attribute( (membershipType) you’d like to add to a user’s identity token or make readily-accessible from the userinfo endpoint. To do that, you can simply create a custom claim that references your custom attribute.

Custom claims are pretty straightforward when it comes to “top-level” user profile attributes like displayName or givenName. But what happens if the user information you want to retrieve comes from an attribute that uses the object datatype or the plural datatype? Can you create custom claims using these datatypes? If so, how do you reference an attribute such as the primaryAddress object’s company attribute? And what happens if you request a custom claim that Hosted Login isn’t able to return?

📘

An object is (typically) a single attribute that has multiple sub-attributes. For example, the primaryAddress object includes sub-attributes such as city, stateAbbreviation, and country. And there’s good reason for that: a mailing address is made up of multiple pieces of information. (Unlike, say, the givenName, which is simply a user’s first name.) By comparison, a plural is an attribute type that can store an indefinite number of instances. For example, the profiles plural contains data collected from all of a user's social media profiles, with each social media provider (Facebook, Twitter, LinkedIn) having its own set of attributes contained with the profiles plural.

In this article, we run through a number of custom claim scenarios using plurals and objects. Equally important, we show you which of these scenarios return data and which ones don’t. Note that, in order to run our tests, we created a number of custom claims by adding this bit of JSON syntax to our login policy:

"customClaims": {
    "id_token": {
        "consents": "consents",
        "consentsmarketing": "consents.marketing",
        "consentsmarketinggranted": "consents.marketing.granted",
        "legalacceptances": "legalAcceptances",
        "legalacceptanceslegalacceptanceid":   "legalAcceptances.legalAcceptanceId",
        "primaryaddresscompany": "primaryAddress.company",
        "primaryaddress": "primaryAddress",
        "clients": "clients",
        "clientsclientid": "clients.clientId"
    }
}

As you can see, the preceding claims center around four user profiles attributes: two objects (consents and primaryAddress) and two plurals (legalAcceptances and clients). So which of these claims did what we hoped they would do? Let’s find out.

But First, A Quick Note About Invalid Claims

Before we go much further we should be clear about what happens if you try to add an invalid claim to your login policy or you specify an invalid claim in your authorization request. As it turns out, Hosted Login doesn’t really care about invalid claims: if a claim doesn’t exist or if a claim can’t be processed then Hosted Login simply ignores it. For example, suppose you try to add a custom claim like the following to your login policy, a claim that references a non-existent user profile attribute (invalid):

"invalidclaim": "invalid"

What happens when you try to create this claim? Well, for one thing, the claim does get created: Hosted Login makes no attempt to verify attribute names when you create a custom claim. As a result, invalidclaim will be a valid claim. And what happens if you include invalidclaim in your authorization request? Nothing. Hosted Login can’t supply a value for the claim (because the invalid attribute doesn’t exist), so it ignores the claim altogether.

The same thing happens if you reference a claim that isn’t in your login policy. Suppose your authorization request includes a claim, notinpolicyclaim, that isn’t defined in your login policy. What happens if you include notinpolicyclaim in an authorization request? The answer is the same: nothing. Hosted Login can’t supply a value for a claim it’s never heard of, so it ignores the request and continues with the authorization process.

In general, the fact that Hosted Login can easily shrug off invalid claims is a good thing: that means that user authorization won’t fail because of a claims-related error. On the other hand, this can also complicate troubleshooting problems with claims. For example, suppose claims aren’t showing up in your identity token. Is that because they’re incorrect referenced in the authorization request? Is that because the claim can’t be found in the login policy? Is that because the claim references a user profile attribute that doesn’t exist? To be honest, there’s no straightforward way of knowing. Which simply means that you’ll need to be prepared to do a little digging and a little investigating if your claims aren’t showing up as expected.

Plurals

When it comes to custom claims, plurals tend to be all-or-nothing: 1) you can return all the instances and all the attributes of a plural; or, 2) you can’t return any of the instances and attributes of the plural. To explain what that means, let’s start by taking a look at a table that shows the results of our custom claims experiment, and tells us whether a given claim actually returned any data:

Attribute referenced in the claimData returned for the attribute?
legalAcceptancesYes
legalAcceptances.legalAcceptanceIdNo
clientsYes
clients.clientIdNp

So what does that mean? Let’s start with the legalAcceptances plural. If you look at the schema for the legalAcceptances plural, it’s composed of the top-level legalAcceptances plural attribute plus a number of additional attributes (id, clientId, etc.):

In our login policy, we created a claim that requested the top-level legalAcceptances attribute:

"legalacceptances": "legalAcceptances",

This claim works. After we logged on, all the attributes (and attribute values) for the legalAcceptances plural were copied to the identity token:

"legalacceptances": [
    {
        "clientId": "34way7esasgyjsq99wu7emu6wtt82j8w",
        "dateAccepted": "2020-09-14 21:58:38 +0000",
        "id": 8835,
        "legalAcceptanceId": "privacyPolicy-v1"
    },
    {
        "clientId": "34way7esasgyjsq99wu7emu6wtt82j8w",
        "dateAccepted": "2020-09-14 21:58:38 +0000",
        "id": 8836,
        "legalAcceptanceId": "termsOfService-v1"
    }
],

In other words, requesting the top-level attribute for a plural attribute returns everything stored in that attribute.

Perfect.

Our login policy also defines a custom claim for the clients plural:

"clients": "clients",

This claims also returns a full set of attributes and attribute values:

"clients": [
    {
        "clientId": "34way7esasgyjsq99wu7emu6wtt82j8w",
        "firstLogin": "2021-01-21 22:24:23 +0000",
        "id": 8834,
        "lastLogin": "2021-01-21 22:24:23 +0000",
        "name": null
    }
],

The net takeaway? A top-level plural attribute can be used in a custom claim, and this top-level attribute returns data.

However, that’s as far as we can go, at least when it comes to plurals. For example, we also defined a custom claim that, in theory, returns the value of the legalAcceptances.legalAcceptanceId attribute:

"legalacceptanceslegalacceptanceid":   "legalAcceptances.legalAcceptanceId",

As it turned out, however, this claim doesn't return any data. If you look at the identity token, you won’t see any listing for the legalacceptanceslegalacceptedid claim:

Similarly, the claim we defined for the client.clientId attribute also failed to return any data. Which simply means that, with plurals, you can create custom claims for, and return data from, the top-level attribute only:

This why we said that plurals were all-or-nothing: you can create a claim for the top-level attribute (e.g., legalAcceptances) and get back all the data stored in that attribute. But trying to narrow things down to a sub-attribute (legalAcceptances.legalAcceptanceId) doesn’t return any data at all. All or nothing: take your pick.

Objects

When it comes to creating custom claims, plurals are pretty easy to deal with: top-level attributes can be used in custom claims but sub-attributes can’t. By contrast, objects don’t appear to follow a similar sort of pattern, as shown in the table below. For testing purposes, we created claims that use the primaryAddress and consents objects, We also defined claims for a custom object (testObject) we added to our schema. Here’s what we go back:

Attribute referenced in the claimData returned for the attribute?
primaryAddressYes
primaryAddress.companyYes
consentsNo
consents.marketingNo
consents.marketing.grantedNo
testObjectYes
testObject.subObjectYes
testObject.subObject.nameYes

So what’s going on here? Let’s start by looking at the primaryAddress object, an object that did work as a custom claim. This object is comprised of a top-level object (primaryAddress) plus a number of attributes (e.g., address1 and address2):

As you might recall, in our login policy we defined custom claims for both the top-level object (primaryAddress) and for a single attribute of that object (primaryAddress.company). And guess what? Both of those claims return data. When we use the top-level claim (primaryAddress) we get back all the object attributes and attribute values:

"primaryaddress": {
    "address1": "555 NE 55th Ct",
    "address2": null,
    "city": "Kirkland",
    "company": "Akamai",
    "country": "US",
    "phone": null,
    "stateAbbreviation": "WA",
    "zip": "98011",
    "zipPlus4": null
 },

And when we reference the primaryAddress.company attribute? In that case, we get back the value of that attribute:

"primaryaddresscompany": "Akamai",

Which is exactly what we wanted to get back.

The moral of this story is that objects like primaryAddress work just fine in custom claims: call the top-level object to return all the attributes and attribute values, or call a single attribute to return just that attribute and that attribute value. Either approach works.

But what about the consents object, an object that didn’t work as a custom claim? As it turns out, the consents object differs from primaryAddress: it contains sub-objects, meaning that you can have more than one consent. In our sample schema, the consents object includes a marketing and a personalizedAds sub-object:

In our login policy we defined three consents, one on the top-level object (consents), one on a sub-object (consents.marketing), and one on an attribute of that sub-object (consents.marketing.granted):

"consents": "consents",
"consentsmarketing": "consents.marketing",
"consentsmarketinggranted": "consents.marketing.granted",

So how did these custom claims work? Not very well. For example, the top-level consents claim returned the names of the sub-objects and their attributes, but didn’t return any attribute values:

"consents": {
    "marketing": {
        "clientId": null,
        "context": null,
        "granted": null,
        "type": null,
        "updated": null
    },
    "personalizedAds": {
    "clientId": null,
    "context": null,
    "granted": null,
    "type": null,
    "updated": null
}

The consentsmarketing claim (associated with the consents.marketing attribute) did the same thing, albeit only for the consents.marketing sub-object:

"consentsmarketing": {
    "clientId": null,
    "context": null,
    "granted": null,
    "type": null,
    "updated": null
},

Meanwhile, the consents.marketing.granted claim, an attempt to return just the granted attribute for the consents.marketing sub-object, well, that didn’t return anything at all. Total failure.

So does that mean that you can’t return information from objects that contain sub-objects? That’s what we thought … at first. As it turns out, however, consents is a “different” kind of object, and its behavior has nothing to do with OIDC or Hosted Login. In fact, you’ll get nothing but null values even if you access the object using the Identity Cloud REST APIs:

{
    "result": {
        "consents": {
            "personalizedAds": {
                "clientId": null,
                "granted": null,
                "context": null,
                "updated": null,
                "type": null
            },
            "marketing": {
                "clientId": null,
                "granted": null,
                "context": null,
                "updated": null,
                "type": null
            }
        }
    },
    "stat": "ok"
}

By comparison, any custom attributes you add to your schema return data just fine. For example, we created a simple object (testObject) that includes a pair of sub-objects:

We then created three custom claims aimed at: 1) the top-level object (testObject); 2) one of the two sub-objects (testObject.subObject); and, 3) a single property of that sub-object (testObject.subObject.name):

"testobject": "testObject",
"testsubobject": "testObject.subObject",
"testobjectsubobjectattribute": "testObject.subObject.name",

📘

As with plurals, remember to use dot notation and the full attribute path when creating your claim. The attribute isn’t the subObject attribute; it’s the testObject.subObject attribute.

And what happened when we included these three claims in our authorization request? This happened:

As you can see, all three of our object-based claims returned the expected data. The consents object can’t be used in a custom claim, but other objects (including custom objects, and including objects that contain multiple sub-objects) should work just fine.