Custom Claims

Descope supports custom claims to restrict data based on user privileges. Ex: Displaying different data within a school application based on different types of users. Administrators, Teachers, and Students.

You can update a user's JWT with new custom claims multiple times. If a custom claim is modified and the key is not currently in the JWT, Descope will add the custom claim to the JWT. If a custom claim is modified and the key already exists, it will override the value.

Secure vs Non-Secured Custom Claims

The backend cannot trust Custom claims specified by the client SDK or API due to potential tampering and spoofing by malicious users, the lack of a verification mechanism for client-added claims, the violation of defined trust boundaries which treat client data as untrusted, inconsistent enforcement policies on accepted claims, and the foundational security principle that cautions against trusting user input. With this security in mind, custom claims added by the client SDK will be within the nsec claim in the session token JWT.

When utilizing the management SDK or Descope flows, custom claims that are added will be marked secure, and will not be within the nsec claim.

Below is an example of a session JWT with custom claims added under the nsec claim since they were added by the client.

{
  "amr": [
    "email"
  ],
  "drn": "DS",
  "exp": 1692304651,
  "iat": 1692304051,
  "iss": "P2RFvFexVaxxNFK6rhP0ePtaGfTK",
  "nsec": {
    "email": "example@email.com",
    "name": "Joe Person"
  }
  "sub": "U2RG6grrbT3REKYqk5yC4SjkMqzA",
  "tenants": {
    "T2U7vUH1NPy4JzWHruoOVIGyzYlu": {
      "permissions": [
        "AppSecEngineer",
        "Marketing",
        "Support"
      ],
      "roles": [
        "Engineering",
        "Product Manager"
      ]
    },
    "T2U7vVBqyZv6HdGtGLdnkgCbNxrC": {
      "permissions": [
        "AppSecEngineer",
        "Support"
      ],
      "roles": [
        "Support"
      ]
    }
  }
}

Using Custom Claims within Descope Flows

Descope flows can add additional custom claims to the user's JWT during the execution of a Descope flow. This can be done during the initial authentication flow, or can be done via a step-up or similar flow.

Add the Custom Claims Action

To add custom claims within your Descope flow, you can open the applicable flow to which you want to add the custom claims. Once you have opened the flow, click the add button on the bottom left, search for the action "Custom Claims," and add it to your flow.

Descope custom claims management add to flow.

Configure the Custom Claims Action

Within the custom claims simple configuration, you can add string, boolean, numerical, or dynamic values and assign to your desired keys. The dynamic values are populated from available data from Descope pertaining to the user.

You can also utilize the advanced configuration by clicking Advanced within the custom claims action. There you can work with the custom claims as a JSON object. For an example of using the advanced configuration, look at our Hasura Custom Claims article.

Descope custom claims management add to flow.

Save and Attach the action

Click done on the action and then add it after the user's been verified. In this example, we will add it just before the end of the flow.

Descope custom claims management add to flow.

Test the Descope Flow

We can then test it within the sample app on the getting started screen.

Descope custom claims management add to flow.

The advanced custom claims configured are now added to the user's session JWT.

Descope custom claims management add to flow.

Appending Items to aud Claim

The aud (Audience) claim in a JWT specifies the intended recipients of the token, ensuring the correct application or server uses it. It can be a single entity or multiple entities, defined as a string or an array of strings, to prevent the token from being accepted by unintended parties.Descope allows you to append to the aud claim by utilizing the Custom Claims action to append an item. Below is an example of appending to the aud claim using the Descope custom claims action.

Note: Validating session with aud claim needs to have the audience passed in the call.

Descope custom claims management add to flow.

The returned JWT would look similar to the below.

{
  "amr": [
    "oauth"
  ],
  "aud": [
    "xxxx",
    "something"
  ],
  "drn": "DS",
  "eml": "xxxx",
  "enabled": true,
  "exp": 1709674989,
  "iat": 1709674809,
  "iss": "xxxx",
  "name": "xxxx",
  "rexp": "2024-04-02T21:40:09Z",
  "sub": "xxxx",
  "tenants": {
    "xxxx": {}
  }
}

Update Custom Claims Utilizing Backend SDK

Install SDK

NodeJSPythonGoJavaRuby
npm i --save @descope/node-sdk
pip3 install descope
go get github.com/descope/go-sdk
// Include the following in your `pom.xml` (for Maven)
<dependency>
    <artifactId>java-sdk</artifactId>
    <groupId>com.descope</groupId>
    <version>sdk-version</version> // Check https://github.com/descope/descope-java/releases for the latest versions
</dependency>
gem install descope

