
Scope Best Practices
On this page
What are OAuth 2.0 Scopes?
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 Type | Access Level | Scope Value |
---|---|---|
order | read | order_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: API Scopes and Clients
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:
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.
Scopes and User Consent
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.

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.
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:
Scope | Grants access to |
---|---|
order | Full information about orders, which perhaps not many clients should have access to |
order:item | Information about items within an order |
order:price | Prices offered to customers based on their benefits |
order:shipping:status | Details on whether the order has been successfully delivered |
order:shipping:address | Information 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:
Scope | Access Granted |
---|---|
order | Read only access to full order information |
order:items | Read only access to details about order items |
inventory.write | The ability to create, change or delete an entire inventory item |
inventory:price.write | The 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:
Scope | Usage |
---|---|
openid | Indicates that OpenID Connect is used and that the app identifies the user |
profile | The OpenID Connect profile claim which enables the app to get the logged in user's name |
customer:benefits | End users can view their benefits based on prior purchases |
inventory | End users can view inventory details when placing orders |
orders.write | End users can create orders and then view or update them afterwards |
shipping.write | When 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:

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 Role | Scopes | Authorization Rule |
---|---|---|
Customer | customer:benefits | A customer can view but not change their benefits |
Administrator | inventory.write | An administrator can create or update inventory items and their prices |
Supplier User | inventory:sales | A 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 Role | Authorization Rule |
---|---|
Customer | A customer can only view benefits and orders associated to their own user ID |
Customer | Customers with higher subscription levels can access additional inventory |
Administrator | An administrator may have access to all data, though this often involves business rules such as regional restrictions |
Supplier User | A 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.

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.

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:

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.

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.
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial