Scopes and Claims

Scopes are defined by RFC 6749#section-3.3 as the permissions that the user grants the application (the client). Common practice is that these scopes are defined by, and represent, the APIs that the applications need access too.

Scopes are defined as simple strings or prefixes, and must be configured in the profile before they can be assigned to a client.

Claims are defined in Section 5. of the OpenID Connect Core specification. A claim is an attribute, that has a name and a value that is asserted by the issuer of the token, i.e. a claimed value. Where OpenID Connect specifies only profile-related claims, Curity Identity Server considers claims a more generic concept beyond those claims that are standardized by OpenID Connect.

Where OpenID Connect specifies a limited set of scopes to act as groups of claims (e.g. the email scope represents the claims email and email_verified), with Curity you can specify your own scope-to-claim mappings.

In general, Curity considers a scope a group of claims. When reading about scopes throughout this section, always keep that in mind. The exception to this, are prefix scopes, which can not contain claims by definition.

Adding a scope to the profile

When creating a new scope, the following factors need to be considered:

  1. What should the scope be called
  2. What claims, if any, should the scope represent
  3. How long should it be valid for
  4. Whether the scope is required
  5. Whether the scope is a prefix scope

These questions will be discussed in the next sections, and the first is a matter of policy. Many organizations name their scopes according to some domain+operation schema. Such as insurance_read, books_write or titles_admin.

Curity does not enforce any particular naming scheme but recommend to not create too many scopes per function since it tends to explode in numbers and implode in usability. Prefix scopes may be a solution for cases where fine-grained access needs to be defined, as we’ll see below.

A scope may be created via the user interface or via the REST API, as in the following example.

Add a scope using curl

Listing 192 Add a scope
curl -X POST -H "Authorization: Basic YWRtaW46UGFzc3dvcmQx" -H "Content-Type: application/vnd.yang.data+json" -d '{
  "scope" : {
      "id" : "insurance_read",
      "description" : "This scope gives read access to insurance operations"
  }
}' "https://localhost:6749/admin/api/rest/running/profiles/profile/oauth1,as:oauth-service/settings/authorization-server/scopes?deep"

Adding a scope to a client

When the scope exists in the profile’s configuration, it can be added to clients that should be allowed to request it, and by indirection, request the claims that the scope contains. Each client configuration holds a list of references to some or all of the existing profile’s scopes.

To add a new scope to a client, simply add it to the list:

Add a scope to a client using curl

Listing 193 Add a scope to a client
curl -X PATCH -H "Authorization: Basic YWRtaW46UGFzc3dvcmQx" -H "Content-Type: application/vnd.yang.data+json" -d '{
"client" : {
  "id" : "client-one",
  "scope" : "insurance_read"

  }
}' "https://localhost:6749/admin/api/rest/running/profiles/profile/oauth-dev,as:oauth-service/settings/authorization-server/client-store/config-backed/client"

Scope Lifetime

Commonly scopes live for the lifetime of the grant. That is until the refresh token no longer can be used to issue more access tokens. In Curity this lifetime is represented by a delegation. When the delegation expires, all tokens tied to it are also expired.

Many times however its useful to let the client keep refreshing the tokens over a long time, but only let it access critical data immediately after the user has authenticated.

Bank app example

Consider an app that allows you to do banking business, such as transferring money between accounts and viewing your balance.

In this case, it’s very poor user experience to have the customer authenticate strongly just to check their balance, but very critical that it authenticated strongly when transferring money.

For this purpose, we can set lifetimes for scopes. Imagine the following scopes:

  • account_transfer
  • account_balance

The account_transfer scope requires the bank to know that the user is there, but the account_balance scope could be safe to allow for months after the user has authenticated.

To accomplish this, the administrator can assign lifetimes to each scope. No lifetime means it will live until the delegation expires.

Scope Time To Live
account_transfer 30 minutes
account_balance 1 month

