Claims Best Practices

Claims Best Practices

Intro

The Scope Best Practices article provides architectural advice on designing scopes to represent areas of data and allowed operations. The result is that an access token issued to a client cannot be replayed to API endpoints outside of the client's remit.

This article completes the API authorization design by ensuring that each allowed API endpoint can make further restrictions on access to data. This often requires domain-specific data that is not included in access tokens by default.

Example Business Scenario

The example business scenario from the scopes article is a system that sells products to online customers, resulting in a number of APIs and clients:

Example components

Claims Concepts

In the scopes article, the following example authorization rules were identified, based on resource ownership:

User RoleAuthorization Rule
CustomerA customer can only view orders for their own user ID
CustomerCustomers with a higher subscription level can access additional inventory
AdministratorAn administrator may be able to see all data, though in some cases, there could be regional restrictions
Supplier UserA supplier business partner can only view the inventory for their own company's supplier ID

These rules are dynamic in nature and depend on the user who is signed in, so they are enforced using claims. In requests to get collections of resources, the API filters on the subset of items the user in the token is allowed to access. In requests that reference a single resource to which the user is not authorized, the API returns a forbidden response.

What are Claims?

The Introduction to Claims article provides a summary on why important security related values should be provided to APIs via digitally verifiable tokens:

Claims are trusted assertions

Claims are assertions that allow APIs to trust attributes about a user.

Identifying Claims

The example authorization rules will be implemented using the following fields:

  • User ID
  • User Role
  • Customer Subscription Level
  • Supplier Company ID

Avoid Claims in URLs or Headers

Including this type of value in access tokens is a secure design, and we recommend against sending security-related identifiers in URLs or HTTP headers. A man in the middle (MITM) could potentially exploit the below URL by replacing the customer with a different one:

  • /customers/23/orders

Issuing Domain-Specific Claims

Your Authorization Server must support collecting claims from external data sources, such as a SQL or NoSQL database, an LDAP store, or an API. This enables you to include domain-specific secure values in tokens. In a microservices setup you might prefer to provide custom claims via an API, in which case the Authorization Server makes an HTTP request at the time of token issuance. Claims need to be assigned runtime values based on the user who is signing in. So, the Authorization Server sends its user attributes, then receives custom claims in the response:

Claims Pattern

Claims vs. Roles

In older systems, authorization was done using Role-Based Access Control. Claims represent a superset of roles, and this provides a more powerful and extensible mechanism. An API enforcing the example authorization rules from this article would use roles in conjunction with other claims.

Claims Design

Claims for business authorization often originate from identifiers within your business data. Order related data for the example business scenario would most commonly be stored in several separate tables or databases. Some kind of foreign key would enable owners or creators to be linked to resources where needed:

Microservices Data

In this partial data schema, orders have a User ID and inventory is provided via supplier companies, each of which has a Partner ID. These fields would be important values when deciding who can access which data.

Claims Principal

In APIs, the Claims Principal is an object populated from the access token claims after the JWT has been verified. Some technology stacks will construct this object automatically. It is recommend to design a Claims Principal early for each API, based on the fields needed for authorization. The claims for an Inventory API might look like this, and other APIs might use slightly different claims:

export class ClaimsPrincipal {

   scope: string[];

   sub: string;

   userId: number;

   userRole: string;

   subscriptionLevel: string;

   companyId: number;
}

When deciding which values belong in the Claims Principal, it is recommend to adopt a 90/10 rule and to only include frequently used claims. Identity-related values such as user and company IDs are usually always good choices:

  • 90% of API calls: A data value used frequently for authorization in most API endpoints
  • 10% of API calls: A data value used rarely, perhaps only in a single API endpoint

In addition to the simple values shown above, claims can also be arrays and objects. It is common throughout many industries to restrict data access for one or more of a user's locations, and this can be represented by array claims.

Claims Based Authorization

At runtime, your API must validate a JWT access token on every request. Tutorials are available in API Guides to show how to do this in various technologies. The JWT payload will contain data that includes the custom claims, and this will be deserialized into the Claims Principal for the current request. API developers can then code their authorizations simply and securely.

{
  "sub": "556c8ae33f8c0ffdd94a57b7eab37e9833445143cf6847357c65fcb8570413c4",
  "purpose": "access_token",
  "iss": "https://localhost:8443/oauth/v2/oauth-anonymous",
  "active": true,
  "token_type": "bearer",
  "client_id": "web-client",
  "aud": "api.company.com",
  "nbf": 1611673855,
  "scope": "openid profile userid transactions",
  "exp": 1611674155,
  "delegationId": "93f036b6-7cdc-4f9e-89a6-2ab5ec635fbc",
  "iat": 1611673855,
  "user_id": 123,
  "user_role": "customer",
  "user_subscription_level": "gold",
  "user_company_id": 0
}

Using Claims to Filter Collections

The following code filters on orders owned by the user referenced in the access token. This is done using the user ID from the business data:

public getOrderItems(): OrderItem[] {

    if (!claimsPrincipal.hasScope("order:item"))
        throw forbiddenError();
    }

    if (claimsPrincipal.hasRole(ADMIN_ROLE)) {

      return repository.getAllOrderItems(criteria);

    } else if (claimsPrincipal.hasRole(CUSTOMER_ROLE)) {

       return repository.getFilteredOrderItems(claimsPrincipal.userId);

    } else {

      throw notFoundError();
    }
}

Using Claims to Enforce Resource Access

When specific resources are requested, an API must first check whether the user should be allowed access. In this example, if a user attempts to access data from another company they will be denied access:

public getInventorySales(id: number): InventorySales {

    if (!claimsPrincipal.hasScope("inventory:sales"))
        throw forbiddenError();
    }

    const sales = this.repository.getInventorySales(id);
    
    if (claimsPrincipal.hasRole(SUPPLIER_ROLE)) {
       if (sales.partnerId != claimsPrincipal.companyId) {
           throw notFoundError();
       }
    }

    if (!claimsPrincipal.hasRole(ADMIN_ROLE) && !claimsPrincipal.hasRole(SUPPLIER_ROLE)) {
       throw notFoundError();
    }

    return sales;
}

Unauthorized Responses

The above examples use the standard 403 Forbidden HTTP status code when an access token is received with insufficient scope. When access is denied due to the values of runtime claims, you may prefer to return a 404 Not Found error, if you want the client to receive the same response for both non-existing and unauthorized resources.

Managing Authorization Policies

An alternative to implementing authorization in code is to send the Claims Principal to an Entitlement Management System, such as Open Policy Agent or Axiomatics. This provides a mechanism where a security administrator can enforce all access to sensitive data in one place.

Auditing of Scopes and Claims

The Authorization Server should record details of all scopes and claims issued, and for which clients and users. The Curity Identity Server includes these details in its audit records, to enable understanding of who has been granted access to what data.

Custom Claims Tutorial

See the Implementing Claims Best Practices tutorial and video, for a walkthrough on enabling the claims behavior from this article, using the Curity Identity Server.

Conclusion

Claims are assertions in access tokens, which APIs can trust and then use to enforce business authorization rules in the most secure manner. In real-world systems, you will need the ability to include claims from your own data sources in access tokens. The Curity Identity Server provides powerful and extensible features for managing claims. The result is that you can issue any claims you want, then use basic code to implement your API authorization.