
backwater / Adedotun Ajibade
This blog post continues our discussion of Authorization in the API space. It will explore common authorization patterns with API Gateways and the backend API Providers. Generally, the API Gateway will apply a Coarse Grained Authorization (CGA) decision and the API Provider will implement Fine Grained Authorization decisions. This authorization model is built on top of the concept of End-to-End, Secure Identity Propagation that was introduced in an early blog post. In complex deployments, the ideas presented in “OAuth2 Access Tokens and Multiple Resources (APIs) Series” will be another important building block to what is presented here.
Assumptions
Let’s make some assumptions about how the system may be implemented:
- OAuth2 (or OpenID Connect) is being used as the security model.
- The OAuth2 Access Token is a JSON Web Token (JWT).
- An end user is being authenticated. So, not using the OAuth2 Client Credentials Grant.
- A Role-Based Access Control (RBAC) decision based on user group membership is performed at the API Gateway layer.
- Any additional authorization decision more complex than group membership will be handled at the API Provider layer.
- No delegation is being used to obtain a new downstream token. We’ll change this assumption further down.
- Given the lack of delegation, the same token presented to the API Gateway is passed to the API Provider.
API Gateway CGA Decisions
This section will explore patterns commonly seen on API Gateways for CGA decisions.
Option #1: Access Token has username (and no other useful claims)
One Claim: This scenario has an OAuth2 Access Token (again, a JWT) that only has one useful claim for authorization.
Retrieve Additional Claims: Typically, additional information will be needed to make a CGA authorization decision beyond the username (role information most likely). To obtain this information, a call to an OIDC UserInfo Endpoint, XACML Policy Information Point (PIP), or some proprietary IdP endpoint that allows user attributes to be retrieved must be called by the API Gateway.
Caching Authorization Results: Since the API Gateway will have to retrieve additional user information via a remote call, steps should be taken to ensure this performance hit doesn’t occur on every API call. Any real-world use of this approach would likely involve caching the resulting authorization decision and possibly the intermediate step(s) of caching the remote call response. This could be done by mapping these datapoints to a SHA2 hash of the caller’s access token. Care must be taken to ensure that authorization cache entries are periodically purged on a cycle that matches when OAuth2 Access Tokens would expire.
Managing Authorization Policy: Regardless of how group information is obtained, there is the additional question of how authorization policy information is provided to the API Gateway. Authorization policy maps resources (for APIs, this is API method + path) to an authorized group. This policy information could be represented as a XACML or ALFA document. There are also several proprietary mechanisms that could define this policy. Or, one could create a custom mechanism that implements the same thing. The preferred approach would be to use one of the specs: XACML or ALFA; however, using either of these specs only makes sense in large-scale deployments due to the operational overhead involved. The details of how to manage this aspect of authorization is beyond the scope of this post.
Access Token Scope: Since delegation is not being used, there is no concept of scoping (or an insufficiently coarse grained scope) being applied to tokens for the backend API Providers. This is generally a bad practice, but is very common. Recall from our assumptions above that in this scenario the token presented to the API Gateway is passed on to the backend API Provider (ie, the token is reused between the API Gateway and the API Provider).
Scopes Used instead of Groups: A variation on this RBAC theme would be to use OAuth2 Access Token scopes instead of a group information to make a CGA decision at the API Gateway layer. Some design decisions have to be made around whether OAuth2 scopes represent the an application permission or an end user permission. Generally, the usage would have to be consistent. This blog post has assumed end-user authentication is involved; so, for our discussion we’ll assume that a scope represents a permission of the user. This approach also has the benefit of tying the OAuth2-spec defined scope concept together with our RBAC CGA decisions and allows us to map those scopes (roles for our purposes) into the Swagger/OpenAPI specs.
Option #2: Access Token has username and group claims (no other useful claims)
As long as the API Gateway is only doing a CGA decision based upon group membership, this option is relatively straightforward. No remote calls are needed to obtain group information for each user and no caching of the call result is required.
Caching Authorization Results: Depending on how much overhead is associated with evaluating authorization decisions, it may still be desirable to cache the resulting decision. For example, a few years ago, I used Balana as a self-contained PDP/PEP that evaluated XACML policy files that were uploaded to Apigee in a JAR file. It had the benefit of being fully self-contained, but still took 30ms to evaluate the XACML policy files. In this situation, it made sense to cache the authorization decision. Again, it is important to periodically invalidate the cache entries soon after the Access Token expires.
The “Managing Authorization Policy” and “Access Token Scopes” comments in Option #1 also apply to here.
Option #3: Access Token has all available (known) user claims
In this scenario, there is no need for the API Gateway to go to the IdP to obtain additional claims. This essentially reduces to Option #2 at the API Gateway layer.
Everything stated for Option #2 applies here.
This approach is simple and performs well; however, it exposes additional claim information to end user and the API Consumer application. The risks associated with this can be minimized by encrypting the JWT token with JWE.
Option #4: Access Token presented to API Gateway is exchanged for a downstream token that has all required claims.
Delegation: Now, let’s violate one of the original assumptions. This option uses delegation to exchange the input Access Token for a downstream Access Token that has all claims needed for the API Gateway to make the CGA decision. If the API Provider requires additional user claims that the API Gateway can provide in the OAuth2 Access Token, then the token exchange call performed by the API Gateway is where one obtains the new downstream token.
This approach provides the API Gateway with everything it needs to make the CGA decision while also using the concept of delegation to obtain a properly scoped access token for the backend with an additional claims that may be needed by the API Provider.
API Provider Claims: So far, we’ve only discussed claims needed by the API Gateway. There is also potentially a set of claims needed by the backend API Provider that is potentially different from what the API Gateway uses. The token obtained from the delegation call should contains these claims as well so that the required claims can be passed to the API Provider.
The “Managing Authorization Policy” and “Access Token Scopes” comments in Option #1 also apply to here.
API Provider FGA Decision
This section describes patterns used for FGA decisions at the API Provider layer.
Option #1: Access Token presented to API Provider has username claim only
This is an extension of the API Gateway CGA Option #1.
Obtaining Additional Claims: If the API Gateway isn’t capable of inserting additional claims (through Option #4), then the API Provider would have to obtain any additional claims using the same options available to the API Gateway (ie, OIDC UserInfo endpoint, XACML PIP, proprietary IdP interface, etc).
Application Specific User Profile: The API Provider may also need to obtain information from a datastore to have all the information needed to make the necessary FGA decisions. Most applications of a non-trivial complexity are going to have some type of application-specific profile built into the application that contains information that wouldn’t normally be found in the IdP’s User Store.
Authorization Results Caching: All information obtained to support the FGA decision process should be cached (for a reasonable length of time) on the API Provider to improve performance on subsequent calls. It is also possible that simply caching the end result of the FGA decision process would work. It depends how long the authorization decision can be valid. The lifetime of such a decision will be dependent upon the Access Token(s) lifetime and possibly other factors.
Previous comments about Access Token Scope apply here.
There are variations of this Option in which additional claims are present in the Access Token, but in most cases, some effort must still be put into obtaining additional information.
Option #2: Access Token has all claims from the IdP needed
This could be the end result of Option #3 or Option #4 in the last section.
However, there would still be a need to obtain attributes from the application context that would not be available to the IdP at token issuance. This is the primary reason why authorization decisions cannot be exclusively addressed at the API Gateway.
Summary
This blog post post shows how a standardized set of Coarse Grained Authorization (CGA) decisions can be made for API calls with an API Gateway and Fine Grained Authorization (FGA) decisions can be made for the same API calls at the API Provider layer. These authorization patterns build on several concepts that have been introduced in my blog posts over the last few years.
Does it have to be done this way? Of course not. But, most of the systems I have designed and built over the past ten years did something close to what I describe here.
Image: backwater / Adedotun Ajibade