This would allow the app to show the balance during 1 month, simply by refreshing the access token using the refresh token at any time during that month. If the app refreshes during the first 30 minutes, it will receive an access token containing both scopes, but it if refreshes after the 30 minutes it will receive an access token containing only the account_balance scope.

Min access token lifetime

There is of course a corner case. If the app refreshes say 1 minute before the 30 minutes has elapsed, it would receive an access token that lives for 1 minute with both scopes, and then it would have to refresh again to get a new weaker token. This is not a desirable behaviour, and for that reason the administrator can set a min-access-token-ttl which defines the shortest duration an access token should be issued for. If in the example above this would be set to 2 minutes, then if the client asks for an access token 1 minute before the 30 minute cutoff, the server would drop the scope account_transfer and issue a full lifetime weaker token. This happened because 1 minute < 2 minutes which was the minimum lifetime tokens are issued for.

This gives three parameters that are considered when issuing a new token

  1. The time from the original issuance
  2. The access token time to live setting
  3. The minimum access token lifetime setting

Given the following example settings:

  • Access token lifetime setting = 15 minutes
  • Minimum access token time to live = 2 minutes

We can derive the following graph:

../_images/scope-graph-2.png

Fig. 110 Issuing tokens with limited scope removing scopes prematurely

As the figure above illustrates, the account_transfer scope was dropped even though 30 minutes had not elapsed since the original issuance. This is because the server won’t issue tokens with a lifetime of less than 2 minutes, in this case, and thus dropping the scope to issue a full length token (15 minutes) according to the configuration.

On the other hand, if the request for a refresh would come after 20 minutes of original issuance, then the server would issue a token, but it would be shorter than the configured access token lifetime. It would be calculated to then max length until next scope must be dropped. In this case that is 10 minutes (T+30 - T+20 = 10). Which still is greater than the minimum access token lifetime setting of 2 minutes. This is illustrated below.

../_images/scope-graph-1.png

Fig. 111 Issuing tokens with limited scope lifetimes

Required scopes

Marking a scope as required means that it is required to be included in all tokens. If a client does not include a required scope in its request for a new token, the request will be rejected.

Also, in case User Consent is enabled, a required scope will manifest itself as non-deselectable in the User Consent form.

In general, a required scope must be requested by clients, and it will always be bound to tokens issued through the Token Service’s profile.

Prefix scopes

Prefix scopes are scopes which may contain extra information attached to them. They can be useful for cases in which the access being granted by the scope should be limited to a single instance of a class of actions.

For example, a banking application may need to request permission to complete payment of a specific transaction, rather than for any transactions at all.

In this case, a prefix scope called payment_transaction: could be defined, where the ID of the particular transaction for which the grant is being given is expected to be attached to the scope itself, tying the scope permission to a particular transaction instance, rather than to just any transaction in general.

The following table demonstrates the relation between a configured prefix scope, and examples of valid scopes a client may request:

Prefix Scope Example of a valid scope
payment_transaction: payment_transaction:6949596930224
tid- tid-123456

Note

Clients may not request a scope with the exact same value as a prefix scope. So, for the second case above, requesting the tid- scope would not be allowed, but requesting the tid-0 scope would.

Once a prefix scope is issued with a token, its value cannot changed by refreshing the access token. The scope may be “dropped” from refreshed access tokens by simply not requesting it during refresh, but because the scope was granted with the initial delegation, the client may, at any time in the future, request the scope again with the same initial value (but not with a different suffix, even if the prefix is still the same).

Customizing prefix scope templates and messages

Even though the User Consent Template, by default, can display prefix scopes appropriately as long as the necessary messages are defined for them, you may want to customize the user consent page according to your needs.

See the Showing prefix scopes section for details on how to do that, as well as handling the localization of claims, including prefix scopes.

Claims of a scope

All scopes can contain a number of claims, except prefix scopes which can not contain claims.

When a scope does not include a claim, it can be marked as consentable. This means that when interactive consent is enabled for a client, the scope will show up in the list of items that a user must consent to.

