OAuth 2.0 Token Exchange
In this post, we explore how OAuth 2.0 token exchange enables a secure, standards-based way for services to delegate access while preserving the identity of the original subject.
Token exchange terminology
STS - Secure Token Server. A server that handles the exchange and issuance of different types of tokens. For the context of this blog, the STS is a OAuth 2.0 Authorization server that implements RFC8693
Subject Token - The token representing the identity on behalf of whom the token exchange request is being made. Typically, the subject token is an access token or ID token. However, RFC 8693 also describes refresh tokens, SAML assertions and JWTs. Which token types are supported depends on the STS implementation.
may_act claim - A claim in the subject token that denotes which client - and in case of delegation which actor - is allowed act on behalf of the subject token. Implementation details vary, and not all Authorization Servers make use of it, while others, such as PingAM, require it to allow a token exchange.
Example:
"may_act": {
"client_id": ["API-GW"],
}
Actor Token - Token denoting the identity of the party that acts on behalf of the subject. This token is required for delegation but not used for impersonation.
act claim - In token delegation, this claim in the exchanged token identifies the actor's identity and expresses that token delegation has occurred. For example, a resource server can use this claim for auditability purposes or further access decisions.
The act claim can be nested if multiple exchanges occur and thus provide a complete audit trail. However, RFC 8693 explicitly states that only the current actor MUST be considered for access decisions. Any previous actors are for information only.
Example:
"act": {
"client_id": "API-GW"
}
Background
The goal of OAuth 2.0 is to enable the delegation of access to a resource across different services. An authorization server acts as a mediator between a client, a resource owner and a resource. In today's deployment architectures, the resource a client wants to call is often not a single entity but a collection of (micro-)services. A good example is an API that consists of an API gateway and multiple internal microservices. Securing the communication between those services is often solved using OAuth 2.0 client credential grants.

The API gateway can access the private resource by getting its own access token. The audience of the token can be correctly limited to the private resource, and scopes can be limited to what is needed. On the other hand, authorisation policies would prevent the app from requesting an access token with the required audience and scope to call the private resource directly. In this way, the app is forced to use the API Gateway instead. Further security measures can then be implemented on the API Gateway to validate the app's request and protect the private resource.
However, what if the private resource needs to know the identity of the subject who initiated the request? For instance, this might be necessary because the requested resource is directly linked to
One initial solution is to simply pass the user token to the downstream services, but this causes some issues.
- The audience of the token doesn't match the individual services
- The principle of least privilege cannot be implemented. The private resource has its own scopes that need to be present in the access token, even though the app is not allowed to access the private resource directly.

OAuth 2.0 Token Exchange
The answer is an OAuth 2.0 extension, defined in RFC 8693 - OAuth 2.0 Token Exchange. The authorisation server serves as a Secure Token Service (STS) that exchanges an original token (subject token) with a new one (exchanged token) that can have different scopes, audiences and other claims but retains the original subject. At a high level, this allows a client to act on behalf of another subject.

sub
claim persists, but aud
and scope
have been changed. Implementations and support for OAuth 2.0 token exchange differ between authorisation server products. Ping AM, Ping One and OKTA, for example, all have support for token exchange. Most implementations of token exchange support ID tokens and access tokens as both the original input and exchanged output tokens. Since the main goal is to preserve the subject of an originating token, the token exchange makes the most sense if the original token was minted for a three-legged client - i.e. if the subject identifies a user identity rather than a client.
Impersonation and Delegation
There are two types of token exchanges defined in RFC 8693 - impersonation and delegation.
- Impersonation is a simple exchange where the fact that an exchange happened is transparent to the resource server. With delegation, the resource server doesn't know that client B has impersonated client A. For all intents and purposes, it deals directly with client A.
- Delegation makes the exchange explicit. It adds a layer of complexity by requiring an additional claim to state that the token had been exchanged and who the acting party is. The resulting token is a composite of the subject token A and the actor token B. When dealing with a delegated token, the resource server knows about the delegation.
Impersonation
The impersonating client completely takes on the identity of the subject. The only claim that identifies the actor is the client_id, but there is no indication that a token exchange occurred. Most claims of the subject token, save header claims like jit
, client_id
, or exp
, are copied to the exchanged token. However, the client can request an expanded or limited set of scopes or even entirely different scopes to the subject token. It can also request a different audience claim, so the exchanged token can be used with different resource servers than the subject token.

Detailed impersonation flow
Let's look at a practical end-to-end example of an impersonation flow. In this scenario, we have Alice, the resource owner, using a banking app (client). The banking app has a backend API (resource server) that, in turn, has a private account service (another resource server) that provides information about a user's account balance. The banking app uses OIDC to authenticate Alice and get an access token to access the backend API. The backend API uses token exchange to impersonate Alice and access the account service.

