Create custom claims

In the ​Akamai​ Identity Cloud , an OpenID Connect (OIDC) claim is typically another name for a user profile attribute: a user’s first name is a claim, a user’s last name is a claim, etc. (See the article OpenID Connect Scopes and Claims for more information.) By default, the following standard claims can be returned when a user is authenticated:

sub

Subject. Unique identifier for the end user.
uuid
iss

Issuer Identifier. The URL of your login server.
--
auth_time

Authentication time. Indicates the last time that the user was authenticated.
lastLogon
acr

Authentication Context Class Reference. Specifies a set of business rules that user authentications must meet.
--
name

User’s full name, including titles and suffixes.
--
given_name

Given name (first name) of the user.
givenName
family_name

Surname (last name) of the user.
familyName
middle_name

Middle name of the user.
middleName
preferred_username

Name by which the user wishes to be referred, such as Karim Nafir, K. Nafir or karim_n.
displayName
gender

User's gender.
gender
birthdate

The user’s birthday, represented in ISO8601‑2004 format (e.g., 1967-07-12, or YYYY-MM-DD).
birthday
updated_at

Indicates the last time that the user profile was updated.
lastUpdated
address

User’s preferred mailing address.
Derived from primaryAdress
phone_number

User’s preferred cell phone (mobile) number.
mobileNumber
phone_number_verified

Set to true if the user’s phone number has been verified.
Derived from mobileNumberVerified
email

User’s preferred email address.
email
email_verified

Set to true if the user’s email address has been verified.
Derived from emailVerified

That’s a good-sized collection of user information, and for many organizations that might be all the user information they need returned following a successful authentication. However, other organizations might require information not covered by the standard Identity Cloud scopes and claims. For example, suppose that much of your user experience is based on a user’s primaryAddress.company attribute, a user profile attribute not returned by default. That becomes a problem: how do you retrieve user profile attributes if those attributes aren’t automatically returned when a user is authenticated?

That sounds like a daunting task but, as it turns out, there are at least two ways to get back “extra” user profile attributes. One way, of course, is to make an API call and return the required information: the fact that OpenID Connect returns only a limited collection of user profile attributes doesn’t prevent you from using the Identity Cloud REST APIs to return additional attribute values (and to return these values any time you want).

The other option is to create a “custom claim,” an OIDC methodology for returning user profile attributes that aren’t returned by default. Do you need to have a user’s primaryAddress.company attribute returned after he or she logs on? No problem: just define a custom claim in your login policy that asks for the primaryAddress.company attribute to be returned along with the standard scopes and claims.

📘

Does it matter whether you use an API call or a custom claim to return user profile attributes? Not really: either way you’ll get back the desired information. The advantage to using a custom claim is that you don’t have to write any code for retrieving and storing this information: the Identity Cloud takes care of that for you. And if you change your mind and decide you don’t need the primaryAddress.company after all, just remove the custom claim from the login policy. Again, no coding required.

One quick clarification before we go any further: custom claims can only be specified in the login policy, which means that only users who employ that policy will have custom claim information returned. For example, suppose you define a custom claim for primaryAddress.company in Login Policy A. If a user employs Login Policy A when he or she logs in then the primaryAddress.company attribute will be returned. But what if a different user employs Login Policy B or Login Policy C when they log on? In those cases, the primaryAddress.company attribute will not be returned. That’s because the login policy employed by that user doesn’t include the custom claim.

To make a long story short, if you want to return the primaryAddress.company attribute for all your users then your custom claim has to be added to Login Policies B and C as well as to Login Policy A. (Of course, if you don’t use multiple login policies then you don’t have to worry about this.)

To add a custom claim to a login policy, use syntax similar to this:

"customClaims": {
       "user_info":
           {"organization": "primaryAddress.company"}
     }

In the preceding code snippet, user_info specifies that primaryAddress.company is to be returned as part of the user data accessible from the userinfo endpoint. Alternatively, you can replace user_info with id_token:

"customClaims": {
       "id_token":
           {"organization": "primaryAddress.company"}
     }

In that case, primaryAddress.company will only be returned as part of the user’s identity token: the attribute value won’t be returned if you make a call to the userinfo endpoint.

Could you have some attributes associated with the identity token and other attributes associated with the userinfo endpoint? You bet you can:

"customClaims": {
       "id_token":
           {"organization": "primaryAddress.company"},
       "userinfo":
           {"cell_phone": "mobileNumber"}
     }

Note the following syntax:

{"organization": "primaryAddress.company"}

As you might have guessed, primaryAddress.company is the path to the user profile attribute. As shown in this example, if the attribute is part of a plural or object attribute (like primaryAddress.company is) then you must specify the full path in your custom claim using dot notation:

primaryAddress.company

Meanwhile, organization is simply the name given to the custom claim. This name is completely arbitrary; it can be the same name as the user profile attribute name or it can be something completely different. The following isn’t a particularly user-friendly claim name, but it’s valid:

{"XXXXX": "primaryAddress.company"}

That creates a claim named XXXXX. Like we said, it’s not user-friendly, but it works.

Incidentally, this is a good time to point out that, when it comes to naming your custom claims, OIDC can be a little … quirky …. As you know, the Identity Cloud typically uses “camel casing” when naming things; that’s why we have attributes like lastLogin and mobileNumber. Because of that, you might be tempted to use camel casing when naming custom claims:

{"userOrganization": "primaryAddress.company"}

When you do that, your API call will succeed. However, the custom claim name userOrganization will be converted to this: user_organization. That’s because OIDC doesn’t like uppercase letters and camel casing. Therefore, the uppercase O in userOrganization is replaced by an underscore (_) and a lowercase o. In other words, and to meet OIDC standards, the login policy will end up as if we’d used this syntax:

{"user_organization": "primaryAddress.company"}

To avoid confusion we recommend you avoid uppercase letters and stick to “snake casing”: all lowercases letters, with underscores used as separators.

And yes, you can include more than one attribute in a custom claim:

"customClaims": {
       "id_token":
           {"organization": "primaryAddress.company"},
           {"cell_phone": "mobileNumber"},
           {"most_recent_update": "lastUpdated"}
     }

To add a custom claim to a login policy, use the PUT method and /{customer_id}/config/loginPolicies/{policy_id} endpoint. For example, in curl your API call might look like this:

curl -X PUT \
  'https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/config/loginPolicies/ad2cad34-e06f-463e-a43f-b5c8af0ee965' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer FN6pZvTNZg0OVQyubabW8BlWZhVf6qNHRYOY69EntYdbUS2JVqXeXMGFJegkBUAg' \
  --data-raw '{
       "id": "ad2cad34-e06f-463e-a43f-b5c8af0ee965",
       "identityStoreDetails": {
           "type": "janrainCapture",
           "connectionDetails": {
            "domain": "se-demos-gstemp.us-dev.janraincapture.com",
            "applicationId": "79y4mqf2rt3bxs378kw5479xdu",
            "entityType": "GREG_DEMO",
            "clientId": "y4xfg6f44msac3vepjjvxggzvt3e3sk9",
            "clientSecret": "95ccxk7czbvuzx6dpte5k9p6dj5bzeku"
        }
    },
    "loginURL": "https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/auth-ui/login",
    "customClaims": {
        "userinfo": {
            "organization": "primaryAddress.company"
        },
        "id_token": {
            "organization": "primaryAddress.company"
        }
    },
    "title": "GREG_DEMO Login Policy"
}'

And yes, you’re right: that’s a lot of code just to add a single custom claim. But remember that, like most of the OIDC Configuration endpoints, /{customer_id}/config/loginPolicies/{policy_id} requires you to specify all the properties of an object (e.g., a login policy) even if you’re only changing one of those properties. The only change we’re making here is the custom claim: everything was copied from the current set of properties and property values and pasted into our API call exactly as-is.

📘

As a matter of fact, yes, that is the best way to update a login policy: use the GET method to return all the current properties of the policy, copy the properties and property values, then paste those items into the body of your PUT call. After that you can then make all your changes (like adding a custom claim).

That leaves us with just one problem: now that we have a custom claim, what exactly do with it? Well, if want to return the values associated with that claim we need to include the claim in our in our initial authentication request. That’s done by adding the claims parameter to the authentication request:

