Delegation Patterns for OAuth 2.0 using Token Exchange

Scott Brady
Scott Brady
OAuth ・ Updated August 2021 30 August 2021

With the rising popularity of patterns such as microservices, it is becoming more and more common that the API your client application is calling isn’t the API that will be handling the request. Instead, you could be calling an API gateway, or an API will end up calling yet another API.

OAuth is all about delegation. It allows a client application to ask the 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 needs to interact with other protected resources? How do you keep this request acting on the user’s behalf? How do you handle this API-to-API communication securely without getting the user involved again?

The OAuth working group has solved this with OAuth token exchange (spoilers), but let’s look at some API-to-API scenarios where I’ve seen this issue in production and then look at some possible solutions before looking at token exchange.

OAuth with API gateways and microservices

Let’s look at the common architecture pattern of an API gateway. Here you have a single, public-facing API that sits in front of many other APIs. The API gateway knows how to route to them, and it might be the only service that can call some internal APIs.

You’ll see this kind of pattern often used when using microservices and in cloud services such as Azure Service Fabric. Sometimes, it will only be the API gateway that understands OAuth, with the other APIs just looking for something like a magic header. I’m not a fan of this approach, and you’ll see why throughout this article

An OAuth client application calling an API Gateway, which in turn calls many other APIs
A client application calling an API gateway, which in turn calls many other APIs.

Another scenario is an API that needs to call another API as part of a request from a client application. This API-to-API request is common for microservices.

Or maybe, the first API simply acts as a public gateway for the second API, which is hosted internally behind your organization’s firewall. Here the gateway acts as a passthrough but will contain most of the network hardening and security features.

An OAuth client application calling an API Gateway, which in turns calls one other API
A client application calling an API Gateway, which in turns calls one other API.

So, you know how to use OAuth to get an access token to talk to an API on behalf of a user, but how do you keep acting on the user’s behalf on further API calls? If you ever need an API to call another API during a user’s request, you will face this problem.

Let’s look at your options.

Poor-man’s Delegation: re-using the access token

The simplest thing to do is have the API gateway re-use the access token it receives and pass it on to the next API.

An API gateway taking the access token from the request and replaying it on it's own request, impersonating the client application

While this gets the job done, it feels dirty.

Let’s play the authorization request through in our heads. You’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, you’re now using that token to call API2. This means API2 must accept tokens intended for API1, meaning that the APIs share an audience and scopes, breaking your authorization model.

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

You could maybe mitigate this by saying the token has multiple audiences. However, is API1 even allowed to read the token? For example, this token could be a structured piece of data containing sensitive data that API1 cannot read.

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

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

Machine-to-machine authorization using OAuth client credentials

You could have API1 call your authorization server and get a new token using the client credentials grant type. This would mean the initial access token authorized by the user would now be scoped to API1, but unfortunately, it does mean that we lose track of the delegating user.

An API gateway using client credentials to talk to another API, but losing track of the user

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

To make this approach work, you would have to allow API1 to access any user’s data on API2. You 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 accessible to other applications, you would have to replicate these authorization rules within each consuming application.

Custom Delegation Grant

An approach 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 new access token that can call API2 while still acting on the user’s behalf.

An API gateway using a custom grant to swap an incoming token for a new access token, keeping track of user, calling application, and appropriate scopes

This example keeps your access token’s 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, you are talking about your 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
Content-Type: application/x-www-form-urlencoded

grant_type=delegation
&client_id=api1
&client_secret=secret
&scope=api2
&token=accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC

With this style, you are walking the line between delegation and impersonation. While you 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.

If you are using IdentityServer4, then there’s a really useful implementation of this already available on the Extension Grants section of the documentation. This shows you how to implement an extension grant validator that can start handling calls to the token endpoint using this grant type.

However, there is now a better approach, thanks to OAuth’s token exchange.

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.

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