Step 1: Get Subject Token
When Alice accesses the banking app and wants to check her current account, the app needs to get an access token. It may do that in one of two ways - either by refreshing an existing access token if Alice logged in before - or by initiating an authorization code grant flow.

So far, this is no different than any other OIDC flow. But if we now have a look at the access token, we start seeing some changes. The token contains a may_act
claim, allowing the banking API to exchange the token and access the accounting services.
{
"sub": "Alice"
"client_id": "banking_app"
"aud": "banking_api"
"may_act": {
"client_id": "banking_api"
}
"scope": "openid banking:account"
}
Access Token containing the may_act claim. Some standard claims are omitted for better readability
The banking app now requests the account status from the banking API, providing the bearer access token in the authorization header.
Step 2: Token exchange
The banking API cannot fulfil the request on its own; it needs to request the information from the account services. To do that in Alice's name and with the correct audience claim, it exchanges the token it received using the token exchange grant.
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=banking_api
&client_secret=secret
&scope=account:read
&audience=account_services
&subject_token=YjY2nY5N2Qa2YzhhYzMjMy
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
There's quite a lot going on here, so let's have a look at each parameter:
Parameter | Description |
---|---|
grant type | urn:ietf:params:oauth:grant-type:token-exchange Identifies the token exchange |
client_id | The client ID of the client that wants to exchange the token |
client_secret | The client secret. |
scope | The scope(s) the client wants to request. |
audience | Optionally request an audience. |
subject_token | The token identifiing the subject, in this case Alice, received from the banking app |
subject_token_type | The type of the subject token.RFC8693 defines the following typesurn: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 |
The STS responds with a new access token. Note that the response is slightly different than a standard token response. The issued_token_type
let's the client know that this is an exchanged token and what type of token it is.
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "jZTZhNDdlOWVjZWZmO",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 60
}
Looking at the exchanged access token, either via introspection or decoding a JWT token, we can see that the subject is still Alice and the client is still the banking app, but the aud
and scope
claims have changed.
{
"sub": "Alice"
"client_id": "banking_api"
"aud": "account_services"
"scope": "account:read"
}
Access token claims showing the banking_api impersonating Alice
Step 3: request the resource
Now, the banking API can access the account service using the exchanged token. Account service can validate the token, ensuring it is meant for them with the audience claim. The account service can also identify Alice as the subject and apply authorization rules based on the sub
claim, ensuring only Alice's data is accessed.
The one thing the account service doesn't know is where the request originated. For all it knows, it has been accessed by the banking app. If we want to enhance the audit trail with a chain of actors or if the account services need to make further authorization decisions based on the chain of actors, we need to look into delegation.
Delegation
Delegation explicitly expresses that one identity (the actor) is acting on behalf of another identity (the subject). The exchanged token will have the subject token's sub
claim and can be manipulated in the same way as in the impersonation case. However, the exchanged token also contains an act
claim that makes the token exchange explicit and identifies the actor's identity. Resource servers can use that information to ensure a complete audit trail or to make fine-grained authorization decisions based on the act claim.

