Scope Best Practices

Scope Best Practices

Intro

OAuth 2.0 scopes are strings provided to APIs, so that they know whether to grant access to the type of data and operation requested, as described in the Introduction to Scopes page. OAuth standards documents do not provide instructions on how best to manage scopes though, and that is instead left to designers of each system. A common way to get started with scopes is to use a combination of the type of resource and the access required on it:

Resource TypeAccess LevelScope Value
orderreadorder_read

Most online resources do not explain how to use scopes in real world systems, where there may be many APIs. Therefore this article will show how to manage scopes at scale and avoid common problems. It will also explain some advanced ways in which scopes can be used.

Example Business Scenario

Software solutions are usually composed of multiple APIs and clients, and a number of business areas. A system that sells products to online customers might use a number of microservices called by multiple apps, targeted at different roles of user:

Example components

Scopes in Clients

Each client application is configured with particular scopes, to restrict the API resources that client can access. After a user authenticates, the client receives access tokens containing scopes, which are then sent to APIs.

During user authentication, a consent screen can be shown to end users, which can be useful for visualizing scopes and claims that will be issued by the Authorization Server. Consent screens are used in scenarios where a data owner grants third parties access to their own data, which is not the case for the example business scenario. You should understand though, that in the general case, scopes granted may be a subset of those requested by the client.

Consent to scopes

After authentication and consent, scopes are contained in or referenced by access tokens, which the client will later send to APIs. Scopes granted usually then last for an entire authenticated user session, without needing to redirect the user again, or ask for re-consent. During an authenticated user session, the client can silently refresh access tokens, and this can either use all scopes from the sign in request, or a subset of them.

Designing Scopes

Scopes for real world systems need to be designed coherently and the following sections provide some techniques which companies can consider in order to meet these goals:

  • Use conventions that people will find easy to understand and extend to new business areas
  • Ensure that scopes in each component are easy to reason about, by keeping them high level

Data Models

APIs should separate data both by business area and data sensitivity, so that different parts of the model can be exposed to different clients. A partial model for the above components is illustrated below.

Data model

Hierarchical Scopes

When data is hierarchical it usually makes sense to also use hierarchical scopes, as in the below examples, where colon characters are used to navigate to subresources:

ScopeGrants access to
orderFull information about orders, which perhaps not many clients should have access to
order:itemInformation about items within an order
order:pricePrices offered to customers based on their benefits
order:shipping:statusDetails on whether the order has been successfully delivered
order:shipping:addressInformation on where the order will be shipped

Use Least Privilege Scopes

Design clients so that they only have access to the data they need, and limit the scope to read-only access when write access is not needed. One possible convention is to make read-only access the default and then add a 'write' suffix when higher privilege is needed:

ScopeAccess Granted
orderRead only access to full order information
order:itemsRead only access to details about order items
inventory.writeThe ability to create, change or delete an entire inventory item
inventory:price.writeThe ability to change the price details for an inventory item

Avoid Scope Explosion

If not designed carefully, you can end up with a large number of scopes that are difficult to maintain over time. The most common cause of scope explosion is when client specific concerns are used in scope names, as in the following examples. This leads to scopes being duplicated one or more times:

  • inventory-supplier-2
  • order-admin-usa.write

Scopes per Component

Within each component, the scopes used should make sense to its developers, and represent that component's business areas. A moderately complex client application might interact with a number of APIs and use multiple scopes:

ScopeUsage
openidIndicates that OpenID Connect is used and that the app identifies the user
profileThe OpenID Connect profile claim which enables the app to get the logged in user's name
customer:benefitsEnd users can view their benefits based on prior purchases
inventoryEnd users can view inventory details when placing orders
orders.writeEnd users can create orders and then view or update them afterwards
shipping.writeWhen end users create orders they also provide details for a shipping record

API Authorization Using Scopes

Scopes are primarily a mechanism used by APIs to perform initial authorization checks when they are called with a valid JWT access token. Verifying scopes is only an entry-level check, and not a complete API authorization solution.

Enforcing Scopes in API Gateways

When a particular API operation is called, high level scopes can optionally be enforced at the entry point, in a Reverse Proxy or API Gateway. Typically the gateway returns 401 unauthorized if a token is expired, or 403 forbidden if a required scope is missing. This prevents obviously invalid calls from ever reaching the actual API, which reduces costs in some API architectures:

Reverse proxy scope checks

Enforcing Scopes in APIs

