Skip to content

A2A delegation

When one agent calls another, the second should act on behalf of the first — and the original user — without inheriting more authority than it needs. oauth.work issues delegated, on-behalf-of tokens narrowed at every hop, each carrying a chain naming who is acting for whom.

User ──grants──▶ Agent A ──on-behalf-of──▶ Agent B ──DPoP token──▶ MCP tool

Every arrow is a real token exchange. Agent A holds a token the user consented to; to call Agent B it exchanges that token for a narrower one.

RFC 8693. The requesting agent authenticates as itself and presents the subject’s token:

POST /token
Authorization: Basic <base64(agent_b:cs_…)>
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<agent A's token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&scope=tool:search
&resource=https://tools.acme.com/mcp
{
"access_token": "eyJ…",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "tool:search"
}

Delegated tokens live 5 minutes, not an hour. An on-behalf-of token should not outlive the task it was minted for.

Four constraints are enforced at exchange time.

Only agents may exchange. The requesting client must be a registered agent client. A web app or service client attempting an exchange gets unauthorized_client.

Scopes only shrink. The granted scope is the intersection of what was requested, what the subject token holds, and what the requesting agent is registered for:

granted = requested ∩ subject_scopes ∩ agent_allowed_scopes

If nothing survives that intersection, the request fails with invalid_scope. An agent cannot gain a capability by being delegated to.

No cross-tenant delegation. The agent and the subject token must belong to the same organization. Otherwise: invalid_grant, “agent and subject belong to different organizations”.

The subject token must be ours, currently valid, and not revoked.

The issued token keeps the original subject in sub and records the acting agent in act. The immediate actor is outermost; earlier actors nest beneath it (RFC 8693 §4.1):

{
"sub": "usr_01J…",
"client_id": "agent_b",
"act": {
"sub": "did:key:z6Mk…agent-b",
"act": { "sub": "did:key:z6Mk…agent-a" }
},
"scope": "tool:search",
"aud": "https://tools.acme.com/mcp",
"actor_type": "agent"
}

Read it outward-in: agent B is acting, on behalf of agent A, on behalf of user usr_01J…. A resource server that wants to authorize on the immediate caller reads act.sub; one that wants to attribute the action to a person reads sub.

Each hop adds one layer. Nothing is ever removed, so the full provenance of a call survives to the end of the chain.

  • Scopes only shrink, at every hop, by construction.
  • Tokens are short. Five minutes limits the value of an intercepted delegated token.
  • DPoP binding is preserved. Present a proof at exchange and the delegated token is bound to the requesting agent’s key.
  • Audience binding. A delegated token minted for one tool server is refused by another.
  • Every exchange is recorded. A token_exchanged audit event names the subject, the actor, the agent client, and the granted scope — and the delegation itself is persisted, so a misbehaving agent is attributable after the fact, not just in the moment.

Plain OAuth proves delegation only to parties that trust the same authorization server. When agent B has to prove to a third party that it may act for the user, exchange the delegated token for a credential:

POST /credentials/issue
Authorization: Bearer <delegated token>
{ "format": "sd-jwt-vc" }

Because the token carries act, the platform issues a DelegationCredential rather than an agent identity credential:

{
"delegatedSubject": "usr_01J…",
"actor": { "sub": "did:key:z6Mk…agent-b", "act": { "sub": "did:key:z6Mk…agent-a" } },
"organization": "org_01J…",
"scope": "tool:search"
}

It is signed by the tenant key and verifiable against the tenant’s published DID document by anyone — which is the part plain OAuth cannot do. See verifiable credentials.

The platform publishes an A2A AgentCard at /.well-known/agent-card.json (and the legacy /.well-known/agent.json), describing its transport and the client-credentials security scheme, so an A2A peer can discover how to authenticate before it has any configuration.