Delegation and impersonation are mechanisms for temporarily granting permissions to agents (either traditional automations, humans, or AI) so they can perform tasks as (or on behalf of) another user.
It’s similar to the sudo command in Unix systems. Sometimes you want to perform a task that requires privileged permissions, like installing an app. When you are using the sudo command you become (impersonate) the root user to the eyes of processes and logs, so you can run privileged tasks, and then revert to your regular user permissions when you are done.
Or, think about an AI agent accessing external resources on behalf of several users. Following AI agent security best practices, this agent should have access to resources only for the short time it takes to perform the task, and its permission level should be tied to the user requesting the task.
These are examples of impersonation and delegation, and the RFC 8693 extension to OAuth allows you to implement these flows in your applications in a standardized fashion.
In this article, we’ll:
- Discuss the differences between impersonation and delegation.
- Dive into the technical implementation details in OAuth.
- Cover some tips for developers implementing impersonation and delegation in their platforms.
How Does Delegation Differ from Impersonation?
Both impersonation and delegation are mechanisms for one actor, who already has its own identity, to act as another actor.
The difference between the two is the level of transparency:
When the actor impersonates the user:
- They identify as the user.
- They get the same permissions as the user.
- The resource servers aren’t aware of the impersonation.
An impersonation token looks like a regular access token, so you usually don’t need to adapt existing services to support an impersonation flow.
When the actor uses delegation on behalf of the user:
- They keep their identity while acting on behalf of the user.
- The other actors may be aware of the delegation chain, so they may treat it differently.
Delegation tokens also look like regular access tokens, but they contain an extra claim for the delegation chain. You can adapt your services to understand this claim, but they will work fine if you don’t; they just won’t be aware that a delegation is taking place.
So, it’s not a technical choice between impersonation and delegation. The deciding factor comes down to compliance and governance policies. Do you need the extra audit trail, and where do you need it?
Let’s dive into the technical differences now.
How Do Impersonation and Delegation Work on OAuth?
The standard way to implement impersonation and delegation in OAuth 2.0 is defined in the RFC 8693 extension. This extension covers token exchange, of which impersonation and delegation are a subset of use cases.
Under this extension, when a resource server:
- Receives a specific request.
- Acts as an OAuth client to request a new access token.
- To access another protected resource.
We’ll explore this impersonation and authorization flows with an example in which a frontend service needs to access a backend server to fulfill a user’s request:
Check out What Is OAuth? to refresh the basics of OAuth.
Note: Although the standard is open to different types of tokens, like OIDC JWTs or SAML assertions, the most common option for exchanges is JWTs. It’s what RFC 8693 uses in its examples, and it’s what we’ll use in this article.
Impersonation on OAuth
Step 1: The flow starts with the frontend.example.com resource server receiving a request from the user:
GET /resource HTTP/1.1
Host: frontend.example.com
Authorization: Bearer accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC
Step 2: It turns out that to fulfill this request, the frontend.example.com resource server needs to access the backend.example.com resource. So, the frontend requests a token exchange from the authorization server: as.example.com.
The token exchange request would look like this:
POST /token HTTP/1.1
Host: as.example.com
Authorization: Basic cnMwODpsb25nLXNlY3VyZS1yYW5kb20tc2VjcmV0
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&resource=https%3A%2F%2Fbackend.example.com%2Fapi
&subject_token=accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
The first thing we notice is that the resource server already has its own access token for the authorization server and includes it in the Authorization header.
Also, in the request body:
grant_typeindicates it’s requesting a token exchange.resourceis the service’s URI that will consume the new token.subject_tokenmatches the authorization token the user sent in the initial request.subject_token_typeindicates that the token is an OAuth access token.
The standard considers other optional fields:
- You can use the
audiencefield instead ofresourceto provide a logical name, as in SAML or OIDC resources. scopeto request a specific list of OAuth scopes for the new access token.
Note: The authorization server may require additional parameters, like client_id, in certain scenarios. We kept our examples to the spec’s minimum requirements for token exchange.
Step 3: As this authorization server issued both tokens, it knows that:
- The
subject_tokencorresponds to thebdc@example.comuser. - The token in the
Authorizationheader is from thefrontend.example.comservice.
If both the user and the resource server have access to the new resource, the authorization server will respond with a new JWT access_token:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache, no-store
{
"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjllciJ9.eyJhdWQiOiJodHRwczovL2JhY2tlbmQuZXhhbXBsZS5jb20iLCJpc3MiOiJodHRwczovL2FzLmV4YW1wbGUuY29tIiwiZXhwIjoxNDQxOTE3NTkzLCJpYXQiOjE0NDE5MTc1MzMsInN1YiI6ImJkY0BleGFtcGxlLmNvbSIsInNjb3BlIjoiYXBpIn0.40y3ZgQedw6rxf59WlwHDD9jryFOr0_Wh3CGozQBihNBhnXEQgU85AI9x3KmsPottVMLPIWvmDCMy5-kdXjwhw",
"issued_token_type":"urn:ietf:params:oauth:token-type:access_token",
"token_type":"Bearer",
"expires_in":60
}
Step 4: This new token can be used to access the backend.example.com resource.
GET /api HTTP/1.1
Host: backend.example.com
Authorization: Bearer eyJhbGciOiJFUzI1NiIsImtpZCI6IjllciJ9.eyJhdWQiOiJodHRwczovL2JhY2tlbmQuZXhhbXBsZS5jb20iLCJpc3MiOiJodHRwczovL2FzLmV4YW1wbGUuY29tIiwiZXhwIjoxNDQxOTE3NTkzLCJpYXQiOjE0NDE5MTc1MzMsInN1YiI6ImJkY0BleGFtcGxlLmNvbSIsInNjb3BlIjoiYXBpIn0.40y3ZgQedw6rxf59WlwHDD9jryFOr0_Wh3CGozQBihNBhnXEQgU85AI9x3KmsPottVMLPIWvmDCMy5-kdXjwhw
You can use jwt.io to explore the contents of the JWT’s payload:
{
"aud": "https://backend.example.com",
"iss": "https://as.example.com",
"exp": 1441917593,
"iat": 1441917533,
"sub": "bdc@example.com",
"scope": "api"
}
We can observe in the sub claim that the frontend server identifies itself as bdc@example.com, the user. This is impersonation, as the second resource server believes they are serving the original user and isn’t aware that there’s an intermediary.
Also note how the aud claim matches the resource parameter in the token request, which is the backend server’s URI.
Finally, the issuer (iss) of this new token is the as.example.com authorization server.
Delegation in OAuth
The flow for the delegation use case is quite similar, with two main differences.
First (step 2), the POST request includes an additional token from the resource server in the actor_token parameter:
POST /as/token.oauth2 HTTP/1.1
Host: as.example.com…
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&resource=https%3A%2F%2Fbackend.example.com%2Fapi
&subject_token=accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
&actor_token=cnMwODpsb25nLXNlY3VyZS1yYW5kb20tc2VjcmV0
&actor_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
The extra actor_token parameter represents the identity of the actor that will act on behalf of the subject (the user). In this case, it contains the frontend’s access token.
When the authorization server returns the new access token (step 3), it will contain an extra act claim:
{
"aud": "https://backend.example.com",
"iss": "https://as.example.com",
"exp": 1441917593,
"iat": 1441917533,
"sub": "bdc@example.com",
"act": {
"sub": "https://frontend.example.com"
},
"scope": "api"
}
We can observe that the sub claim remains as bdc@example.com.
However, the act claim now indicates that the bearer of this token is frontend.example.com acting on behalf of sub.
This is a delegation, as the backend.example.com resource server can check these claims to know the underlying actor and modify its behavior accordingly.
Read our article on token exchange for more advanced examples in which the subject and actor tokens are JWTs.
The Delegation Chain
The act claim can also hold an entire delegation chain. This materializes as nested sub and act claims.
{
"aud":"https://service26.example.com",
"iss":"https://issuer.example.com",
"exp":1443904100,
"nbf":1443904000,
"sub":"user@example.com",
"act":
{
"sub":"https://service16.example.com",
"act":
{
"sub":"https://service77.example.com"
}
}
}
In this example:
- The user initiating the request is
user@example.com. - The
service77resource server got the first request. - Then, this server delegated to
service16. - Finally,
service16can use this token to accessservice26(audclaim).
The may_act Claim
The token exchange extension does not define how the authorization server should verify that the agent has permission to act on the user’s behalf.
However, it provides an optional mechanism for the user to explicitly consent to this exchange.
If the original user access token is in JWT format (step 1), it can include a may_act claim:
{
"aud":"https://consumer.example.com",
"iss":"https://issuer.example.com",
"exp":1443904177,
"nbf":1443904077,
"sub":"user@example.com",
"may_act":
{
"sub":"admin@example.com"
}
}
In this example, user@example.com is explicitly authorizing admin@example.com to act on their behalf.
Read our article on token exchange to learn more about this spec.
Impersonation and Delegation Tips for Developers
Implementation Tips for Token Exchange
Token exchange best practices apply to both impersonation and delegation:
Use expiration dates: Although the standard does not prohibit returning long-lived tokens, it is a best practice to set the shortest expiration dates possible, adapted to the task. Exchanging a token is a lightweight operation, so you should code your clients to discard the received tokens after the task is done rather than holding onto them.
You can use IAM tools to enforce expiration dates and implement complex policies like requesting human approval or granting access based on time of day.
Use scope to limit access: When setting up your authorization server, limit the scopes of exchanged tokens to the minimum required for the task.
Implementation Tips for Delegation
We previously mentioned that supporting impersonation doesn’t require special changes on the services. However, implementing delegation can feel like playing 3D chess, with layers upon layers of identities and unexpected interactions.
Here are some tips to avoid the most common pitfalls.
Honor the act claim: Your resource server is free to base its decisions solely on the sub claim, ignoring the delegation chain. However, you should at least log the act claim for auditing purposes.
Consider also which scopes of your application shouldn’t be delegated. For example, a support human agent troubleshooting a ticket for a user, impersonating them, shouldn’t access the user’s direct messages. Restrict access to these scopes from exchanged tokens.
This best practice limits the damage a bad actor can cause if one agent is compromised.
Some authorization servers can work alongside IAM tools to implement policies like this one across all your organization.
Constrain delegation: Most impersonation use cases are straightforward. In cases like an admin user acting as a regular user, or an AI agent acting on behalf of a user, it’s usually safe for the middleman to obtain all of the user’s permissions.
However, think about a support technician solving an issue for an admin user by impersonating them. Should the technician inherit admin permissions? Probably not; it would be a security flaw enabling privilege escalation.
Identify these cases and constrain delegation so a compromised agent cannot use delegation to escalate privileges. Code could look something like:
class CombinedPermissions implements User {
let subject;
let actor;
hasPermission(permission) {
return subject.hasPermission(permission) && actor.hasPermission(permission);
}
}
A recent example of this concept, although not related to OAuth, is Kubernetes 1.36 constrained impersonation. Without this feature, a user with permission to impersonate other users was able to impersonate a cluster administrator, resulting in a privilege escalation.
Conclusion
As adoption for automation grows, machines are acting more and more on behalf of humans. With agentic AI, automation is expanding to a whole new set of tasks. A transparent model for delegation is needed now more than ever.
Luckily, the answer has been around for a while, as OAuth 2.0’s token exchange provides this model.
With impersonation, agents have access to protected resources only when needed.
And, building on that, delegation provides transparency so that applications know when they are dealing with an agent.
Frequently Asked Questions About Impersonation and Delegation
What is the difference between impersonation and delegation?
How does OAuth support impersonation and delegation?
What is the act claim in OAuth delegation?
act claim in OAuth delegation?act claim identifies the actor operating on behalf of the token subject. It can also represent a chain of delegation when multiple services act on behalf of one another.