How agents authenticate each other
When agents call other agents and tools on your behalf, a claimed name is not security. Learn machine identity, OAuth, mTLS, scoped authorization, delegation, and confused-deputy defenses for multi-agent systems.
What you'll learn
- Why agent-to-agent trust requires verifiable identity and request authentication
- How OAuth client credentials, mTLS, workload identity, and A2A discovery fit together
- How scopes, audiences, resources, and amount limits enforce least privilege
- How delegated authority prevents confused deputies when an agent acts for a user
- How to diagnose expired tokens, wrong audiences, replay, and over-broad permissions
Before you start
At 3:02 a.m., Maya’s expense agent calls the billing agent to refund a train ticket. The request says:
agent_name = billing-helper
refund invoice-481 for $480
The billing service accepts it. An attacker changes agent_name to
billing-helper and sends a refund request of their own. If the service trusts
the name, the attacker gets the same result.
Now consider the less obvious case. The genuine expense agent is compromised. It presents a valid credential and asks the billing agent to refund $60,000 to a new bank account. A valid identity does not make that request legitimate.
When agents call other agents and tools—over A2A, MCP, or plain HTTP—the callee needs to know both which workload is calling and which authority that workload may use.
Three questions, in order
Every agent call must settle three different questions:
- Identity — who are you? A stable identifier for a workload, service account, or user.
- Authentication — what evidence can I validate? This might be an issuer’s token, a bearer credential, or proof bound to the presenting workload through mTLS, DPoP, or a request signature.
- Authorization — what may you do? A policy decision about this action, resource, user, and amount.
An identity is a name. Authentication is evidence. Authorization is a separate decision about the requested operation.
Before the diagram, predict it. An attacker tries to impersonate the billing agent, then the genuine agent asks for something outside its job. Which gate stops each request?
A spoofed identity fails when the callee cannot validate its credential or an authenticated identity lookup. A signed JWT proves that a trusted issuer signed claims; it does not prove that the current presenter holds the issuer’s private key. A stolen bearer token may validate when presented by another workload. mTLS or DPoP can add sender binding.
An over-scoped request fails at authorization even when the credential is genuine. That protects against buggy, compromised, or manipulated agents.
What the callee checks
An access token is a credential presented to a service. It may be an opaque value looked up by the authorization server or a signed JWT carrying its claims.
For a JWT, the billing service should verify:
- The signature matches a trusted issuer key.
issis trusted andaudidentifies the billing service.expandnbfmake the token currently valid.- Scopes and other claims permit the request.
For an opaque token, it should use authenticated introspection or lookup over a protected channel. Require an active result and validate the issuer, audience, expiry, scopes, and policy data. Failure or malformed data means denial.
Then apply local policy. Check:
- the tenant
- the resource
- the amount
- the subject
- the actor
- the delegation
- the destination
- the approval state
A signature does not show that the claims are sensible or sufficient for this refund.
Audience is especially important. An inventory token must not work at billing just because both services trust the same issuer. Otherwise one stolen token is a skeleton key.
TLS encrypts traffic and authenticates the server. Mutual TLS, or mTLS, authenticates both workloads with certificates. Neither answers which user approved a refund or whether this invoice may be changed.
Machine-to-machine choices
Use established identity systems rather than inventing agent cryptography.
- OAuth client credentials: a workload authenticates to an authorization
server and receives a short-lived token for its own permissions, such as
billing.refund. This is the usual choice when an agent acts as itself. - mTLS: authenticates workloads and protects connections, especially in a service mesh. It still needs application authorization for users, resources, and tasks; certificates also require rotation.
- Cloud workload identity: lets a deployed workload obtain credentials from its cloud or cluster identity instead of storing a static secret. It does not make an over-broad IAM role safe.
- A2A Agent Cards and MCP tool descriptions: advertise endpoints, capabilities, and supported security schemes. They are discovery metadata, not permission to call or use the capability.
Bearer tokens make short expiry and secure storage important: a copied token can be replayed until it expires. mTLS-bound tokens or DPoP reduce that risk by requiring proof from a key held by the presenter.
Authorization is more than a scope
A scope such as billing.refund names a broad permission. Resource policy must
narrow it. For Maya’s refund:
- Her account permits up to $1,000.
- The expense agent permits up to $500.
- This delegation permits invoice-481 and $480.
- The billing service permits only invoices in her tenant.
The effective limit is the intersection: min(1000, 500, 480) = 480. A request
for $620 must fail despite a genuine token and the right scope. Use integer cents
for money.
Delegation and the confused deputy
When an agent acts for a user, avoid both a powerful service credential and a forwarded, broad user token. Instead, exchange or mint a narrowed delegated token containing the user, calling agent, target service, and task limits:
subject: Maya
actor: expense-agent
audience: billing-agent
scope: billing.refund
resource: invoice-481
maximum amount: 48000 cents
expires: five minutes from issue
The subject is the user whose authority is used; the actor is the workload
making the call. The audience prevents reuse at another service. The resource
and amount turn a general permission into a bounded task.
The token-exchange flow must verify that the expense agent may request this delegation and that Maya’s authority supports it. Delegation cannot widen either party’s authority:
delegated authority = user authority ∩ agent authority ∩ task limits
The billing agent must still authenticate the expense agent on the immediate
hop. Do not trust a caller-supplied header such as
X-Original-Agent: expense-agent.
A confused deputy appears when a lower-privilege caller persuades a higher-privilege agent to use its authority. Prompt injection makes this easier: hostile text can request a call to a powerful peer, but text is not permission. Authenticate every hop, pass scoped delegation, and never let model output or tool content escalate privileges.
A small authorization check
This is a partial policy example. A trusted identity layer must first validate
the token—by signature and claims for a JWT or authenticated introspection for an
opaque token—and independently produce the immediate caller. The
authenticated_caller value must never come from an HTTP field.
def authorize(
claims,
authenticated_caller,
requested_subject,
action,
resource,
tenant,
amount_cents,
now,
):
if not isinstance(claims, dict):
return "deny: malformed claims"
required_claims = (
"aud", "exp", "scope", "resources", "max_amount_cents",
"sub", "act", "tenant",
)
if any(name not in claims for name in required_claims):
return "deny: malformed claims"
aud = claims["aud"]
exp = claims["exp"]
scope = claims["scope"]
resources = claims["resources"]
max_amount_cents = claims["max_amount_cents"]
subject = claims["sub"]
actor = claims["act"]
delegated_tenant = claims["tenant"]
if (
not isinstance(aud, str) or not aud
or type(exp) is not int
or not isinstance(subject, str) or not subject
or not isinstance(actor, str) or not actor
or not isinstance(delegated_tenant, str) or not delegated_tenant
or not isinstance(scope, str) or not scope.strip()
or not isinstance(resources, list) or not resources
or not all(type(item) is str and item for item in resources)
or type(max_amount_cents) is not int or max_amount_cents < 0
):
return "deny: malformed claims"
if (
not isinstance(authenticated_caller, str) or not authenticated_caller
or not isinstance(requested_subject, str) or not requested_subject
or not isinstance(action, str) or not action
or not isinstance(resource, str) or not resource
or not isinstance(tenant, str) or not tenant
or type(amount_cents) is not int or amount_cents < 0
or type(now) is not int
):
return "deny: malformed request"
scopes = set(scope.split())
if actor != authenticated_caller:
return "deny: wrong delegated actor"
if subject != requested_subject:
return "deny: wrong delegated subject"
if delegated_tenant != tenant:
return "deny: wrong tenant"
if aud != "billing-agent":
return "deny: wrong audience"
if exp <= now:
return "deny: expired token"
if action not in scopes:
return "deny: missing scope"
if resource not in resources:
return "deny: resource not allowed"
if amount_cents > max_amount_cents:
return "deny: amount limit"
return f"allow {action} {resource} ${amount_cents / 100:.2f}"
now = 1_700_000_000
token_claims = {
"aud": "billing-agent",
"exp": now + 300,
"sub": "Maya",
"act": "expense-agent",
"tenant": "maya-company",
"scope": "billing.refund",
"resources": ["invoice-481"],
"max_amount_cents": 48000,
}
requests = [
("Maya", "refund", "invoice-481", "maya-company", 48000),
("Maya", "refund", "invoice-481", "maya-company", 62000),
("Maya", "refund", "invoice-999", "maya-company", 48000),
]
for subject, action, resource, tenant, amount in requests:
print(
authorize(
token_claims,
"expense-agent",
subject,
action,
resource,
tenant,
amount,
now,
)
)
It prints:
allow refund invoice-481 $480.00
deny: amount limit
deny: resource not allowed
The first request matches all of these checks:
- actor
- subject
- tenant
- audience
- scope
- resource
- 48,000-cent limit
The second exceeds the limit; the third was not delegated. Negative amounts,
missing claims, and malformed types also deny. Exact scope membership prevents
billing.ref from matching billing.refund.
Choosing a mechanism
Use workload identity plus OAuth or cloud IAM within one cloud, and mTLS in a service mesh. Use OAuth client credentials for cross-service calls. When an agent acts for a user, use token exchange or an equivalent delegated flow. Combine layers when they answer different questions: mTLS can authenticate the workload while OAuth carries user delegation and fine-grained permissions.
An Agent Card or MCP description helps discover an endpoint and its auth scheme; it never replaces either layer.
Failure modes
- 401 after a gateway: check missing headers, expiry, issuer, and audience. Obtain a token for the resource-owning service; do not blindly forward a token minted for another audience.
- 403 with a valid token: inspect scope, tenant, resource, and delegated limits. Authentication succeeded; authorization rejected the action.
- Duplicate side effects: authentication can succeed twice after a retry. Use an idempotency key enforced by the billing service.
- A powerful agent appears without a user: require verifiable subject and actor delegation for user-triggered actions and log both.
- Expiry failures: synchronize clocks, allow only documented clock skew, refresh before expiry, and rotate cached issuer keys correctly.
- A stolen token works elsewhere: this is the weakness of bearer tokens. Use short lifetimes, strict audiences, secure storage, and sender-constrained tokens such as mTLS-bound credentials or DPoP.
The trade-off
Authentication adds latency, dependencies, certificate lifecycle work, and policy testing. A small single-process prototype may need only a simple process identity. An agent that can refund money or change production data needs the additional machinery.
Mature systems use existing workload identity, OAuth service authorization, mTLS where the mesh provides it, and explicit resource-level policy. They trace actor and subject through every hop; observability is part of the security control.
Quick check
Quick check
Next
Authentication is one slice of running agents safely in production. See agent security for prompt injection and least-privilege controls, agent reliability for safe side effects and retries, and observability to log and trace every inter-agent call.
Practice this in an interview
All questionsNo. Agent-to-agent authentication identifies the calling agent, but it does not establish that the original user authorized the requested action. Preventing a confused deputy requires verified identity propagation, explicit delegation, and authorization at every hop.
Keep raw credentials outside model context and traces. Let the model propose typed intent, authorize the final action and arguments deterministically, then have a trusted executor inject a short-lived, narrowly scoped, audience-restricted credential for one call. Re-authorize downstream and gate high-impact writes with explicit approval.
Autonomous agents are risky because untrusted prompts, retrieved documents, tool outputs, and memories can influence a model that has real authority to read data and take actions. The main risks are prompt injection and hijacking, excessive permissions and confused-deputy actions, data exfiltration, poisoned memory or tools, and runaway cost or destructive loops; defenses must enforce authorization, isolation, approvals, validation, budgets, and auditability outside the model.
An AI agent is an application that lets an LLM choose and execute validated tools in a bounded loop, carrying observations and state forward until it reaches a goal or needs approval. A single LLM call produces one response or tool-call proposal and stops; it does not itself provide the loop, live-system access, memory, or side effects.