Import and initialize Management SDK

NodeJSPythonGoJavaRuby
import DescopeClient from '@descope/node-sdk';

const managementKey = "xxxx"

try{
    //  baseUrl="<URL>" // When initializing the Descope clientyou can also configure the baseUrl ex: https://auth.company.com  - this is useful when you utilize CNAME within your Descope project.
    const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey });
} catch (error) {
    // handle the error
    console.log("failed to initialize: " + error)
}

// Note that you can handle async operation failures and capture specific errors to customize errors.
//     An example can be found here: https://github.com/descope/node-sdk?tab=readme-ov-file#error-handling
from descope import (
    REFRESH_SESSION_TOKEN_NAME,
    SESSION_TOKEN_NAME,
    AuthException,
    DeliveryMethod,
    DescopeClient,
    AssociatedTenant,
    RoleMapping,
    AttributeMapping
)

management_key = "xxxx"

try:
    # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com  - this is useful when you utilize CNAME within your Descope project."
    descope_client = DescopeClient(project_id='__ProjectID__', management_key=management_key)
except Exception as error:
    # handle the error
    print ("failed to initialize. Error:")
    print (error)
import "github.com/descope/go-sdk/descope"
import "github.com/descope/go-sdk/descope/client"
import "fmt"

// Utilizing the context package allows for the transmission of context capabilities like cancellation
//      signals during the function call. In cases where context is absent, the context.Background()
//      function serves as a viable alternative.
//      Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
import (
	"context"
)

managementKey = "xxxx"

// DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com  - this is useful when you utilize CNAME within your Descope project.
descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey})
if err != nil {
    // handle the error
    log.Println("failed to initialize: " + err.Error())
}
import com.descope.client;

// Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY)
var descopeClient = new DescopeClient();

// ** Or directly **
var descopeClient = new DescopeClient(Config.builder()
        .projectId("__ProjectID__")
        .managementKey("management-key")
        .build());
require 'descope'


descope_client = Descope::Client.new(
  {
    project_id: '__ProjectID__',
    management_key: 'management_key'
  }
)

Update Custom Claims

NodeJSPythonGoJava
// Args:
//   jwt: the current/original session jwt of the User
const jwt = "xxxxxx"
//   customClaims (dict): Custom claims to add to JWT, system claims will be filtered out
const customClaims = {"custom-key1": "custom-value1", "custom-key2": "custom-value2"}

const resp = await descopeClient.management.jwt.update(jwt, customClaims)
if (!resp.ok) {
  console.log("Failed to update jwt.")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully updated jwt.")
  console.log(resp.data.jwt)
}
# Args:
#   jwt: the current/original session jwt of the User
jwt = "xxxxxx"
#   custom_claims (dict): Custom claims to add to JWT, system claims will be filtered out
custom_claims = {"custom-key1": "custom-value1", "custom-key2": "custom-value2"}

try:
  resp = descope_client.mgmt.jwt.update_jwt(jwt=jwt, custom_claims=custom_claims)
  print("Successfully updated jwt.")
  print(resp)
except AuthException as error:
  print ("Unable to update jwt.")
  print ("Status Code: " + str(error.status_code))
  print ("Error: " + str(error.error_message))
// Args:
//   ctx: context.Context - Application context for the transmission of context capabilities like
//        cancellation signals during the function call. In cases where context is absent, the context.Background()
//        function serves as a viable alternative.
//        Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
ctx := context.Background()
//   jwt: the current/original session jwt of the User
jwt := "xxxxxx"
//   customClaims (map[string]): Custom claims to add to JWT, system claims will be filtered out
customClaims := map[string]any{"custom-key1": "custom-value1", "custom-key2": "custom-value2"}

res, err := descopeClient.Management.JWT().UpdateJWTWithCustomClaims(ctx, jwt, customClaims)
if  (err != nil){
  fmt.Println("Unable to update jwt.", err)
} else {
  fmt.Println("Successfully updated jwt.", res)
}
JwtService jwts = descopeClient.getManagementServices().getJwtService();
try {
    String res = jwts.updateJWTWithCustomClaims("original-jwt",
            new HashMap<String, Object>() {{
                put("custom-key1", "custom-value1");
                put("custom-key2", "custom-value2");
            }}).getJwt();
} catch (DescopeException de) {
    // Handle the error
}

Access Keys

Sometimes, communication between machines that use access keys requires passing custom parameters or tagging purposes. Custom claims can also be added to an access key for those reasons. As previously discussed - secure vs. non-secured can be practiced here to read further about using custom claims for access keys click here.