Practical LLM Security · Multi-Agent Security · Part 1 of 5

Carry the user, the acting agent, and a shrinking set of permissions through every hop of a multi-agent workflow using token exchange, audience binding, and short lifetimes.

The identity question every hop must answer

When a user asks an orchestrator agent to do something, and the orchestrator asks a research agent, which then asks a billing agent, each downstream service needs to know three things: on whose behalf the request is made, which software is acting now, and what this particular hop is allowed to do. Most multi-agent prototypes answer only one of those questions. They forward the user's original token unchanged, or they let each agent call tools with its own powerful service account.

The site's confused deputy guide introduces the intersection rule and the act claim. This chapter turns that into an implementable design for a three-agent chain: a token broker, a per-hop exchange policy, scope attenuation, audience restriction, and the ADK integration points where identity enters and leaves the framework.

A concrete failure: one token, three audiences

Northstar's concierge agent receives a customer's access token with scopes for reading orders and requesting refunds. To save effort, it forwards the same bearer token to a research agent that summarizes order history, and the research agent forwards it again to a billing agent that executes refunds. The research agent also processes untrusted product reviews. A crafted review induces it to call the billing agent with a refund for a different order. The billing agent sees a valid token containing the refund scope and processes the request.

The token was valid, but it was the wrong credential for the hop. It was not issued for the research agent, it was not restricted to the billing service as audience, it carried a write scope that the research step never needed, and it did not record that the research agent was the actor. The MCP security best practices name the general pattern token passthrough and state that servers must not accept tokens that were not explicitly issued for them. The same reasoning applies between agents.

Architecture: a broker that issues one token per hop

HopSubjectActor chainAudienceScopesLifetime
User to conciergecustomer c-104noneconciergeorders.read refunds.request15 min
Concierge to researchcustomer c-104conciergeresearchorders.read5 min
Concierge to billingcustomer c-104conciergebillingrefunds.request order:o-772 min
Research to billingdenied by policy

OAuth 2.0 Token Exchange, RFC 8693, defines how a client presents a subject token, and optionally an actor token, to an authorization server to obtain a new token. It defines the act claim to identify the current actor; a chain of delegation is expressed by nesting act claims, with the outermost representing the current actor and nested claims representing prior actors. It also defines may_act, a claim stating which party is authorized to act on behalf of another. RFC 8707 adds the resource parameter so a client can indicate the protected resource for which the token is requested, producing an audience-restricted token.

The broker applies a per-hop policy: which agent may exchange for which audience, which scopes may be requested from which incoming scopes, and how deep a chain may go. Scopes only shrink. The billing token is also bound to the specific order the user referenced, using a resource-specific scope or a rich authorization detail, so designation is part of the credential rather than a free argument. The OAuth security best current practice, RFC 9700, recommends audience-restricted and preferably sender-constrained access tokens, which limits the damage when one agent leaks a token.

Implementation: exchange policy and ADK integration

Illustrative python; adapt the persistence and identity adapters to your service.

HOP_POLICY = {
    ("concierge", "research"): {"orders.read"},
    ("concierge", "billing"): {"refunds.request"},
}
MAX_DEPTH = 2

def exchange(broker, subject_token, actor, audience, requested, designation=None):
    allowed = HOP_POLICY.get((actor, audience))
    if allowed is None:
        raise PermissionError("hop_not_allowed")
    claims = broker.verify(subject_token)
    if len(claims.actor_chain) >= MAX_DEPTH:
        raise PermissionError("chain_too_deep")
    scopes = set(requested) & allowed & set(claims.scopes)
    if not scopes:
        raise PermissionError("no_remaining_scope")
    return broker.issue(subject=claims.sub,
                        actor_chain=[actor, *claims.actor_chain],
                        audience=audience, scopes=sorted(scopes),
                        designation=designation, ttl_seconds=120)

The function is an illustrative policy layer in front of a real token service that implements RFC 8693; it is not a library API. Note the three-way intersection: requested scopes, scopes allowed for this hop, and scopes present in the incoming token. A hop can never gain a scope the user lacked.

In ADK, identity enters in two documented places. The ADK safety guide distinguishes agent-auth, where a tool uses the agent's own identity such as a service account, from user auth, where the tool uses the controlling user's identity, typically via OAuth. For user auth, tools can request credentials, which surfaces an adk_request_credential event that the client application must handle before the run resumes. Inside callbacks and tools, the context object exposes the invocation's user_id as a read-only property; it is only as trustworthy as the value your server passed to the runner, so set it from the authenticated session and never from model output. Between agents over A2A, the protocol specification lets an agent signal that it needs authorization mid-task through the TASK_STATE_AUTH_REQUIRED state, which is the right moment to perform an exchange instead of reusing a broader token.

Tradeoffs to decide explicitly

Per-hop exchange adds a network call and a dependency on the authorization server at every delegation. Cache exchanged tokens per task and audience for their short lifetime, and fail closed for write scopes when the broker is unavailable; read-only hops may degrade to a cached result if that matches the product promise. Very short lifetimes reduce replay value but cause failures for long-running tasks. Prefer re-exchange at each step over long-lived tokens, and design tasks to checkpoint so a refused exchange stops cleanly.

Not every downstream API supports audience restriction or token exchange. Put a thin gateway in front of such APIs that validates the exchanged token and then uses a narrowly scoped service credential for that one call, logging the original subject and actor chain so attribution survives. The ADK safety guide notes that with agent-auth all actions appear to come from the agent, so logs must preserve the user attribution explicitly.

Worked case: the review-driven refund, replayed

With the broker in place, the concierge exchanges the customer token for a research token with only orders.read and audience research. The poisoned review still influences the research agent, which tries to call the billing agent. The billing agent rejects the call because the presented token has audience research and no refund scope. The research agent cannot obtain a better token because the hop policy has no entry for research to billing.

The legitimate refund path still works. The concierge asks the customer to confirm the refund for order o-77, then exchanges for a billing token with refunds.request bound to that order and a two-minute lifetime. The billing agent verifies audience, scope, designation, and the actor chain, then commits the refund through the controls described in the refund ledger chapter. The audit record shows customer c-104 as subject and the concierge as actor.

Verification steps

Write tests that try every forbidden hop and assert rejection at the receiving service, not merely at the broker. Present a token for the wrong audience, a token with an extra scope, an expired token, a token whose designation names a different order, and a chain deeper than the limit. Assert that no side effect occurs and that the rejection reason is logged with subject and actor. Run the same suite after framework or SDK upgrades. The companion delegation boundaries lab traces privileges across three agents and lets you compare forwarding a token, ambient service identity, and per-hop exchange with attenuation.

Practice and continue

Open the Delegation boundaries: trace privileges across three agents lab to change the assumptions with deterministic synthetic data and inspect each decision. This chapter belongs to the Multi-Agent Security group of Practical LLM Security, which builds on the 25-chapter abuse and fraud prevention series.

Sources and implementation scope

Primary references checked October 16, 2026. Framework details follow the linked documentation and, for ADK, the google-adk 2.12.0 package source; pin and test the version your application uses. The Northstar scenarios, policies, figures, and code are original illustrative designs, not measured production results or complete integrations.

Decide authority with verified identity and provenance, enforce it where the effect happens, and test both containment and legitimate use.