Scaling OAuth to Many APIs

Organizations providing digital services need to expose data over the Internet. This data may be accessed by other organizations or by user-facing applications such as web and mobile apps. These days, APIs are used to enable secure access to any sensitive data related to customers or business partners.

The most powerful way to implement API security is to use the OAuth 2.0 authorization framework. This enables the same type of API credential to be issued regardless of the client calling the API. Doing so externalizes the complex security routines from API code and simplifies the code developers need to write.

The API credential is called an access token. Its permissions can be customized and locked down in many ways using 'claims', which are values contained in the token. These can include business rules and runtime context that APIs use for authorization.

In OAuth 2.0, the authorization server issues the least-privilege access tokens to clients. These are then sent to your APIs, which use claims to identify the user and ensure the correct authorization. The authorization server provides a powerful toolbox, and its claims-issuing capabilities play a key role in enabling a zero-trust API architecture.

Although this enables state-of-the-art security, it is easy for an organization to overuse OAuth features and run into problems when scaling the architecture to many APIs. This can be especially true when complex business rules are used. In this post, I will therefore describe five tips architects should consider so that the OAuth access token design scales well.

1. Always Use Scopes and Claims

An end-to-end flow looks something like this, where an authorization server is hosted alongside your APIs. It handles the lower-level security work and can be thought of as a specialist API that you plug in rather than program yourself. At the time of token issuance, a client requests scopes, and the authorization server issues the associated claims. To collect these claims, the authorization server can contact custom data sources or API endpoints when required.

curity-blog-scaling-ouaht-apis-1

Scopes are values fixed at design time and are the same for all users. Scopes enforce boundaries to restrict where an access token can be sent. Claims are then used to identify the user to APIs and for the finer-grained authorization. Claims can be assigned different values per user and provide many interesting ways to restrict tokens instead of always allowing a user's full privileges. Claims also provide a mechanism for conveying runtime context to APIs, such as the authentication strength.

2. Consider People Interaction

Yet, scopes and claims can be managed in too technical a manner. For example, if a distinct scope is used per microservice, it may result in clients needing frequent updates as new microservices are added. In the following diagram, technical responsibilities are separated across teams who are configuring business permissions as claims. These teams collaborate with a central team that manages the authorization server:

When designing scopes and claims, also consider the viewpoints around productivity, developer experience, and versioning. Aim to avoid or reduce the need for volatile business values to be continually reconfigured in the authorization server. Such values can change often or have different meanings across teams. Instead, aim for a manageable setup that would work well for 100 API teams.

curity-blog-scaling-ouaht-apis-2

3. Design for Manageability

It's a good idea to start small when designing scopes and claims. By default, consider modeling scopes on high-level business areas, and aim to only use a handful to prevent clients from accessing business areas outside their context. For claims, start with the essential identity values, including a customer ID, tenant ID, and country code. Roles are also usually good values to add to tokens, though organizing users into roles may not be possible in some use cases.

Typically, you should avoid adding the finer business permissions associated with roles to tokens. In some cases, you might also use the same scope for multiple microservices if they all operate in the same business area. This might lead to an updated flow similar to this, where the claims in tokens only use stable values:

curity-blog-scaling-ouaht-apis-3

4. Separate Authorization Logic

In APIs, it is usual to create some kind of OAuth filter, which follows JWT Security Best Practices. The overall process looks something like this, where you first ensure that the token is valid, then form a claims object, then use the claims object in business logic classes:

curity-blog-scaling-ouaht-apis-4

Finer business permissions, such as an array of branches a retail store manager can use to access data, may be best handled outside the authorization server, especially when only a single API uses those permissions. Since such values are derivable from token claims, the API can look them up from its own database, which the API trusts.

Looking up finer business permissions could be done in multiple ways. One option is to do so after the token is validated to add extra values to the claims object, which then contains 'claims' from multiple authorities. The additional values can be cached to make the lookup efficient. 

java
12345
class ClaimsPrincipal {
TokenClaims tokenClaims;
ExtraClaims extraClaims;
}

There are trade-offs to this type of solution. It requires API developers to write more code, yet it also gives API teams more independence. In some setups, it can help reduce people bottlenecks and the need to coordinate rollouts of APIs with authorization server configuration.

The result is that the API still implements zero trust. This starts with validating a JWT access token on every request. Next, the claims principal provides all the authorization information needed by the API's business logic classes. Finally, the API implements detailed claims-based authorization on every request.

5. Ensure Least Privilege

However you manage authorization values, review your security carefully. First, ensure that scopes result in a token being immediately rejected if sent to an API outside its domain. For example, a token for a marketing API should not get past an OAuth filter if sent to a finance API. Also, ensure that all core identity values are issued to access tokens, as expected in security reviews. Also, consider threats carefully, and ensure that the token never allows privilege escalation if an attacker somehow gets hold of one.

Once the basic token privileges are designed for each API, consider how scopes and claims will flow between APIs and how the user identity will remain verifiable and auditable. For further information on this topic, see these token-sharing approaches.

Finally, evaluate how you want to manage authorization at runtime, which may impact the claims design. It is possible to centralize authorization to an entitlement management system, such as Open Policy Agent. For example, a security team might need to change authorization decisions without changing the API code or the authorization server configuration. One option is to have an API send its access token to a policy decision point (PDP) to consume the access token, inspect its claims, and return an authorization result.

Conclusion

In OAuth API architectures, managing scopes and claims is a balancing act. Therefore, use the following main steps when designing your OAuth API architecture to ensure it remains scalable:

  • Keep scopes high level and avoid scope explosion.

  • Issue core fields about the user identity to access tokens consumed by APIs.

  • Use judgment when configuring business permissions in the authorization server.

  • Ensure that developers can easily work with all authorization values.

  • Learn about the advanced security patterns that may affect token designs.

Join The Discussion

Follow @curityio on Twitter

Next steps

Start Today

Ready to modernize IAM? Build security and improve ease of use to stay ahead of the competition.