- Mar 10
Understanding OAuth 2.0 Token Exchange: Delegation vs Impersonation
- Daniel Krzyczkowski
Modern applications rarely exist in isolation. A single user action in a web application often triggers calls across multiple services, APIs, and backend systems. In such distributed architectures, securely propagating user identity and permissions between services becomes a significant challenge.
This is where OAuth 2.0 Token Exchange becomes extremely valuable.
In this article, we will explore the problems that arise when services act on behalf of users and how the OAuth 2.0 Token Exchange specification (RFC 8693) helps address them.
The Challenges of Acting on Behalf of a User
Acting on behalf of a user is a common requirement in modern applications, especially in customer support, administration tools, and automated services. In these scenarios, a system or operator needs to access resources or perform actions as if they were the user, while still maintaining security and transparency.
However, this introduces several challenges. Systems must ensure that the original user identity remains clear, that permissions are not unintentionally escalated, and that every action can be properly audited. Without a structured mechanism, impersonation can blur accountability, complicate debugging, and create security risks.
Standards such as OAuth-based delegation mechanisms help address these issues by enabling controlled, traceable access where both the acting party and the original user are clearly represented in the authorization context. Let's discuss some challenges further.
API-to-API Calls on Behalf of the User
In modern microservice architectures, it is common for one service to call another API after receiving a user request.
For example:
A user logs into a frontend application.
The frontend calls an API gateway.
The gateway calls several backend services.
Some services may call additional APIs.
How should user identity and permissions be securely propagated across these services? A common but problematic approach is simply forwarding the original user access token across all services. While this works functionally, it introduces several security and operational risks.
Excessive Privileges with Forwarded Tokens
Forwarding user tokens across services introduces another problem: excessive privileges. The original access token often contains permissions intended only for the initial service. When that token is forwarded to downstream services, those services may unintentionally gain access to more permissions than they actually require. This violates the principle of least privilege, increasing the risk surface of the system.
Audit Trail and Traceability Issues
Forwarded tokens also complicate audit logging and traceability.
If multiple services reuse the same user token:
Logs may only show the user's identity
The actual acting service becomes invisible
It's difficult to reconstruct the chain of actions
This becomes especially problematic for:
security investigations
compliance requirements
operational debugging
What we really need is a way to:
propagate user context
limit permissions for each service
maintain traceability across systems
Real-world scenario: Support Team Members Accessing the Application in User Context
Let's discuss the scenario when support or operations teams need to investigate user's issues.
For example:
A user reports an issue with their account.
A support engineer needs to access the application as the user to diagnose the problem.
If implemented incorrectly, this can lead to serious problems:
Support engineers may gain excessive privileges.
Actions taken by support staff may appear as if they were performed by the user.
It becomes difficult to trace who actually performed an action.
Without proper identity delegation mechanisms, accountability quickly becomes blurred. This is why there is a need to clearly indicate in the context of which user the support team member is acting. The best option would be to obtain an access token to act in context of a specific user but have the possibility to include additional information who is acting on behalf of the user (like support team member).
Introducing OAuth 2.0 Token Exchange
OAuth 2.0 Token Exchange is defined in RFC 8693 and introduces a standardized way for services to exchange one security token for another. Instead of forwarding the original token, a service can request a new token specifically scoped for a downstream service.
The flow looks like this:
A service receives a user's access token.
The service calls the authorization server.
It requests a new token intended for another service from the authorization server.
The authorization server issues a new token with appropriate scope and audience.
This new token may:
have reduced permissions
be targeted to a specific API
contain additional context about the calling service
As a result, each service operates with a token tailored to its needs.
Key Concepts from RFC 8693
The token exchange specification introduces several important concepts.
Delegation vs Impersonation
When using OAuth token exchange, a service that receives a token may need to call another service using the authority of the original user. There are two fundamentally different models for doing this:
Impersonation
Delegation
Although they may appear similar at first glance, they represent very different security and identity semantics.
Impersonation
Impersonation means that the intermediary service fully assumes the identity of the user when calling downstream services. From the perspective of the downstream service the request appears to come directly from the user. The intermediary service becomes invisible in the identity chain.
Additionally, an access token looks like this:
{
"sub": "user123",
"aud": "downstream-service",
"scope": "read:documents"
}As we can see, there is no information about the previous service (API) that obtained an access token on behalf of the user.
However, impersonation can also occur when a user is authorized to act with the rights of another user, so that the target system sees the impersonated user (principal) as the actor.
In the above scenario the access token looks like this:
{
"sub": "userB",
"aud": "service-b",
"scope": "read:documents"
}The target system is not aware that it is User A performing the actions. It sees that the access token was issued for the User B. This can be a challenge if we need to keep the full audit trail and know exactly who performed specific actions in the system. This is where delegation can be helpful.
Delegation
Delegation means that a service acts on behalf of the user but retains its own identity.
The downstream service can see:
who the user is
which service or user is acting on behalf of the user
This is explicitly supported by RFC 8693: OAuth 2.0 Token Exchange through the act (actor) claim.
Additionally, an access token looks like this (in case of including the information about the delegated service acting on behalf of authenticated user:
{
"sub": "user123",
"aud": "service-b",
"scope": "read:documents",
"act": {
"sub": "service-a"
}
}We can also include the information about another user acting on behalf of another user (like in case of support team explained before):
{
"sub": "user123",
"aud": "service-b",
"scope": "read:documents",
"act": {
"sub":"support-user-123"
}
}The main difference is that in delegation, we can retain full information about the actor performing the actions, whereas in impersonation, only the impersonated identity is visible to the target system. Additionally, it is worth mentioning that multiple act (actor) claims can be nested within a JWT token. In this example we
{
"aud":"downstream-service-02",
"iss":"https://issuer.example.com",
"sub":"user@example.com",
"act":
{
"sub":"downstream-service-01",
"act":
{
"sub":"downstream-service"
}
}
}Token Exchange: Technical Flow
Let's dive a little bit more into technical details for the token exchange flow.
Typical token exchange looks like this:
POST /token
Host: auth.techmindfactory.com
Authorization: Basic cnMwODpsb25nLXNlY3VyZS1yYW5kb20tc2VjcmV0
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:tokenexchange
&resource=https://backend.example.com/api
&subject_token=accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC
&subject_token_type=urn:ietf:params:oauth:token-type:access_tokenThe above sample request contains contains only the required parameters but please keep in mind that more can be used, especially if we want to obtain an access token for delegation.
1) Client Initiates Token Exchange Request
The client (which could be a traditional OAuth client or a service acting on behalf of a user) makes an HTTP POST to the token endpoint of the authorization server using a special grant type. The request uses application/x‑www‑form‑urlencoded format.
2) Required and Optional Request Parameters
Request must contain required parameters and can be enriched with additional, optional parameters. Let's discuss them all.
Required
grant_type - must be: urn:ietf:params:oauth:grant-type:token-exchange
subject_token - a security token (e.g., access token or ID token) representing the identity on whose behalf the request is being made.
subject_token_type - identifies the type of the subject_token (e.g., access token, JWT, SAML).
Optional
actor_token - a token representing the actual acting party. This is used when doing delegation (not pure impersonation).
actor_token_type - required if actor_token is present; indicates the type of the actor token.
resource - a URI of the resource or backend service where the new token will be used. Helps the authorization server apply appropriate policy.
audience - logical name(s) of target services intended to consume the new token (analogous to resource).
scope - space‑delimited scope values indicating desired permissions in the context of the new token.
requested_token_type - optional identifier for the type of token the client wants returned. If omitted, the server decides based on context.
3) Authorization Server Processing
When the server receives the request:
It authenticates the client using normal OAuth methods (client secret, JWT client auth, etc.).
It validates the subject_token (and actor_token if present).
It applies policy based on resource/audience/scope and decides what token to issue.
This may include impersonation or delegation semantics, depending on configuration.
4) Successful Response
If the request is valid, the server returns a standard OAuth token response (JSON) containing:
access_token - the issued token for the target service
issued_token_type - token type identifier (e.g., access token)
token_type - typically "Bearer"
expires_in - lifetime of the token
scope - scope of the issued token (if different from requested)
refresh_token - (optional) a refresh token, in selected cases
To summarize, Token Exchange flow enables:
Impersonation - client uses a subject_token to request a new token that represents the subject directly at the target
Delegation - client also supplies an actor_token, allowing the issued token to contain info about both subject and actor
Practical Example: Delegated Access for Support Teams
Let's revisit the earlier scenario with delegated access for the Support Team.
Step 1 - Support Engineer Authentication
A support engineer logs into the Application using their own credentials.
The Application now knows:
the support engineer identity
their permissions (used for authorization)
Step 2 - Delegation Request
When the engineer needs to investigate a user's issue, the Application requests a delegated token via token exchange.
The request includes:
the user identity (ID token of a user)
the support engineer as the actor (access token of the support team member)
limited scope for investigation
Step 3 - Authorization Server Issues Delegated Token
The authorization server issues a new access token containing:
sub claim: the user's identifier
act claim with sub property: the support engineer's identifier
restricted permissions
Step 4 - Application Access
The application receives the delegated access token and can see both:
which user context is being accessed (using the sub claim)
which support actor initiated the request (using the act claim)
Logs now contain clear information about the performed actions:
subject: user-123
actor: support-engineer-45
scope: account.read
This ensures:
full traceability
least privilege access
secure support workflows
Now there can be a question: how does application obtained an ID Token of a user if a support team member was authenticated and logged into the application? One of the secure and modern approaches would be to use OpenID Connect Client-Initiated Backchannel Authentication (CIBA) Flow. In case of CIBA, support team member could sign in to the app and using special management tab, could indicate which user he wants to ask for approval to proceed with delegated access. In such scenario, the backend of the app could use CIBA flow to ask Authorization Server to send approval request to the user. Once user accepts, the backend could receive and ID token of the user and then use it along with an access token issued in context of support team member to obtain delegated access token to perform actions on user's behalf using the token exchange flow. This is only one possible scenario to implement such flow. The user’s ID token should never be exposed directly to the browser. It should be obtained and handled exclusively on the backend to prevent potential token leakage.
Summary
OAuth 2.0 Token Exchange solves several critical problems in distributed systems:
Secure service-to-service delegation
Fine-grained permission control
Clear actor attribution
Improved auditing and compliance
Reduced risk of token misuse
As architectures evolve toward microservices and zero-trust environments, token exchange becomes an increasingly important tool for maintaining both security and observability.