https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/login/authorize
    ?client_id=a123ef65-83dc-4094-a09a-76e1bec424e7
    &redirect_uri=https://oidc-playground.akamai.com/redirect_uri
    &scope=openid
    &code_challenge=ZJvyt3-dkp_mmf6VWUiRiG_8O3QxQswrNs99Zlk7khU
    &code_challenge_method=S256
    &response_type=code
    &claims={"userinfo":{"organization":null},"id_token":{"organization":null}}
    &state=mclPck7S-uMvEi8EVZyPIyYHKABav8SScGMEyI3jc3o

As shown above, we’re making two requests here: we’re adding the organization claim to both the userinfo endpoint and to the Identity Token. Note the syntax used to make these request:

"userinfo":{"organization":null}

In this syntax:

  • userinfo indicates that data should be copied to the userinfo endpoint. (But you already knew that.)

  • organization is the name of the custom claim. (You already knew that, too.)

  • null simply means that we want the claim returned in the usual manner. There are other ways to return custom claims, but none of those are of interest to us, at least not for now.

If you want to return multiple claims, just specify those additional claims as part of a comma-separated list. For example:

&claims={"userinfo":{"organization":null,"cell_phone":null},"id_token":{"organization":null,"cell_phone":null}}

Note, too that there’s no difference whatsoever in the way you specify a custom claim vs. the way you specify a standard claim. For example, this syntax specifies a standard claim (gender) and a custom claim (organization):

&claims={"userinfo":{"gender":null,"organization":null}}

So what happens when we make an authorization request use the organization custom claim? Well, if we asked that the claim be included in the identity token then that token will end up looking something like this:

{
       "at_hash": "TQEIgup-iE5cOMWJa_W4-w",
       "aud": [
             "a123ef65-83dc-4094-a09a-76e1bec424e7"
       ],
       "auth_time": 1581695715,
       "exp": 1581698818,
       "global_sub": "https://se-demos-gstemp.us-dev.janraincapture.com/79y4mqf2rt3bxs378kw5479xdu/GREG_DEMO/b48f3a24-28e7-4f0b-8379-53f7d3ff6ec0",
       "iat": 1581697018,
       "iss": "https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/login",
       "jti": "2JCs3cgjoUNmFwu-MiSCvRGa",
       "organization": "Akamai",
       "sub": "b48f3a24-28e7-4f0b-8379-53f7d3ff6ec0"
}

And if we also asked to have organization copied to the userinfo endpoint, here’s the sort of information we can expect to get back if we send a userinfo request:

{
       "global_sub": "https://se-demos-gstemp.us-dev.janraincapture.com/79y4mqf2rt3bxs378kw5479xdu/GREG_DEMO/b48f3a24-28e7-4f0b-8379-53f7d3ff6ec0",
       "organization": "Akamai",
       "sub": "b48f3a24-28e7-4f0b-8379-53f7d3ff6ec0"
}

Here's something important to keep in mind. Suppose you request the organization claim, but the user doesn’t have an organization listed in their user profile; that is, the primaryAddress.company attribute is null. In a case like that, you might expect the organization claim to be returned as a blank value:

{
  "global_sub": "https://se-demos-gstemp.us-dev.janraincapture.com/79y4mqf2rt3bxs378kw5479xdu/GREG_DEMO/b48f3a24-28e7-4f0b-8379-53f7d3ff6ec0",
  "organization":"",
  "sub": "b48f3a24-28e7-4f0b-8379-53f7d3ff6ec0"
}

However, that’s not how it works. Instead, if a claim is null that claim will not be returned at all. That means that, in the identity token or the user_info endpoint, your user data will look like this, with no mention whatsoever of the organization claim:

{
  "global_sub": "https://se-demos-gstemp.us-dev.janraincapture.com/79y4mqf2rt3bxs378kw5479xdu/GREG_DEMO/b48f3a24-28e7-4f0b-8379-53f7d3ff6ec0",
  "sub": "b48f3a24-28e7-4f0b-8379-53f7d3ff6ec0"
}

If your authorization request doesn’t return a claim, and if you’re sure that you requested a valid claim, the most likely answer is that the underlying user profile attribute is null.

Just remember: any custom claim you request must be defined in your login policy. As long as you can remember that the rest should be easy.