APIs should make code checks to verify that the required scopes are present, and return a 403 response when they are not. In the following example, the hasScope method might return true if either there is an exact match on the order:item scope, or if the parent scope of order is present:

public getOrderItems(criteria: Criteria): OrderItem[]  {

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

    return repository.getOrderItems();
}

APIs could be designed to require an additional suffix for data changing commands, in which case the code for that type of operation would look equivalent:

public void updateInventoryPrice(item: OrderItem) {

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

    return repository.createInventoryItem(item);
}

Real World Authorization

In the example business scenario, these high-level user authorization rules might need to be enforced. This can be managed using scopes, which are usually always fixed values decided at design time:

User RoleScopesAuthorization Rule
Customercustomer:benefitsA customer can view but not change their benefits
Administratorinventory.writeAn administrator can create or update inventory items and their prices
Supplier Userinventory:salesA supplier business partner is able to view reports on sales of their inventory items

Scopes are only part of real-world authorization, and a complete implementation will also need to enforce rules such as those listed below. This requires dynamic behavior based on the user who signed in:

User RoleAuthorization Rule
CustomerA customer can only view benefits and orders associated to their own user ID
CustomerCustomers with higher subscription levels can access additional inventory
AdministratorAn administrator may have access to all data, though this often involves business rules such as regional restrictions
Supplier UserA supplier business partner can only view inventory for their own company's supplier ID

The finer details of authorization should be handled by Claims, another part of the security architecture, and we will explain how to enforce this type of business rule in Claims Best Practices.

Scopes and Multiple APIs

By default, the token issued to the client can simply be forwarded to other APIs developed by the same company. This is common in a microservices architecture, and each API will only check for its own scopes. This is the simplest and most commonly used option within an organization.

Token forwarding

Downgrading Scopes via Token Exchange

In some cases, such as when sending tokens to less trusted divisions of the organization, a different Token Sharing strategy would be used. The Token Exchange flow can be used in this case, and is described in the RFC8693 proposed standard. This is illustrated below, for the call between the Orders and Shipping APIs, where scopes other than those related to shipping are removed, without changing the token's user identity or expiry.

Token exchange

The Curity Identity Server has a custom implementation to make downgrading scopes in tokens easy via a simple HTTPS call. This would require the Orders API to be configured as an OAuth client with the Token Exchange capability.

curl -X POST https://localhost:8443/oauth/v2/oauth-token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=https://curity.se/grant/accesstoken" \
    -d "client_id=orders-api" \
    -d "client_secret=EFfA7Ml9kkIqJPcFNuo-z" \
    -d "scope=payment.write" \
    -d "token=56acc3f6-b9ef-4a34-a9d4-f9d7a27a505b"

Prefix Scopes

These are a special type of scope, to enable clients to request scopes that are unknown at design time. This is managed by configuring a scope such as transaction-, where the convention is to use a trailing hyphen in the name. The client then asks for a scope containing a specific transaction ID at runtime. The scope granted is always for a concrete resource, as can be seen if the consent feature is used:

consent to prefix scopes

Prefix scopes can be useful in more advanced scenarios, such as those used in Financial-grade, though scopes should not usually identify concrete resources. Another limitation of prefix scopes is that they cannot be composed of claims.

Scopes and Time to Live

Access tokens are usually given a short expiry time such as 15 minutes. They are usually then silently refreshed within the same user session. By default the same scopes are included in all of these access tokens, and this may not be the desired behavior for high privilege scopes, such as those used for a money transfer.

The Curity Identity Server provides a feature where you can take finer control over the time to live for high privilege scopes. This works by dropping the high privilege scopes when tokens are refreshed. Further information on the details of token lifetimes are provided in the Token Service Admin Guide.

Scope expiry

If a client used access tokens with a time to live of 15 minutes, then attempted to use the above payment scope 20 minutes into a user session, a 403 error response would be received, due to the dropped scope. The client would then need to redirect the user to authenticate again, after which a new token with the high privilege scope would be issued.

Conclusion

Scopes are plain strings in OAuth 2.0 and you should design them to convey the following meaning. Aim to issue least privilege scopes to clients and do not use scopes for detailed business authorization rules:

  • Scopes control which areas of data a caller can access with a token
  • Scopes control what operations can be performed on that data

The Curity Identity Server supports many scope related features, providing a toolbox for implementing high level access control. These features can then be scaled to many APIs, requiring only simple code. A complete API authorization solution will also require you to design and use claims, as explained in Claims Best Practices.