Who is the agent acting as?
I spent a chunk of the spring looking at how agents were wired into companies' systems, for three clients, in three industries. The agents were different. The identity problem was identical.
Here is the shape. A user in the company's app asks the agent to do something. The request goes to an agent service. The agent service calls a model, decides to use a tool, and calls an internal API or an MCP server. To make that call it uses a credential. In every one of the three companies, that credential was a service account, created when the agent was set up, with a token that could do everything the agent might ever need to do, for every user.
So the CRM saw a request from agent-svc. The database saw a connection from agent-svc. The audit log said agent-svc read 400 customer records on Tuesday. Which user asked for those records, whether that user was allowed to see them, and whether the 400 was one request or forty, was not in any log, because the user's identity had been dropped at the first hop.
That is not a hypothetical concern. It is the precondition for the two most common agent incidents: an agent reading data on behalf of a user who should not have had it, and an agent being tricked, through injected text, into doing something with an access level no human in the company has.
Three questions every call needs to answer
Before the fix, the standard. For any action an agent takes against a system, the system should be able to answer:
Which human is this for. Not which service. Which person, with which permissions, in which session.
Which agent is doing it. A different agent, or a different version of the same one, is a different actor and should be distinguishable.
What was the agent allowed to do, for this task, and was this inside it. The task the user asked for defines a scope, and a call outside the scope should be refused regardless of what the user could do in general.
The service account model answers none of these. It answers "was the caller the agent", which is the least useful question.
The pieces that exist now
The good news is that this is an old problem with a new face, and the identity people have been building the pieces for a while. Three of them matter.
Token exchange, RFC 8693, is the mechanism by which a service that holds a user's token can trade it for a new token that is scoped down and that carries both the user's identity and the service's. The agent service receives the user's access token from the app, exchanges it at the identity provider for a token that says "user U, acting through agent A, permitted to do X", and uses that downstream. The downstream sees the user and the agent. The act claim in the resulting token is the delegation chain.
Resource indicators, RFC 8707, let a token be minted for one specific downstream. A token for the CRM cannot be replayed against the database, because the CRM's identifier is in the token's audience and the database checks for its own.
The MCP authorization specification, updated in 2025, made both of these the expected pattern for MCP servers. A client obtains a token for a specific server, the server validates the audience, and the spec explicitly says that servers must not accept tokens that were issued for something else. Most MCP servers in the wild still take a static API key at startup, but the protocol has the right shape, and the servers that follow it can be given per user, per task tokens.
What it looks like wired up
The flow for one of the three clients, after the change, was this:
sequenceDiagram
participant B as Browser
participant A as Agent service
participant I as Identity provider
participant M as CRM MCP server
B->>A: request + user access token
A->>I: token exchange (subject=user, actor=agent, audience=crm, scope=contacts:read)
I-->>A: scoped token (sub=user, act=agent, aud=crm)
A->>M: tool call with scoped token
M->>M: validate aud, enforce sub's permissions
M-->>A: resultThe agent service never holds a credential of its own that can reach the CRM. It holds the user's token for the duration of the request, exchanges it for the narrowest thing that can complete the task, and that is what it uses. When the request ends, the scoped token expires, usually within minutes.
On the CRM side, the MCP server checks that the token's audience is itself, that the subject is a known user, and then enforces that user's permissions the same way it would if the user had clicked in the UI. The act claim goes in the audit log. The log now says: user Priya, through the triage agent v2.3, read 12 contacts, scope contacts:read, at 14:22. That is the sentence you want to be able to write when the security team asks.
The code on the agent side is not large. The exchange is one call:
async function tokenFor(userToken: string, audience: string, scope: string) {
const res = await fetch(`${IDP}/oauth/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
subject_token: userToken,
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
actor_token: await agentIdentityToken(),
actor_token_type: "urn:ietf:params:oauth:token-type:jwt",
audience,
scope,
}),
});
if (!res.ok) throw new Error(`token exchange failed: ${res.status}`);
return (await res.json()).access_token as string;
}The actor_token is the agent's own identity, issued to the agent service by the identity provider through whatever workload identity mechanism you already have: a Kubernetes service account token federated to the provider, a SPIFFE identity, a cloud instance identity. That is how the agent proves it is the agent, and it is separate from the user proving they are the user.
Scope is the task, not the user
The part that took the most arguing was scope. The instinct is to give the exchanged token the user's full permission set, because the user could do all of that anyway. That instinct is what makes injection attacks work.
If a user with admin rights asks the agent to summarise a ticket, the agent needs tickets:read for one ticket. It does not need users:delete, even though the user has it. If the ticket contains text that tries to make the agent delete a user, the attempt should fail at the CRM with a scope error, not succeed because the human behind the agent happened to be powerful.
So the scope in the exchange is derived from the task, and the agent service has a small table that maps task types to the scopes they need. It is boring code. It is also the boundary that makes the difference between an injection being an incident and an injection being a log line that says "scope error, contacts:delete, refused".
When a task needs more than the table gives it, which happens, the agent asks. The request comes back to the user as "this task needs permission to update contacts, allow?" and the user's yes is a new exchange with a wider scope. That is consent, and it is the same shape as a mobile app asking for camera access: at the moment of need, for the specific thing, with a human in the loop.
The audit log is the point
I said the incidents were the motivation, and they are, but the thing that actually got the budget approved at all three clients was the audit log. Regulated industries have to be able to say who accessed what. "The agent did" is not an answer an auditor accepts, and neither is a service account, and the companies knew it. The identity work was the cost of being allowed to run the agent at all.
Once the delegation chain is in the token, the log writes itself, because every downstream system already logs the subject of the token it received. There is no agent specific logging to build. The user, the agent, and the scope are in the same field the systems have always logged, and the reports that compliance already runs pick them up without changes.
Where it is still rough
Not every downstream supports token exchange. Some legacy internal APIs take a single API key and that is that. For those, the pattern is a thin proxy that does the exchange and the audience check, holds the legacy key, and forwards with the user identity in a header the legacy system is taught to log. It is a compromise and it is better than the service account.
Identity providers vary in how well they implement RFC 8693. Some support it fully, some support a subset, one that I met supported it only for tokens the provider itself had issued, which broke a federation setup. Test the exchange flow against your actual provider before committing to the design.
And MCP servers from the ecosystem mostly do not validate audience yet. If you connect a third party server, assume its token handling is a static key until you have read the code, and put the proxy in front of it.
If you do one thing
Find out what credential your agent uses to reach your most sensitive system, and look at that system's access log for the agent's entries. If the entries say the agent's name and not a person's, you have the problem, and the first step is to stop the agent from holding that credential at all. Everything else follows from making the agent borrow the user's identity for the duration of a task instead of owning a permanent one of its own.