OAuth 2 Access Token Usage Strategies for Multiple Resources (APIs): Part 2
In the first post of this series, “OAuth 2 Access Token Usage Strategies For Multiple Resources (APIs): Part 1,” we explored several options for using OAuth 2 access tokens with multiple back-end resources (think APIs on the same API gateway or a single consumer accessing APIs spanning multiple API providers without a common gateway) with single page applications (SPAs) or mobile applications.
In this post, we look at how scope and audience are used to describe resources and how these different options might be implemented.
Use Case: The API Gateway and a Mix of API Providers
All of the assumptions and earlier comments from the first blog post about the target use case I have in mind still apply here. I’ve chosen a specific use case that multiple clients of mine have encountered after a certain level of maturity with API gateways. I believe every shop utilizing an API gateway or just multiple APIs will eventually encounter some variation of the use case I describe here. So the discussion and potential solutions are relevant to many, but the solution for your use case will probably vary, based on your particular needs.
In my chosen scenario, where there is a common API gateway proxying requests to application-owned APIs and third-party API providers, the API gateway is playing the role of mediator between a common front-end security model and potentially multiple unique back-end security models of the varying API providers. For example, the original end user security context could be passed to the back-end API provider if that provider trusts and understands the same IdP that the API gateway does.
In another scenario, the front-end security token may need to be replaced with a back-end token (same user described, different issuer, audience, scope, etc.) that is understood by a third-party API provider. Another characteristic of this scenario is that the OAuth 2 access token presented to the API gateway (again, assume JSON Web Token; more on that in a moment) should contain the minimum information needed to securely convey the authenticated end user’s security context. Additional information can be obtained from an OpenID Connect (OIDC) UserInfo Endpoint or similar mechanism; you’ll probably be caching this information. I explore these patterns in more detail in “API Management and Perimeter Security for COTS Applications,” “Identity Propagation in an API Gateway Architecture,” “API Security vs. Web Application Security: Part 1,” and “API Security vs. Web Application Security: Part 2.”
Although my preferred approach to this security-integration pattern is to use an API gateway to implement the transformation between the front-end security model and different back-end security models, there are other ways to do it. The pattern could be embedded in the API consumer (typically, the OAuth 2 client) or calling the application itself. The logic could also be found in an application-specific API provider that integrates with third-party APIs. Regardless of how it is implemented, the discussion is still mostly relevant.
How To Describe Resource, Scope and Audience
Neither the OAuth 2 nor the OIDC specs dictate the OAuth 2 access token format; OAuth 2 describes the what (the token’s function and protocol), but not the how (token format). As far as a standardized approach to using scope and audience information with an access token, these specs simply do not address it in practical detail, though certain usage patterns are implied by the OAuth 2 spec for scope (more on that below).
For our purposes, we can assume that the access token is a standards-compliant JWT (many of the leading IdP vendors already do this), and we can also find some guidance from other sources:
The OAuth 2 spec defines a concept of scope that can be associated with access tokens, but doesn’t necessarily tell us what the scopes are meant to represent. Likewise, it doesn’t dictate how a scope is represented beyond that it is a string (sometimes it is a URI, sometimes a URN, sometimes just a word with a context-specific meaning); different IdP vendors impose different requirements on the structure.
Since the OAuth 2 spec RFC6749 doesn’t dictate the structure of the access token, it traditionally fell on the authorization server to track scopes associated with the access tokens. These scopes can be used as the basis for controlling authorization decisions on resources (including APIs). Traditionally, the OAuth 2 scopes were a mechanism meant to describe application permissions, but as OAuth 2 has become the de facto standard for API security, its use has been applied to a wide variety of situations for which it wasn’t originally intended. This has driven an expansion of what a scope could represent from an application permission to include user permissions in a variety of scenarios.
Furthermore, the OIDC spec extends the use of scopes to turn it into a mechanism for requesting collections of claims. None of the OAuth 2, OIDC or JWT specs actually require a scope claim to be present in the OAuth 2 access token, which isn’t required to be a JWT anyway. The OIDC Core spec, Section 16.8, states, “the access token should be audience and scope restricted. One way of implementing it is to include the identifier of the resource for when it was generated as audience.” That statement greatly influences the solutions presented below.
The OAuth 2 Token Exchange spec, which is still in draft status, defines a mechanism for exchanging one token (with audience A) for another (with audience B). The spec doesn’t dictate the supported token types for the input or output token, but it does call out JWT and SAML as examples in several places. The spec also describes “security tokens employing impersonation and delegation,” but that isn’t relevant to what we are talking about here. The OAuth 2 Token Exchange spec defines a resource parameter for use against the token endpoint. It also defines a scope claim (called scp) in JWT tokens that is an array of strings.
All of this provides a path forward for what we are trying to accomplish in this post.
JWT Acting as an Access Token
The JWT acting as an access token has the following properties:
Requesting these access tokens from the token endpoint can be done with:
The scopes that are requested by an application may not necessarily be granted by the identity provider. A couple of scopes are defined by OIDC specs (“openid,” “profile” and others), but generally, it is left to the OpenID provider (authorization server) vendor to put structure around how scopes are granted. This can become quite elaborate, especially if the IdP is given a concept of what the resource servers are and what scopes they are allowed to have, and a delegation relationship is defined between the application and the resource server (resources, APIs).
If mapped to a resource server, the scopes can be treated as a role and the basis of a CGA (coarse-grained authorization) decision. This CGA decision could be an RBAC or ABAC decision. All required claims to make these decisions should be included in the access token claims list, or retrievable from the IdP UserInfo Endpoint (cache any information pulled from this endpoint). In theory, this information could also be retrieved from an XACML PIP (policy information point), but that is far outside the scope of anything I’ve been focused on. Role information could also be described by a custom claim that lists the roles.
The full power of JWT acting as an OAuth 2 access token comes into focus when the scope and audience fields are used together to describe exactly what operations (scopes) the actor (application acting at the behest of the user) can perform on the resource (described in the audience field). I must admit at this point that I am turning an OAuth 2 access token into something that it wasn’t necessarily originally meant to be: a secure, end-user security-context propagation mechanism. And more than that, it is one that is based almost entirely on industry standards. I submit to you that this is not only okay, but also very useful.
Requesting Access Tokens
Today, the only standardized way for a client to specify an audience is to represent those audiences as scopes (e.g., a https://api.iyasec.io/api scope). Then, this value would either be defined as an audience in the access token or just tracked as a scope. This is vague and confuses the concepts of scope and audience as defined earlier.
It is very important to remember that since the OAuth 2 and OIDC specs do not define a required mechanism of allowing a client to specify what the access token audience should be or how it is represented, a proprietary mechanism must be developed by each IdP vendor. Whatever mechanism is chosen, it will likely match one of the approaches described in this section. You can imagine an extension to the OAuth 2 and OIDC specs some day that formalizes all of this.
Using information from the above, we can request access tokens using one of the approaches below. All of this should be done within an authentication library (used by the client, our SPA or native mobile app) that the application developer doesn’t need to worry about, other than a small amount of integration logic.
Option #1: Resource Parameter Given to OAuth 2 Token Endpoint
The call to the authorization endpoint looks similar to:
GET /oidc/authorize?
response_type=code
&Scope=openid%20profile%20email
&client_id=s6BhdRkqt3
&state=af0ifjsldkj...rtereyt
&redirect_uri=https%3A%2F%2Fapi.iyasec.io%2Fcb HTTP/1.1
The call to the token endpoint will look similar to:
POST /oidc/token HTTP/1.1
Host: api.iyasec.io
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https%3A%2F%2Fapp1.iyasec.io%2Fcb&
client_id=s6BhdRkqt3&
resource=https://api.iyasec.io&
Scope=openid%20api.iyasec.io/read%20api.iyasec.io/write
Option #2: Resource Parameter with Multiple Audience Values
The calls for this option are essentially the same as that of Option #1. The only difference is that the resource parameter must support multiple audience values. We could use a similar pattern used with the scope parameter: a space-separated list of values. So the token endpoint call would look similar to:
POST /oidc/token HTTP/1.1
Host: api.iyasec.io
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https%3A%2F%2Fcapi.iyasec.io%2Fcb&
client_id=s6BhdRkqt3&
resource=https://api.iyasec.io/api1%20https://api.iyasec.io/api2&
Scope=openid%20api.iyasec.io/read%20api.iyasec.io/write
Option #3: Use The id_token_hint Parameter with the OAuth 2 Authorization Endpoint
For Option #3, the initial end-user authentication would occur more or less the same as with Option #1. The resource parameter to the token endpoint would probably not be needed.
A new OIDC authorization code flow should be initiated in the background for each new access token that is needed. These subsequent OIDC flows can be initiated with:
GET /oidc/authorize?
response_type=code
&Scope=openid%20profile%20email
&client_id=s6BhdRkqt3
&state=af0ifjsldkj
&redirect_uri=https%3A%2F%2Fapp1.iyasec.io%2Fcb
&prompt=none
&id_token_hint=lskjdflksjdfdslk... HTTP/1.1
The id_token_hint parameter contains the ID token from the original user authentication. If the IdP doesn’t support this parameter, a security-session tracking cookie could also be used to authenticate the user. However, this is not a mechanism that is defined by the OAuth 2 or OIDC specs.
If a session timeout or other issue caused an error to be returned, the authentication library would have to handle the error and initiate an authorization code flow that the end user can use to re-enter credentials.
Again, it must be pointed out that what I’m presenting in this section is attempting to fill a gap in the original specs and is most certainly non-standard at this point. However, one can imagine that in the future, the specs could be extended to address this specific use case, but that new mechanism probably won’t exactly match the syntax I’ve laid out here.
The options above will result in an access token (JWT format) being returned that looks similar to the following:
{
"iss": "https://idp.iyasec.io",
"aud": "https://api1.iyasec.io",
"Scope": "openid api1.iyasec.io/read api1.iyasec.io/write",
"sub": "6dfc53a9-9d8f-4668-a310-861ef662d256","Scope": "openid api1.iyasec.io/read api1.iyasec.io/write",
"iat": 1484016426,
"nbf": 1484016426,
"exp": 1484020326,
"claim1": "value1",
.
.
.
}
Or
{
"iss": "https://idp.iyasec.io",
"Aud": [ "https://api1.iyasec.io",
“https://api2.iyasec.io” ],
"Scope": "openid api1.iyasec.io/read api1.iyasec.io/write",
"sub": "6dfc53a9-9d8f-4668-a310-861ef662d256","Scope": "openid api1.iyasec.io/read api1.iyasec.io/write",
"iat": 1484016426,
"nbf": 1484016426,
"exp": 1484020326,
"claim1": "value1",
.
.
.
}
Token Refresh
The token refresh grant could be extended in a similar way to obtain new access tokens with the same audience and scope information. Here, we are primarily focused on obtaining new access tokens, but depending on the implementation, a new refresh token or ID token may come back as well.
Any refresh token associated with a particular access token would need to be cached and used to obtain new refresh tokens in the future, when the current access token it belongs to expires. Once the access token is initially obtained, the refresh token grant can be used to refresh the token until the user’s security session finally expires and the user must provide credentials again.
Additional Concepts That Affect Your Approach
In this post, we’ve looked at how each of these options might be implemented. Some IdP vendors implement this functionality the way it is described here; some implement it differently. Some IdP vendors support all three of these options; some only support one. The relevant specs do not stipulate a required approach. Future versions of OAuth 2 or OpenID Connect might be more prescriptive.
In the last post in this series, we’ll explore supporting concepts that will impact how the three options are used.