When a scope includes claims, a request for that scope will translate in a request to the claims of the requested scope. As well as the other way around: when a token indicates it has the scope bound to it, then all the claims of that scope will be part of the token.

There are quite some nuances to using claims, which are explained in the rest of this section.

Claims I/O

In order to get claims included in a token, a client can request either a scope that represents a set of claims as configured, or it can request individual claims through the claims request parameter. The claims a client is allowed to request is restricted by the claims of the scopes that are configured for the client to be allowed to request.

If a client requests a scope, and all the claims of that scope are allowed (i.e. consent is given to release all the requested claims of that scope, either implicit or explicit), then a token is returned that contains both the requested scope as well as the claims of that scope. If one of the claims of the scope is not granted, then the token is issued with the allowed claims but without that scope. In other words: if a token is issued with a scope, then all the claims of the scope are also in the token.

Example of requesting a scope and its claims

Given the following configuration: The scope show_balance has claims bank_account and account_name. A client balance_shower_123 is configured with the show_balance scope.

When the client makes a request for a token through the code flow, the request URL could look like:

https://curity.example.org/authorize?response_type=code&client_id=balance_shower_123&scope=show_balance

Once the user is authenticated, and the claims are consented to, a code will be returned that the client can redeem for an access token from the token endpoint through the code flow:

POST /oauth/v2/token HTTP/1.1
Host: curity.example.org
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
client_id=balance_shower_123&
code=<the-authorization-code>

The response to this request, will be an access token, the scope that was added to the access token, as well as the claim names that are granted to the access token:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"0f16ace4-9c95-4917-b77b-28c761450328",
  "token_type":"bearer",
  "expires_in":3600,
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
  "scope":"show_balance",
  "claims":"bank_account account_name"
}

Example of requesting individual claims

Given the configuration from the previous example, the client needs an access token that contains the bank_account claim and wants to obtain it through the implicit flow. To express this, the client needs to send the claims request parameter, and creates its value like this:

Listing 194 claims request parameter
1
2
3
4
5
{
  "access_token" : {
    "bank_account" : null
  }
}

The redirect URL takes the claims parameter value, URL-encode it, and pass it in the claims request parameter. The resulting URL would look like:

https://curity.example.org/authorize?response_type=token&cliet_id=balance_shower_123&claims=%7B%0A%20%20%22access_token%22%20%3A%20%7B%0A%20%20%20%20%22bank_account%22%20%3A%20null%0A%20%20%7D%0A%7D

When the user is authenticated and the claim can be released, the redirect URI will look like:

https://balance-shower-123.example.org/callback#access_token=f421bf9a-9e60-4327-99df-42a1d2a94566&token_type=bearer&expires_in=3600&claims=bank_account

Note that no scope is returned in the response.

Claim mappers

The claims engine of Curity includes the concept of a claim mapper. The purpose of a claim mapper is to map claims by name to their usage. This is best explained by the following diagram

../_images/claims-mappers-1.png
The diagram shows that:
  • there are default mappings for Access Token, ID Token and UserInfo
  • there is a mapping for the custom usage internal_token
  • the internal_token is an access token (i.e. its purpose is access_token)

Notice that claims are divided into system claims and custom claims. A system claim is a claim that always exists and has a value that is established by the context, e.g. iat is a system claim that is assigned the issued-at value of the issued token. System claims are predefined for each token purpose and owned by the system; they can not be redefined. By default, a system claim is always included in a token, and as such it can not be requested explicitly. Custom claims on the other hand are claims that are defined by configuration and are never included in a token by default.

When a claim is allowed to be included in a token, the claims mapper will map the allowed claim to the token when the token is issued. This makes the claims mapper act as an output filter of claims, as a claim can never end up in a token that is not the target of a claims mapping.

Use claims mapper usage with claims request parameter

A claim can be requested for use with a specific token usage through the claims request parameter. For example, if a claim should be issued as part of an internal_token only, the claims parameter can be used like this:

Listing 195 claims request parameter with claims mapper usage
1
2
3
4
5
{
  "internal_token": {
    "bank_account": null
  }
}

When this request is satisfied, the bank_account claim will be issued as part of an access token with usage internal_token, but not in any other token. Even when mappings exist for that claim in other token usages.

Use custom claim usage in procedure

By default the token procedures will issue default tokens. That means that the default mappings are applied to the claims of the issued tokens. If you want to issue non-default tokens, a custom token procedure is required.

The following token procedure would issue two access tokens, one default access token, and one access token of the internal_token usage. Given the previous example, this would result in the internal_token to contain the claim bank_account, whereas the default access token will not contain this claim.

Listing 196 token procedure that issues a token for custom usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function result(context) {
    var delegationData = context.getDefaultDelegationData();
    var issuedDelegation = context.delegationIssuer.issue(delegationData);

    var accessTokenData = context.getDefaultAccessTokenData();
    var issuedAccessToken = context.accessTokenIssuer.issue(accessTokenData, issuedDelegation);

    // issue a custom access token
    var internalAccessTokenData = context.getDefaultData('internal_token')
    var issuedInternalAccessToken = context.accessTokenIssuer.issue(internalAccessTokenData, issuedDelegation);

    return {
        scope: accessTokenData.scope,
        access_token: issuedAccessToken,
        internal_access_token: issuedInternalAccessToken,
        token_type: 'bearer',
        expires_in: secondsUntil(accessTokenData.exp),
        claims: context.getAccessTokenClaimNames()
    };
}

Notice that the response will only include a claims response parameter to indicate the claims that were included in the default access token, so in this example the claims response parameter would not have a value (because the custom claim is only included in the custom internal_token access token).

Claim value providers

A claim is an attribute with a name and a value. The value of a claim is always provided by a claim value provider. Curity by default comes with a number of claim value providers, each tailor made to return values from different sources.

A claims value provider returns a map of key-to-value pairs.

Curity provides the following claims providers. New providers can be added via the plugin extensibility mechanism.

Name Description Supported attributes
Script Claims provider based on a JS procedure Dependent on the procedure implementation
Data Source Claims provider using a data source as the attribute source Dependent on the data source information
Client Certificate Claims provider using the client certificate as the attribute source
  • psd2Roles - list with the the PSD2 roles present in a qualified statement
  • subjectDn - string with the subject distinguished name
  • subjectDnAttributeValueAssertions - object with the subject distinguished name attribute-value assertions
  • subjectOrganizationIdentifier - string with the subject organization identifier
  • x5t - base64url string with the certificate SHA-1 fingerprint
  • x5t_S256 - base64url string with the certificate SHA-256 fingerprint
Authentication Subject Claims provider using the authentication subject attributes Dependent on the authentication subject attributes (e.g. userName)
Authentication Context Claims provider using the authentication context attributes

Dependent on the authenticated context attributes. Example:

  • acr - The authentication class reference used to authenticate the subject.
  • client - The requesting client. A map containing the following properties.
    • id - The ID of the client.
    • name - The friendly name of the client.
    • properties - A map of the client properties.
Account Manager Claims provider using a configured account manager Dependent on the account attributes

Configuring a claim

The concepts that have been described above are brought together in the claims configuration. Here, claims properties are set, but it is also configured how the claim value is established, as well as to what scope or scopes a claim belongs.

A claim value is sourced from a Claim value providers. This claim value provider returns a bunch of attributes. When the attribute that is returned from the claim value provider has the same name as the claim name, it can be taken as claim value without any further processing.

However, the value can be transformed by a claim transformation procedure. This procedure takes as input the attributes that the claim value provider produces as output, and returns a value that is used as the claim’s value that will be included in the token. The input attribute names must be manually defined in the claim’s configuration settings.

../_images/claim-value-transformation.png

Fig. 112 Configuring claims value transformations