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.
email
email_verified
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.
When creating a new scope, the following factors need to be considered:
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.
insurance_read
books_write
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.
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.
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:
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.
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.
1 minute < 2 minutes
This gives three parameters that are considered when issuing a new token
Given the following example settings:
We can derive the following graph:
Fig. 146 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.
Fig. 147 Issuing tokens with limited scope lifetimes
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 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:
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).
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.
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.
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.
show_balance
bank_account
account_name
balance_shower_123
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:
claims
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.
The claims engine allows claims to be configured in different ways. This section explains the claim specific settings.
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
internal_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.
iat
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:
{ "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.
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).
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.
psd2Roles
subjectDn
subjectDnAttributeValueAssertions
subjectOrganizationIdentifier
x5t
x5t_S256
subjectAlternativeNames
subjectAlternativeNamesUris
subjectAlternativeNamesDnsNames
subjectAlternativeNamesEmails
subjectAlternativeNamesIpAddresses
userName
acr
client
id
name
properties
entityId
baseUrl
zone
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 that are provided to the value transformation procedure must be manually defined in the claim’s configuration settings.
Fig. 148 Configuring claims value transformations
When establishing a claim’s value, a procedure can be used to process input attributes. In case there are no input attributes defined for a claim, and the claim does not define a claim value provider, this transformation procedure is called a generator.
A value transformation procedure must be implemented using the transform method fingerprint:
transform
function transform(attributes) { // the `attributes` parameter provides the input-attributes, as defined on the claim's settings // return the claim's value }
A generator procedure must be implemented using the generator method fingerprint:
generator
function generate() { // the generate() method does not have any input, but has a procedure context to interact with // return the claim's value }
The most common claim type is described in the previous paragraph: a claim that takes its value from a Claim value providers, and can optionally be transformed by a custom procedure. However, a claim does not have to source its value from a Claim value providers: it is possible to have a claim without a claim value provider that establishes its value in a generator procedure, as described in Value transformation or generator procedure.
Another type of claim is the so-called Reference Claim. A Reference Claim defines a reference to another claim as the source of its value. Once the referenced claim value is resolved, the reference claim can optionally configure a transformation procedure to transform its value into the value to be issued as the reference claim’s value. This will create the resulting value to be issued for the reference claim.
When a value transformation procedure is defined, it will always be given the value of the referenced claim, by the name of that referenced claim, as its input, as described in Value transformation or generator procedure.
For example, when given a claim with the name first_name, that is configured with an account-manager-claims-provider type Claim value providers:, and another claim that is a reference claim called initials and references the first_name claim, we can write the following value transformation procedure on the reference claim:
first_name
account-manager-claims-provider
initials
function transform(attributes) { return attributes.first_name.split(' ').map(name => name.charAt(0).toUpperCase()).join(' ') }
Note that it is possible to reference another reference claim, up to 10 levels deep.
A Composite Claim is a claim that acts like a container for other claims, also referred to as its parts. When Curity resolves a composite claim, it resolves its parts. A Composite Claim can have another Composite Claim as one of its parts, resulting in a nested claim value structure.
For example, a Composite Claim called contact that has two other Composite Claims email and phone inside them, could look like this:
contact
phone
1 2 3 4 5 6 7 8 9 10 11
{ "contact": { "email": { "email_unverified": "teddie@unverified.example.com", "email_verified": "teddie@example.com" }, "phone": { "phone_unverified": "192837465" } } }