Claims Best Practices

Claims Best Practices

architect

Intro

In the Scope Best Practices article we provided 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.

In this article, we will complete our API authorization design by ensuring that each allowed API endpoint can make further restrictions on access to data. As we shall see, this often requires domain-specific data that is not included in tokens by default.

Business Scenario

Our business scenario is a system that sells products to online customers, resulting in a number of APIs and clients:

Example components

Authorization Rules

In the scopes article, we identified the following natural authorization rules based on resource ownership:

User RoleAuthorization Rule
CustomerA customer can only view orders for their own customer 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

To enforce the types of rules above, it is common to implement the following behavior using claims:

  • In requests to get collections of resources, filter on the subset of items the user in the token is allowed to access
  • In requests that reference a single resource, return a 403 forbidden response if the user does not have access to that resource

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 allow APIs to trust attributes about a user.

Domain-Specific Claims

The Authorization Server can issue its own claims, such as name and email, though, in our scenario, the APIs need to perform detailed authorization checks using these fields from the API’s data:

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

Avoid Claims in URLs or Headers

Including these values 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 could potentially exploit the below URL by replacing the customer with a different one:

  • /customers/23/orders

Claims vs. Roles

In older systems, authorization was done using Role-Based Access Control. Claims, on the other hand, is a more powerful and extensible mechanism. Claims can include one or more roles, as demonstrated by the claims we are using.

Data Storage in Microservices

For our example APIs, the order-related data 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.

In the below partial data schema, we see that orders have a User ID and that stock items are provided via supplier companies, each of which has a Partner ID. These fields would be important values when deciding who can access which data.

Microservices Data

User Data in Microservices

When designing business data, it is usual to have a users table containing a stable User ID, usually generated by the database, that can be used as a foreign key to owned resources:

  • In some systems, a user data history may exist before you integrate OAuth security.
  • The user data history will need to be extended for future transactions.

There are many ways to model users in your business data. Below, we’ll use the following example fields. Note that there is no Personally Identifiable Information (PII) here, and all PII fields are instead managed and audited by the Authorization Server.

FieldDescription
IDA generated key that can be used to store resources against users
SubjectAn immutable user id such as a GUID, provided by the Authorization Server in access tokens
CompanyIDThe company the user originates from, where applicable
RoleA name to categorize the type of user

Claims Principal Design

In APIs, the Claims Principal is an object populated from the access token claims after the JWT has been verified. Some API technology stacks will construct this object automatically.

We recommend designing a Claims Principal early for each API based on what the API will need for its authorization. The claims for our Inventory API might look like this, and other APIs may have slightly different claims:

public class InventoryApiClaimsPrincipal {

   // OAuth scope values from the access token
   String[] scopes;

   // The OAuth immutable subject claim
   String subject;

   // The User Id in the API's own data
   int userId;

   // The User Role in the API's own data
   String userRole;

   // The subscription level for a customer
   String subscriptionLevel;

   // The User's Company Id, where applicable
   int companyId;
}

When deciding which values belong in the Claims Principal, we recommend adopting a 90/10 rule 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

Array and Object Claims

In addition to the simple primitive 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 — array claims can enable this.

Using Claims to Filter Collections

Once claims are collected, you can use simple code to apply authorization rules to an API. The following method filters customer orders by their user id, which would usually be a simple SQL query:

public List<OrderItem> getOrderItems() {

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

    if (this.claimsPrincipal.hasRole(ADMIN_ROLE)) {

      return this.repository.getAllOrderItems(criteria);

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

       return this.repository.getFilteredOrderItems(this.claimsPrincipal.userId);

    } else {

      throw forbiddenError();
    }
}

Using Claims to Enforce Resource Access

When specific resources are requested, an API must either allow or deny access. It is also possible to use scopes and claims to filter fields returned in response objects.

public InventorySales getInventorySales(int id) {

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

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

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

    return sales;
}

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.

Custom Claims Design Pattern

Your Authorization Server should support issuing claims from external data sources, allowing you to include domain-specific identity fields in tokens.

Some example data sources are listed below. We will discuss the API option, since our scenario is microservice-based:

  • A Users Database
  • A Users API
  • An LDAP User Store

The below diagram illustrates typical components used when issuing custom claims, where the Authorization Server calls out to one or more microservices.

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 Received by APIs

Once implemented, your API will receive claims in a JSON payload, and the values will be deserialized into a Claims Principal. This ensures that API developers can code their authorizations simply.

{
  "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
}

Verifying OAuth Built-In Claims

To finish up our discussion on authorization, it is worth reiterating that APIs must also check some other claims from the above payload as part of their generic token validation:

  • Signature: The token’s digital signature must match its content to ensure it has not been tampered with.
  • Time: The token must be valid for use at the current time, based on the exp and nbf claim values.
  • Issuer: The token’s iss claim must match the Authorization Server’s fixed value.
  • Audience: The token’s aud claim must match a fixed value to represent a set of APIs.

This is usually done in an Authorization Filter, which runs before any API logic. The filter uses a security library to make these checks, then returns an error response with a 401 status code if any of them fail.

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, which is often useful for understanding who has been granted access to what data.

Custom Claims Tutorial

The Implementing Claims Best Practices article provides a tutorial on how to implement this pattern and meet all of the above requirements, using the Curity Identity Server.

Conclusion

Claims are assertions in access tokens that APIs can trust and use to enforce detailed authorization rules across multiple APIs in the most secure manner. In real-world systems, you will need the ability to issue claims from your own data sources.

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.

Let’s Stay in Touch!

Get the latest on identity management, API Security and authentication straight to your inbox.

Keep up with our latest articles and how-tos using RSS feeds