Scope Best Practices

Scope Best Practices

architect

Intro

This article will drill into some design patterns for using OAuth 2.0 scopes in real world systems, and will make some recommendations for managing them at scale.

Components

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

Example components

OAuth 2.0 Scopes

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 does 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

Scope Usage in Clients

Clients request scopes whenever they get tokens from the Authorization Server, and in our business scenario this will first occur when users sign in. If required, a consent screen can be shown to end users, which can be useful for visualizing scopes and claims.

Consent to scopes

Consent screens are used in scenarios where a data owner grants third parties access to their own data, which is not the case for our business scenario. However, it is worth being aware that in the general case the scopes granted may be a subset of those requested.

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 user session without needing to redirect the user again, or ask for re-consent.

During a 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.

Scope Design Goals

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

Enforcing Scopes in API Gateways

When a particular API operation is called, high level scopes can 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:

Reverse proxy scope checks

This prevents obviously invalid calls from ever reaching the actual API, which reduces costs in some API architectures.

Enforcing Scopes in APIs

APIs can make more detailed checks on allowed scopes via code similar to the following, which allows either calls with the required scope or a parent scope:

public List<OrderItem> getOrderItems(Criteria criteria) {

    // This is satisfied by either an entire 'order' scope or a restricted 'order:item' scope
    if (!this.claimsPrincipal.hasScope("order:item"))
        throw forbiddenError();
    }

    return this.repository.getOrderItems(OrderItem);
}

Issue Least Privilege Scopes to Clients

Where possible, design clients to only have access the data they need, and limit the scope to read only access. 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

You can then use code as follows in your APIs, to deny access when a data changing scope is not present:

public void updateInventoryPrice(OrderItem item) {

    // This is satisfied by either an entire 'inventory.write' scope or a restricted 'inventory:price.write' scope
    if (!this.claimsPrincipal.hasScope("inventory:price.write"))
        throw forbiddenError();
    }

    return this.repository.createInventoryItem(item);
}

Authorization Requires More Than Scopes

Consider next the following example scopes for our business domain:

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 the authorization solution, and a complete implementation will also need to enforce rules such as these:

User RoleAuthorization Rule
CustomerA customer can only view benefits and orders associated to their own customer 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 explore this topic in Claims Best Practices.

The Curity Identity Server enables scopes to be composed of claims, so that you can elegantly combine the two concepts.

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, such as roles or usage scenarios.

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

In these examples, client concerns have leaked into the scope names, leading to duplication. In the next document, on Claims Best Practices, we will deal more elegantly with enforcing the above rules.

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

If prefix scopes are needed they are supported by the Curity Identity Server, though scopes should not usually identify concrete resources. Another limitation of prefix scopes is that they cannot be composed of claims.

Scopes per Component

Scopes used in each component should make sense to its developers. API developers will use scopes specific to their business area, whereas our Customer App might use all of the following:

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

Scopes and Downstream APIs

By default, the token issued to the client can simply be forwarded to other APIs, as 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 organisation.

Token forwarding

Downgrading Scopes via Token Exchange

In some cases, such as when sending tokens to less trusted divisions of the organisation, 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"

High Privilege Scopes and Time to Live

Access tokens are issued after authentication and should be 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 assign a scope a time to live, which works by dropping scopes when tokens are refreshed. Further information on the details of token issuing are provided in the product documentation.

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 to represent 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. See also the article on Claims Best Practices.

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