Delegation Patterns for OAuth 2.0

Scott Brady
IdentityServer
Published in
8 min readOct 6, 2018

With the rising popularity of patterns such as microservices, it is becoming more and more common that the API that your client application is calling, isn’t the API that is going to be performing the requested functionality. Instead, you could be calling an API gateway.

OAuth is all about delegation. It allows a client application to ask resource owner (a user) for permission to access a protected resource (an HTTP API) on their behalf. It is a delegation protocol.

So, what happens when a client application communicates with a protected resource that itself then needs to interact with other protected resources? How do we keep this request acting on the user’s behalf? How do we do this securely without getting the user involved again?

Let’s start by looking at a typical scenario where I’ve seen this issue, and then look at some solutions presented by the community and the OAuth Working Group.

Scenario: API Gateway

Let’s look at the common architecture pattern of the API gateway. Here we have a single, public facing API that sits in front of many other APIs. It knows how to route to them, and it may be that it’s the only service that can route to them. You’ll see this kind of pattern often used when using microservices and in cloud services such as Azure Service Fabric.

I’ve also seen API gateways to protect singular APIs, hosted internally within the organization behind a firewall, where the gateway simply acts as a passthrough but will contain most of the network hardening and security features.

So, we know how to use OAuth to get an access token to talk to the API gateway, but then how do we keep acting on the user’s behalf after passing through the gateway?

Even outside of gateway scenarios, if you ever have the need for an API to call another API, then you will encounter this same issue.

Poor-man’s Delegation

The simplest thing to do is just to have the API gateway re-use the access token it receives and passes that on to an internal API.

While this gets the job done, it feels dirty.

Let’s play the authorization request through in our heads. We’re asking the user to access a particular API on their behalf, API1 (the API gateway). So API1 is the intended audience of the access token. However, we’re now using that token to call API2. This means API2 must accept tokens intended for API1, meaning that they share an audience and scopes, breaking our authorization model.

If we approach it from the other angle and say that API1 must accept tokens from API2, we’re again breaking our authorization model, but now our gateway will have to support many different audiences and scopes.

We could maybe mitigate this by saying the token has multiple audiences. However, is API1 even allowed to read the token? Could it be that this token is a structured piece of data that contains sensitive data that API1 is not allowed to read?

Even then, no matter which way you look at it, we’ve now opened API2 from being accessed directly. If it is or ever becomes publicly accessible, an access token that should be used via the gateway can now be used to directly access API2. We have the potential to bypass API1.

When using this approach, we are not really using delegation but rather impersonation.

Machine to Machine Communication with the Client Credentials Grant Type

What we could do is have API1 call our authorization server and get a new token using the client credentials grant type. This would mean the initial token authorized by the user would now be scoped to API1, but now we lose track of the delegating user.

By getting a new access token using client credentials, we are no longer acting on the user’s behalf. In this grant type, the resource owner is the client application (API1), who has an entirely different security profile than the initial user.

To make this approach work, we would have to allow API1 to access any user’s data on API2. We would also have to move API2’s user-based authorization rules to within API1. If you want to ensure that the user can only access their own data within API2, the only place with enough information to make that decision is now API1.

This might work for a high trust scenario, but as soon as you want API2 to be accessed by other applications, you would have to replicate these authorization rules within each consuming application.

Custom Delegation Grant

An approach is seen used by the community is to create a new grant type that can be used to exchange access tokens used to access API1 for a token to access API2, while still acting on the user’s behalf.

This keeps our tokens intended audience scoped to only what is necessary and keeps the delegating user intact.

This delegation protocol would rely on client authentication to remain secure. However, we are talking about our protected resources: secure APIs that should be able to keep a secret.

This process would also benefit from a per client configuration for allowed scopes. For instance, API1 can only ask to get tokens to access API2, not vice versa, and certainly not for API3, API4, etc.

The most common form of this implementation I’ve seen uses the OAuth token endpoint, with a request that looks like the following:

POST /token
Host: auth.example.com
Authorization: Basic YXBpMTpzb21lc2VjcmV0
Content-Type: application/x-www-form-urlencoded
grant_type=delegation
&scope=api2
&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJhdWQiOiJhcGkyIiwiaWF0IjoxNTE2MjM5MDIyfQ.xdnkfqb5HmXGxI_eE6AQhbykHsLS9PVXYPWhGP8q80I

With this style, we are walking the line between delegation and impersonation. While we can get a token that includes the user and API1, API2 has no way of knowing if the difference between a token issued via user interaction and a token issued as a result of delegation.

JWT Bearer Authorization Grant (RFC 7523)

From the specification, the JWT Bearer Authorization Grant is:

[A way for] a JWT Bearer Token can be used to request an access token when a client wishes to utilize an existing trust relationship, […] without a direct user-approval step at the authorization server — https://tools.ietf.org/html/rfc7523#section-1

A similar approach is defined for using SAML assertions to get access tokens in RFC 7522.

While at first seeming similar to the custom delegation approach we just discussed, the authorization grant style is not suitable for delegation in our scenario.

This is mainly because the intended audience of the JWT bearer or SAML assertion must be the authorization server. From what I’ve seen in past implementations, this authorization grant style is most suited towards swapping tokens and assertions issued by a different authorization server/identity provider, for tokens issued by our authorization server.

My other gripe with these authorization grants is that they do not require either client authentication or even a client ID. By not requiring a client identification we’re removing much of the delegation features we introduced with our custom grant type. If no identification was provided, who are we issuing the new token to? How does this affect or authorization policies within API2?

Much like our custom delegation grant type, this style also errs more towards impersonation that delegation.

And obviously, if you are using SAML 2.0 or JWT access tokens, this authorization grant type is unavailable to you. At least not in the form of a formalized specification.

OAuth 2.0 Token Exchange

The OAuth Working Group are working on a specification to formalize the above delegation scenario, currently called OAuth 2.0 Token Exchange. This draft seems to have been floating around for a while, but based on recent activity (2018), it seems to have picked up steam again.

This specification looks to achieve the same as our custom grant; however, it also takes into account a few other delegation scenarios.

So, a typical request would look like:

POST /token
Host: auth.example.com
Authorization: Basic YXBpMTpzb21lc2VjcmV0
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&resource=https://backend.example.com/api2
&subject_token=accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC
&subject_token_type=urn:ietf:params:oauth:token-type:access_token

Where the subject token is the original token, delegated to API1, and token type is the type of that token (in this case an access token).

The token response is then slightly different than what we are used to:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache, no-store
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJhdWQiOiJhcGkyIiwiaWF0IjoxNTE2MjM5MDIyfQ.xdnkfqb5HmXGxI_eE6AQhbykHsLS9PVXYPWhGP8q80I",
"issued_token_type":"urn:ietf:params:oauth:token-type:access_token",
"token_type":"Bearer", "expires_in":60
}

The issued_token_type tells API1 (now acting as a client application) that it has sent back an access token, informing it how to use the contents of the response. In this case, it’s simply to use to access a protected resource (API2) and not to read or validate.

The defined values for issued_token_type are:

  • urn:ietf:params:oauth:token-type:access_token
  • urn:ietf:params:oauth:token-type:refresh_token
  • urn:ietf:params:oauth:token-type:id_token
  • urn:ietf:params:oauth:token-type:saml1
  • urn:ietf:params:oauth:token-type:saml2
  • urn:ietf:params:oauth:token-type:jwt (just a JWT, not an access token)

This delegation flow also comes with some defined JWT claim types, most notable of which are act and may_act.

The Actor claim type (act) allows us to express that delegation has taken place by using a JWT claim set about the current actor.

{
"aud":"https://consumer.example.com",
"iss":"https://issuer.example.com",
"exp":1443904177,
"nbf":1443904077,
"sub":"user",
"act": {
"sub":"admin"
}
}

Here the initial token was issued to user, and we’re still acting on their behalf. But, by using the act claim set, we can show that admin is the current actor. At the very least this would be valuable for audit trails.

Actor claim sets can be chained (an act claim set within an act claim set) to present a clear chain of delegation; however, it’s always the top-level user and their direct actor that should be used for authorization policies.

The May Act For claim type (may_act) allows us to explicitly state who is allowed to act on someone’s behalf. For instance, if we wanted to explicitly state that admin is authorized to act on user’s behalf:

{
"aud":"https://consumer.example.com",
"iss":"https://issuer.example.com",
"exp":1443904177,
"nbf":1443904077,
"sub":"user",
"may_act": {
"sub":"admin"
}
}

This means in our scenario, the process would now look like the following, where we are getting a new token that includes information about the act of delegation.

This draft makes heavy use of the resource and audience request parameters, as well as the scope parameter; however, this is a topic for another day.

What Should I Use?

For now, I would recommend sticking with the custom delegation grant style, while making sure to keep control of what each client and protected resource can access on behalf of the user.

The OAuth Working Group are working on a formalized solution which will remove the impersonation concerns, but it seems to be a moving target for now.

Originally published at www.scottbrady91.com.

--

--

Scott Brady
IdentityServer

Identity & Access Control Lead at Rock Solid Knowledge. I specialize in Identity, Authentication, OAuth, and OpenID Connect. See scottbrady91.com for more