At first, this may seem similar to the custom delegation approach we just discussed; this 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/assertions issued by a different authorization server/identity provider for tokens issued by another 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, you’re removing much of the delegation features we introduced with the custom grant type or token exchange. If no identification was provided, who are you issuing the new token to? How does this affect your authorization policies within API2?

Much like your custom delegation grant type, this style also errs more towards impersonation than delegation.

And obviously, if you are not 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, interoperable specification.

OAuth 2.0 Token Exchange

The OAuth Working Group has created a specification that formalizes the above delegation scenarios, called token exchange, RFC 8693. It is perfect for API gateways and API-to-API communication while still acting on the user’s behalf.

This specification achieves roughly the same as the custom grant above; however, it also takes into account a few other delegation and impersonation scenarios.

Token exchange request

A typical token exchange request looks like:

POST /token
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id=api1
&client_secret=secret
&scope=api2
&subject_token=accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC
&subject_token_type=urn:ietf:params:oauth:token-type:access_token

This uses a new grant type of urn:ietf:params:oauth:grant-type:token-exchange and again allows the requester to identify and authenticate themselves. Here, API1 is swapping the token it received for access to API2.

The subject token is the token sent by the client application, delegated by the user. Since this token was delegated using OAuth, the subject token type is urn:ietf:params:oauth:token-type:access_token (an access token).

Token exchange supports scoping tokens based on resource, audience, and/or scope. Check out the RFC to learn when to use one or the other.

Token exchange response

The token response for token exchange is then slightly different than what you are used to:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache, no-store

{
  "access_token":"lt6QCYJ58k44GRoCwwBPcFDPzYzHdGClhM9qCuh39DIL",
  "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. This allows API1 to understand how to use the token it has just received. In this case, it’s an access token that it can use to access a protected resource (API2).

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

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

{
  "iss":"https://auth.example.com",
  "sub":"123xyz_user",
  "client_id": "app",
  "aud":"api2",
  "scope": "api2",
  "act":
  {
    "client_id":"api1"
  },
  "exp":1443904177,
  "nbf":1443904077
}

The initial token was delegated by user, to the client application app, and you’re still acting on their behalf. But, by using the act claim set, you can show that api1 is the current actor. At the very least, this would be valuable for audit trials.

Actor claim sets can be chained (an act claim within an act claim) to present a clear chain of delegation; however, it’s always the top-level claims and the direct actor that should be used for authorization policies. The actor claim can also be used for users (e.g. an admin acting on a user’s behalf).

An API using token exchange to swap an incoming token for a new token, keeping track of user, calling application, appropriate scopes, and the delegating app

The authorized actor claim

The Authorized Actor claim type (may_act) allows you to explicitly state who is allowed to act on someone’s behalf. For instance, if you wanted to explicitly state that api1 is authorized to act on app’s behalf, then the original access token API1 receives, may look like:

{
  "iss":"https://auth.example.com",
  "sub":"123xyz_user",
  "client_id": "app",
  "aud":"api1",
  "scope": "api1",
  "may_act":
  {
    "client_id":"api1"
  },
  "exp":1443904177,
  "nbf":1443904077
}

Now, when validating a token exchange request, the authorization server can check that the subject token has this may_act and that it includes the ID of the token exchange requester.

What should I use for token exchange?

I highly recommend using OAuth’s token exchange for API-to-API delegation. Token exchange is now widely supported, and it even sees usage in commercial identity providers (e.g., Auth0’s apple code exchange flow for native apps).

I’m a big fan of zero trust, so I also recommend ensuring that all of your APIs and microservices validate access tokens. Don’t offload authentication (or authorization 😱) onto an API gateway unless you’re dealing with ancient systems that cannot support OAuth access tokens.

You should also ensure that access tokens have minimal scopes (see the principle of least privilege) and can only be used at individual resources if possible. This ensures that even if your architecture changes over time, your authorization model won’t break.

Content is licensed under CC BY 4.0. Remember, don't copy and paste code written by strangers on the internet.