Step 1: Get the subject token
Like in the impersonation example, the banking app requests a subject token using the authorization code grant flow. Depending on implementation (like with Ping AM), the subject token must contain a may_act
claim that state that delegation to the actor is allowed. For delegation, both the client_id
and the sub
claim have to be specified in the may_act
claim. In our example, the actor is itself an OAuth 2 client, so sub
and client_id
claims are the same:
{
"sub": "Alice",
"client_id": "banking_app",
"aud": "banking_api",
"grant_type": "authorization_code",
"scope": [
"banking:account"
],
"may_act": {
"client_id": "banking_api",
"sub": "banking_api"
}
}
Subject token with may_act claim
Step 2: Get an Actor token
To identify the actor, the STS requires an actor token. This token can be an access or ID token. In our case, it is an access token that the banking API requested using the client credentials flow. The token can be requested and refreshed at any time as long as it is valid at the time of the exchange.
POST /access_token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client_id=banking_api
&client_secret=secret
&grant_type=client_credentials
&scope=account:read
Requesting an actor token
Step3: Exchange the access token
Armed with its own actor token and the subject token, the banking client can make a delegation request. In addition to the parameters already described in the impersonation example, the actor_token
and actor_token_type
parameters provide the STS with the information required for delegation.
POST /access_token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client_id=banking_api
&client_secret=secret
&grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&scope=account:read
&audience=account_services
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
The STS validates both subject and actor tokens and validates that the subject token includes the may_act
claim identifying and matching the actor token. The STS response is the same as in the impersonation flow, identifying the token exchange with the issued_token_type
claim
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "jZTZhNDdlOWVjZWZmO",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 60
}
But looking at the claims in the exchanged token reveals an additional act
claim.
{
"sub": "Alice"
"client_id": "banking_api"
"aud": "account_services"
"act": {
"client_id": "banking_app"
}
"scope": "account:read"
}
Access token with 'act' claim
This act
claim identifies the actor (or chain of actors) and indicates to the resource server that the banking API is acting on behalf of the banking app. In other words, the banking app has delegated authorization to access the account service to the banking API.
Step 4: request the resource
The banking API can now request the resource from the resource server using the exchanged token as a bearer token. The resource server - the banking services in this case - is now aware of the intermediary actor thanks to the act
claim and can take this information into account for authorisation decisions and potentially adjust its response. It can also enhance the audit logs it stores.
Potential issues with token exchange
Now that we understand what token exchange is, let's discuss some potential issues. Here are five issues with token exchange that need to be considered when architecting a solution:
Exchange overhead
The examples we looked at are simple, with only one or two token exchanges. But let's imagine a more realistic architecture with multiple layers of services, and dozens of microservices. If each service has to exchange tokens to access another service, the latency introduced by the additional network traffic and authorization overhead could severely impact an APIs response time.
Additionally, the authorization server needs to be able to cope with the additional requests So care has to be taken in how the trust boundaries are set and where exchanging tokens really brings a benefit. Caching tokens can help mitigating the overhead.
Consent
Token exchange is a machine-to-machine interaction, meaning that there is no way to ask the user for consent. Where required, the user has to be asked for consent for all interactions when the subject token is requested. A design using token exchange has to make sure that token exchange isn't causing a scope creep. This is especially the case when token exchange is used to expand the scopes for a client, rather than requesting simply the same or a subset of scopes.
Implementation complexity
Implementing token exchange leads to a more complex architecture. Clients need to support the token exchange, although most OAuth 2.0 libraries support it since it is a standards based. More of an issue is that clients need to know for which services they have to exchange tokens. The token proliferation means that clients have to be able to manage and cache potentially several tokens for each subject and need to know handle revocation and refresh for each of them.
Token revocation
Token exchange is a one time event and the exchanged token has a lifetime independent of the subject token. RFC8693 does not describe a way to revoke an exchanged access token based on the subject token. That means that even after the subject token is expired or revoked, the exchanged token may still be valid. Best practice advice is to keep lifetimes of exchanged tokens as short as possible.
Practical implementation considerations with PingAM
As with many things OAuth, the details of how exactly the RFC is implemented and what is supported depends on the provider. Most authorization server products support impersonation, it seems support for delegation is a bit rarer. Some products require the may_act claim, while others don't support it at all. So as usual, when implementing OAuth Token Exchange refer to the documentation for the specific product and when planning a new implementation where Token Exchange is a core requirement, ensure the use cases can be fully supported. Since my professional background is with PingAM, formerly ForgeRock Access Manager, I will list some considerations for this particular implementation of token exchange.
- may_act claim is required for impersonation and delegation. The claim is set by a script that can be referenced in the OAuth Client configuration of the client issuing the subject token. The
may_act
script forms a core part of the security model ensuring only whitelisted clients can exchange a token.
(function(){
var frJava = JavaImorter(org.forgerock.json.JsonValue);
var mayAct = frJava.JsonValue.json(frJava.JsonValue.object());
mayAct.put("client_id", "banking_api");
token.setMayAct(mayAct);
})();
Example OAuth 2 May Act Script for the banking_app client to allow the banking_api client to exchange its token
- PingAM doesn't implement a full STS, only access tokens and ID tokens can be exchanged. There is no provision to exchange for example a SAML assertion to OAuth. This is something to consider especially with Ping One Advanced Identity Cloud, since the on-prem dedicated STS isn't available.
- PingAM only supports exchange of tokens issued by it's own authorization server. That means exchange across trust boundaries (for example from one organisation to another) is not possible.
- The
audience
parameter to set theaud
claim as described in this blog and the RFC is not directly supported, it needs to be enabled with a short script in the access token modification script.
var requestParams = requestProperties.get("requestParams"):
if(requestParams.audience){
accessToken.setField('aud', requestParams.audience[0];
}
Example snippet for setting the aud
claim based on an audience
parameter in an Access Token Modification script
Conclusion
OAuth 2.0 Token exchange is a useful extension to the OAuth 2.0 protocol that opens up and standardises use cases that are not easily achievable with the standard protocol. It can add an extra layer of security to microservice architectures by implementing token impersonation and, with token delegation, brings a standardised solution to the act-on-behalf problem.