Concepts
Who is acting, for whom, and who said they could?
An API that receives a request from an AI agent needs three answers: which software is calling, whose authority it carries, and what that authority covers. This page explains the parties involved, the ways authority is handed on, and the claims in a token that carry the answers.
The roles
Five kinds of party. Every diagram on this site uses the same names and the same colors for them.
| Role | What it holds | What it verifies | Appears as |
|---|---|---|---|
| UserResource owner | The authority to grant: their own data and permissions. | Proves who they are to the authorization server, never to the agent. | End user, employee, approver |
| AgentClient | A client identity of its own, and whatever tokens are issued to it. | Which authorization server it is talking to, and that responses come from it. | Agent, MCP client, tool server in the middle |
| Authorization serverIssuer | Signing keys, and the record of who approved what. | The user, the client, and whether policy allows the request. Then issues a token. | Authorization server, identity provider, SPIRE server |
| Resource serverThe API being called | Nothing of the user’s or the agent’s. It only receives tokens. | Each token’s issuer, audience, scope and expiry, and proof of possession where required. | API, MCP server, remote agent, website |
| InfrastructureThe platform underneath | Knowledge of which workload is actually running, and where. | A workload’s identity, which it can attest to an authorization server. | Kubernetes, gateway, egress proxy |
The principal is whichever party’s authority a request exercises: usually the user, sometimes the agent itself. The actor is the software exercising it. Most of what goes wrong with agents comes from a resource server at the end of a chain that cannot tell the two apart, or knows neither.
What a request should be able to answer
Nine questions, and where in a token or a request each answer is carried.
| Question | Where the answer lives |
|---|---|
| Who is acting | |
| What software is acting? | client_id, the act claim, and the workload identity of the process making the call. |
| Which agent, exactly? | Often nothing. Most tokens name the application that hosts an agent, not the agent. Several current drafts are trying to fix that. |
| Does it matter who holds the token? | A bearer token can be used by anyone holding a copy. A token with a cnf claim additionally requires proof of the key it names. |
| For whom | |
| Whose authority is this? | The subject of the token: sub. A person, or the agent itself. |
| Was it meant for this resource? | The audience, aud, and whether the first two answers survive all the way to the last hop. |
| Who said they could | |
| On what grounds? | A user’s consent, an administrator’s policy, or a signed mandate. Something outside the agent said yes. |
| What is it for? | scope, authorization_details, and transaction context. |
| When does it stop? | exp, and whether anyone can revoke it sooner. |
| How fresh is it? | iat, auth_time and acr: when and how strongly the person last proved who they are. |
The rest of this site is about the mechanisms that carry those answers from where they are known to where they are needed.
Why agents are different
Agents use the same protocols as every other program. What changes is how hard they lean on parts of those protocols that used to be edge cases.
Delegation is the normal case
A web app mostly acts as itself and occasionally for a user. An agent exists to act for someone else. The distinction between “the user did this” and “software did this for the user” stops being academic.
Connections are made at runtime
Classic OAuth assumes a developer registered the client with each API ahead of time. An agent may be pointed at a server nobody involved has seen before, and must discover how to authenticate on the spot.
Calls fan out
One request becomes an agent calling tools that call services that call other agents. Authority has to follow the chain without growing along the way, and without any hop holding more than it needs.
The caller can be talked into things
An agent reads untrusted text and acts on it. A credential in its reach is a credential an injected instruction can misuse or leak. Controls that live in the prompt are suggestions; controls at the resource are enforcement.
Work outlives the session
A person approves something and closes the laptop. The agent keeps going for hours. Either it holds a long-lived grant, or it needs a way to reach the person later.
Identity has two layers
There is the workload, a process running somewhere, and there is the principal it is working for. They are proven by different credentials, and a sound design carries both.
What the API can verify
One user, one agent, one API. The credential the agent presents decides what the API can establish.
| What the API can verify | The agent presents: its own api key | The agent presents: a token alice delegated |
|---|---|---|
| Who is actingWhich software the credential was issued to | YesThe key maps to the account it was issued to. That identifies the agent only if the agent has a key of its own. | Yesclient_id names the client the token was issued to. That is the agent’s registration, which may stand for many running copies of it. |
| For whomWhich user the request is for | NoNothing the API can verify names Alice. A user ID added by the agent is only the agent’s word. | Yessub names Alice. Where a resource owner is involved, the profile says sub should identify them, and the authorization server that signed the token vouches for it. |
| Who said they couldWhat was granted for that user | NoThe key carries the access an administrator gave the agent, the same whichever user it is working for. Nothing in the request records a grant for Alice. | Yesscope and aud record what the authorization server granted after Alice approved, and where it applies. The API sees that record, not the approval itself, and it covers a scope, not each action. The profile only recommends scope, so a token may carry none. |
| ProofThat the sender is the party it was issued to | NoA key is a bearer secret. It works for anyone who has a copy. | ConditionalOnly for a sender-constrained token. A bearer token can be used by anyone holding a copy. A sender-constrained token additionally requires proof of the key it is bound to, with DPoP or mutual TLS. That proof shows the sender holds the key. It does not establish which software is running. |
| So the API can | The API can limit or refuse the agent as a whole. It cannot tell one user’s request from another’s. When the agent acts as itself | The API can use both identifiers in its access policy: for example, blocking this client while still allowing Alice to use another. How the agent gets this token |
How the API verifies
- API key
- The API looks the key up in its own records and finds the account it was issued to. Nothing is read from the key itself.
- Access token
- The API checks the token’s signature against the issuer’s published keys, and that the issuer, audience and expiry are the ones it expects. An API that cannot read the token itself can ask the authorization server about it instead (token introspection).
Every verdict in the table assumes this has been done. An unvalidated token proves nothing.
About the example token
The token in this example is a JWT access token following the JWT profile for OAuth 2.0 access tokens (RFC 9068). That profile requires sub and client_id, and recommends scope when the request asked for one.
It is abbreviated. A complete token under the profile also carries iss, exp, aud, iat and jti, and is signed.
Not every access token is a JWT, and not every JWT follows this profile. An API that asks the authorization server instead, by token introspection (RFC 7662), gets an answer in which sub, client_id and scope are all optional.
What a valid credential does not prove
- That the sender is the software named. Both are bearer credentials: anyone holding a copy can use them. A sender-constrained token additionally requires proof of the key it is bound to, with DPoP or mutual TLS; that is the fourth check above. Even then, the proof shows possession of a key. It does not establish which software is running.
- That Alice approved this particular request. The token records a scope granted earlier. A request inside that scope looks the same whether or not she would agree to it now.
- That Alice is present, or has not changed her mind. The token is good until it expires, or until the API learns it was revoked.
- Which running copy of the agent sent it.
client_idnames a registration. Many instances, or many agents hosted by one application, can share it.
Refusing and revoking
- The API’s own access policy
- API key: The API can refuse the key. Every user the agent serves is refused together, because nothing in a request tells them apart.
- Access token: The API can use both identifiers: for example, block this client while still allowing Alice to use another, or the reverse. That takes a validated token and a policy that reads both
subandclient_id. Carrying two identifiers does not do it by itself. - Revocation
- API key: Whoever issued the key can revoke it, with the same effect.
- Access token: A client asks the authorization server to revoke a token (RFC 7009). That is the authorization server’s mechanism, separate from the API’s policy.
- How the API learns of a revocation
- API key: The API looks the key up on every request, so it sees a revoked key at once.
- Access token: Local token validation alone does not reveal a later revocation. Without an additional revocation mechanism, the API may continue accepting the token until it expires. Introspection is one such mechanism. RFC 7009 also describes communication between the authorization server and the API, and short-lived tokens that limit the exposure.
Delegation or impersonation
Add a tool server between the agent and the API. The API is now the last hop of a chain: what does it know? Pick how authority is passed down and see.
| Who is the user? | No |
|---|---|
| What software is calling? | Partly |
| Was this credential meant for me? | Yes |
| Is a stolen copy useless elsewhere? | No |
| Can one user or one agent be cut off? | No |
The API sees one caller: whoever holds the key. Every user and every agent run looks the same, the key works from anywhere, and revoking it cuts off everyone at once. Still the most common setup in the wild.
| Who is the user? | Yes |
|---|---|
| What software is calling? | No |
| Was this credential meant for me? | No |
| Is a stolen copy useless elsewhere? | No |
| Can one user or one agent be cut off? | Partly |
The user is visible, but the API is accepting a token that was issued for a different service. Every hop that touched the token can replay it wherever it is accepted. This is the confused-deputy setup, and the MCP specification prohibits it.
| Who is the user? | Yes |
|---|---|
| What software is calling? | No |
| Was this credential meant for me? | Yes |
| Is a stolen copy useless elsewhere? | Partly |
| Can one user or one agent be cut off? | Partly |
Each hop gets a fresh token for the right audience. But the API believes the user is calling directly. Logs show Alice doing things at 3 a.m. that Alice never did, and policy cannot treat software differently from a person.
Token exchange →| Who is the user? | Yes |
|---|---|
| What software is calling? | Yes |
| Was this credential meant for me? | Yes |
| Is a stolen copy useless elsewhere? | Partly |
| Can one user or one agent be cut off? | Yes |
The token names both the user and the chain of software acting for them. The API can apply the user’s permissions and the agent’s limits together, and either can be revoked on its own. A stolen token is still usable until it expires, but only at this one API.
Token exchange →| Who is the user? | Yes |
|---|---|
| What software is calling? | Yes |
| Was this credential meant for me? | Yes |
| Is a stolen copy useless elsewhere? | Yes |
| Can one user or one agent be cut off? | Yes |
As above, and each token is bound to a key held by the caller. Copying a token out of a log or a model’s context gets an attacker nothing. This is where the standards are heading; few deployments are here yet.
Sender-constrained tokens →Impersonation means the downstream service cannot distinguish the software from the person. It is simple, and it is how most integrations work today. Delegation keeps both identities: the token says who it is for and, separately, who is acting. The standard mechanism is the act claim from token exchange.
The confused deputy problem is what happens without it. A service with broad authority is tricked by a caller into using that authority on the caller’s behalf. An agent that forwards tokens, or that serves many users with one identity, is a deputy waiting to be confused. Audience-restricted tokens and explicit delegation are the defenses.
With and without a human
Once it is settled whose authority the agent is using, the next question is whether that person is available when access is needed.
Someone is at the keyboard
They can be redirected to sign in and shown a consent screen. The agent ends up with a token that carries their authority, scoped to what they approved. The risk is consent fatigue and over-broad grants.
Human reachableSomeone can be asked
The agent works alone but stops for approval at specific points. The approval must reach the person on a channel the agent cannot forge, describe one concrete action, and be enforced by something other than the agent.
No humanNobody is there
The agent is the principal. Its identity should come from the platform it runs on, not from a stored secret, and its permissions are whatever an administrator granted in advance. Policy is the only control.
The trap in the middle
The dangerous configuration is the one that is neither: an unattended agent holding a person’s long-lived token. It has a human’s full authority and no human watching. If you find yourself here, narrow the scope, bind the token to a key, make the person reachable for anything irreversible, and know how you would revoke it.
What is in a token
Nine credentials you will meet. Open any claim to see which question it answers.
User access token
Authorization server → agent → API
What an agent holds after a person signs in and consents. The JWT profile (RFC 9068) is one common format; opaque tokens checked by introspection carry the same information.
{"iss": "https://auth.example",
The authorization server that issued the token. The API must check it against the issuer it trusts; an agent that talks to many servers must check it against the one it meant to use.
"sub": "user:alice",
The subject: whose authority this token carries. Here, a human. APIs apply this user’s permissions.
"client_id": "https://agent.example/client.json",
The OAuth client the token was issued to. This is usually the closest thing to “which agent” in a plain access token, and it identifies an application, not an individual agent run. With Client ID Metadata Documents the value is a URL the server can fetch to learn about the client.
"aud": "https://calendar.example/api",
The audience: the one API that should accept this token. Agents talk to many servers, so audience restriction (requested with the resource parameter) is what stops a token given to one server from being replayed at another.
"scope": "calendar.read",
What the user consented to. Coarse by design. For anything transaction-shaped, authorization_details (RFC 9396) is more precise.
"exp": 1794269400,
Expiry. Minutes, not days. A leaked short-lived token is a small incident.
"iat": 1794268800,
When the token was issued.
"jti": "8f1c9a52-…"
A unique identifier for this token, used for replay detection, revocation lists and correlating audit logs.
}Delegated token
Authorization server → tool → downstream API
The result of a token exchange. The user is still the subject; the act claim names who is acting for them. Nested act claims record the chain.
{"iss": "https://auth.example",
Issuer.
"sub": "user:alice",
Still Alice. Delegation keeps the original subject so that downstream permissions are hers.
"aud": "https://warehouse.example/api",
A new audience. The token the tool received was addressed to the tool; this one is addressed to the next hop.
"scope": "sales.read",
Narrower than the original grant. Each exchange is a chance to drop privileges the next hop does not need.
"exp": 1794269100,
Shorter-lived than the token it was exchanged for.
"act": {
The actor claim from RFC 8693. Its presence is the difference between delegation (“the tool, acting for Alice”) and impersonation (“Alice”). Without it the API cannot tell software from the person.
"sub": "https://tools.example/mcp",
The current actor: the party presenting this token.
"act": { "sub": "research-agent" }
A prior actor, nested. RFC 8693 says only the outermost actor is relevant to access control; nested actors are history, useful for audit. Several current drafts would make this chain richer for agents.
}}Agent’s own token
Authorization server → agent → API
Issued by the client credentials grant. There is no user. The agent is the subject, and everything it does with this token is on its own authority.
{"iss": "https://auth.example",
Issuer.
"sub": "invoice-agent",
The subject is the client itself. An API must not treat this as a user; RFC 9068 warns about exactly this confusion and recommends that authorization servers keep client and user identifiers from colliding.
"client_id": "invoice-agent",
Same value as sub: the token was issued to the party it describes.
"aud": "https://erp.example/api",
One API.
"scope": "invoices.read invoices.reconcile",
Granted by an administrator ahead of time. Nobody was asked at issuance.
"exp": 1794269400
No refresh token accompanies it. The agent simply requests another.
}Kubernetes ServiceAccount token
Cluster API server → kubelet → pod → external verifier
A projected, bound token: issued for one pod, with a chosen audience and a short life. It is a workload identity document, and the usual starting point for federation.
{"iss": "https://oidc.cluster.example",
The cluster’s service account issuer. External systems fetch /.well-known/openid-configuration from here to find the signing keys.
"sub": "system:serviceaccount:agents:research-agent",
Namespace and service account name. This is the string that federation trust rules match on, so the mapping from service accounts to agents is your identity model.
"aud": ["https://sts.example"],
Set per projected volume. Give each consumer its own audience. A token whose audience is the Kubernetes API server should never be sent anywhere else.
"exp": 1794272400,
The kubelet refreshes the file on disk before this passes.
"iat": 1794268800,
Issued at.
"jti": "5d2f0c0e-…",
Unique token ID, recorded in API server audit logs so a token’s use can be traced back to its issuance.
"kubernetes.io": {
Binding information. The token is valid only while the objects named here exist. Delete the pod and the token is dead, whatever exp says.
"namespace": "agents",
The namespace, repeated in structured form.
"serviceaccount": { "name": "research-agent", "uid": "…" },
Name and UID. The UID changes if the service account is deleted and recreated, which invalidates old tokens.
"pod": { "name": "research-agent-7c9f-x2x8k", "uid": "…" },
The specific pod. This is the nearest thing Kubernetes offers to “this running instance of the agent.”
"node": { "name": "node-3", "uid": "…" }
The node the pod is scheduled on.
}}JWT-SVID
SPIRE → workload → peer workload
A SPIFFE identity in JWT form, for cases where a certificate cannot reach the application. Deliberately minimal.
{"sub": "spiffe://example.org/ns/agents/sa/research-agent",
The SPIFFE ID: a trust domain (example.org) and a path that the operator chooses. Here the path mirrors the Kubernetes namespace and service account, a common convention.
"aud": ["spiffe://example.org/ns/tools/sa/search"],
Required. The workload asks for a token for a specific audience, and the receiver must reject tokens addressed to anyone else.
"exp": 1794269100,
Required and short. JWT-SVIDs are bearer tokens.
"iat": 1794268800
Issued at.
}DPoP proof
Agent → authorization server or API, on every request
Not a token that grants anything. A one-time signed statement that the sender holds the private key an access token is bound to.
// header{"typ": "dpop+jwt",
Marks this JWT as a DPoP proof so it cannot be confused with any other kind.
"alg": "ES256",
An asymmetric signature algorithm. Symmetric algorithms and none are not allowed.
"jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" }
The public key. The proof is signed with the matching private key. The server computes this key’s thumbprint and compares it with cnf.jkt in the access token.
}// payload{"jti": "e1j3V_bKic8-LAEB",
Unique per proof. Servers remember recent values and reject repeats.
"htm": "GET",
The HTTP method this proof is for.
"htu": "https://calendar.example/api/events",
The URL this proof is for, without query or fragment. A proof captured for one endpoint is useless at another.
"iat": 1794268830,
Creation time. Servers accept only a narrow window.
"ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
Hash of the access token being presented, tying this proof to that token.
}ID-JAG
Enterprise IdP → agent → the application’s authorization server
An Identity Assertion JWT Authorization Grant. Not an access token: a short-lived statement from the company identity provider that this client may get one, for this user, at that application.
// header{ "typ": "oauth-id-jag+jwt" }
An explicit type, so the grant cannot be mistaken for an ID token or an access token and replayed as one.
// payload{"iss": "https://acme.idp.example",
The enterprise identity provider. The application’s authorization server trusts it because the company’s tenant is configured for single sign-on with it.
"sub": "U019488227",
The employee, as the identity provider knows them. The receiving side maps this to its own account.
"aud": "https://auth.chat.example/",
The authorization server of the target application, not the application’s API. Only that server should accept this grant.
"resource": "https://mcp.chat.example/",
The API, here an MCP server, that the eventual access token must be restricted to.
"client_id": "f53f191f9311af35",
The client the grant was issued to, as known to the receiving authorization server. The client presenting the grant must match.
"scope": "chat.read chat.history",
What the administrator’s policy allowed, which may be less than was requested.
"jti": "9e43f81b64a33f20116179",
Single-use identifier.
"exp": 1311281970,
Minutes. The grant exists only to be exchanged immediately.
"iat": 1311280970
Issued at.
}Transaction token
Transaction token service → every workload in one call chain
Issued at the edge of a trust domain in exchange for the external access token, then passed unchanged from service to service.
// header{ "typ": "txntoken+jwt" }
Explicitly typed. Carried in the Txn-Token HTTP header, not Authorization.
// payload{"aud": "trust-domain.example",
The trust domain itself. Every workload inside may accept it; nothing outside should.
"txn": "97053963-771d-49cc-a4e3-20aad399c312",
A unique transaction identifier, the same at every hop. This is what ties the logs of a dozen services to one user request.
"sub": "d084sdrt234fsaw34tr23t",
The user or workload the transaction is for.
"req_wl": "apigateway.trust-domain.example",
The workload that requested this token, usually the gateway that received the external call.
"scope": "trade.stocks",
The purpose of the transaction. Narrower than the external token’s scope.
"rctx": { "req_ip": "69.151.72.123", "authn": "face" },
Request context: the circumstances of the original call, such as the caller’s address and how the user authenticated.
"tctx": { "action": "BUY", "ticker": "MSFT", "quantity": "100" },
Transaction context: the fixed parameters of what was asked for. Signed, so no service downstream, and no agent talked into something else, can change them.
"iat": 1686536226,
Issued at.
"exp": 1686536586
Minutes. Long enough for one call chain to finish.
}Workload Identity Token
Identity issuer → workload → any peer, always with a proof
The WIMSE working group’s workload credential. It names a workload and carries its public key. It is never valid alone.
// header{ "alg": "ES256", "typ": "wit+jwt" }
Typed. Carried in the Workload-Identity-Token header. A receiver must refuse it if it arrives as a bearer token.
// payload{"iss": "https://issuer.example.org",
Recommended for auditing. Trust comes from the pre-configured binding between a trust domain and its issuer keys, not from following this value.
"sub": "spiffe://example.org/ns/agents/sa/research-agent",
The workload identifier: a URI scoped to a trust domain. A SPIFFE ID qualifies. For an agent, this is “what is acting”; a user’s delegation travels separately.
"cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "1CXX…" } },
The workload’s public key. Each request must come with a proof signed by the matching private key: a Workload Proof Token or an HTTP Message Signature.
"jti": "x-_1CTL2cca3CSE4cwb_l",
Token identifier.
"exp": 1745512510
Short-lived and re-issued automatically. Note there is no audience: the same token is shown to every peer, which is safe only because of the key binding.
}- Who is this for?
- What software is acting?
- Where is it valid?
- What may it do?
- How is possession proven?
- For how long?
Rules of thumb
- One audience per token. A token should work at exactly one resource. Request it that way with the
resourceparameter and reject anything addressed to someone else. - Exchange, never forward. A service that needs to call another gets a new token for that hop. Passing along the one it received is how a token meant for one place ends up everywhere.
- Keep credentials out of the model’s context. Anything the model can read, an injected instruction can ask it to repeat. Tokens belong in the runtime around the model, or outside the sandbox entirely.
- Enforce at the resource. The agent asking for confirmation is a courtesy. The API refusing without a valid, specific authorization is a control.
- Prove the workload and the principal separately. Which process is calling, and whose authority it carries, are different questions with different credentials.
- Prefer identity you do not have to store. A platform-issued token that rotates itself beats a secret in an environment variable every time.
- Short lives, narrow scopes, and a way to say stop. Assume a token will leak and an agent will misbehave. Decide in advance how small the damage is and how fast you can end it.