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 claim | Data returned for the attribute? |
---|---|
legalAcceptances | Yes |
legalAcceptances.legalAcceptanceId | No |
clients | Yes |
clients.clientId | Np |
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 claim | Data returned for the attribute? |
---|---|
primaryAddress | Yes |
primaryAddress.company | Yes |
consents | No |
consents.marketing | No |
consents.marketing.granted | No |
testObject | Yes |
testObject.subObject | Yes |
testObject.subObject.name | Yes |
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.
Updated 7 months ago