OAuth·for·Agents

Scenario 06 · adds internal call context

The request fans out inside your own walls

One agent request arrives at your edge and becomes eleven internal service calls, three queue messages and a batch job that runs an hour later. The access token was audience-bound to the edge and shouldn't travel further. Everything downstream still needs to know it's acting for a specific person, under a specific authorization, in a specific request.

The two things people do instead

Forward the access token everywhere

Simple, and it does preserve the user's identity. It also means every internal service now holds a credential that works against your external API — and, if it's a token from scenario 05, possibly against a partner's. A compromised log shipper or a mis-scoped debug endpoint becomes an authenticated caller with the user's full external authority. The token's audience said "the edge" for a reason.

The same mistake has a second form, one hop out, and it's worth naming separately because it doesn't feel internal at all: a tool forwarding the token it was handed. An agent calls your tool with an access token; your tool needs to reach the service behind it; the token is right there. Passing it along makes the service downstream a replay target for a credential minted for you, and it means a leak anywhere in that chain is a leak of the caller's authority rather than your own. Exchange it — token exchange or a transaction token inside your domain, identity chaining when the service sits behind a different authorization server. This applies whether the tool is a plain API or an agent someone else is calling.

Drop the user and trust the network

Services authenticate to each other with their own credentials and pass a user_id in a header or a request body. Every service is now trusting every other service's claim about who asked, with no cryptographic backing. The audit trail says the orders service did it. When something goes wrong you cannot reconstruct what a person actually authorized.

Why agents sharpened this

This was always a bit uncomfortable and mostly survivable, because the fan-out was deterministic — you could read the code and know what a request would touch. An agent-initiated request doesn't have that property. The set of services a task reaches depends on what the agent decided, which depends on input you didn't write. "Trust the network" stops being a shortcut and starts being the whole security model.

The mechanism

A credential scoped to one call chain

Transaction Tokens put a deliberate boundary at the edge of your trust domain. The external token stops there. In its place, the edge calls a Transaction Token Service — a Token Exchange endpoint — and gets back a short-lived token that is valid only inside, and only for this request.

The important property is that part of it is frozen:

  • Immutable context. Who the user is, which agent is acting, what was authorized, what request this is. Set once at the edge, carried through every hop, unchangeable by anything downstream.
  • A transaction identifier. One value tying all eleven calls, three messages and the batch job together. Your traces and your audit log finally describe the same thing.
  • Per-hop requester context. Each service can record what it's asking for without touching the frozen part.
  • A very short life. Minutes. It's scoped to one request, and requests end.
// a transaction token, decoded
{
  "iss": "https://txn-token-service.internal",
  "aud": "https://internal.example",      // inside only. useless outside.
  "txn": "txn_01KAB93QF7X2",               // ties the whole fan-out together
  "sub": "user:9f3c21",
  "purp": "process_return",
  "exp": 1787894700,

  "tctx": {                                // immutable — set at the edge, frozen
    "actor": { "sub": "agent:support-agent/inst-7741" },
    "client_id": "https://agent.example.com/clients/support-agent",
    "approved_by": "user:31f0aa",          // from scenario 04's challenge
    "challenge_id": "chal_01K9M2X7"
  },
  "azd": {                                 // what was actually authorized
    "type": "refund",
    "order_id": "ORD-88213",
    "amount": { "currency": "GBP", "amount": "900.00" }
  },
  "rctx": { "req_ip": "203.0.113.44", "authn": "mfa" }
}

Notice what came through from four rungs down. The agent instance from scenario 00's key. The user from scenario 01. The authorization details from scenario 03. The approval from scenario 04. By the time the payments service — service number nine, which has never heard of an agent — makes its decision, it has the entire story in a signed token it can verify locally.

The flow

Edge to service nine

Exchange at the boundary, propagate inward
  1. AgentEdge / API gateway

    Arrives with a DPoP-bound access token, audience-restricted to the edge. The edge validates it and does not pass it on.

  2. EdgeTxn Token Service

    Token exchange: hands over the external token and the request context, receives a transaction token whose immutable context is now fixed for the life of this request.

  3. EdgeOrders service

    Calls inward, presenting the transaction token. Orders validates the signature locally — no callback to an authorization server on the hot path.

  4. OrdersTxn Token Service

    Needs to call two more services. Requests a token for the next hop; the service can add its own requester context but cannot alter tctx, sub or azd.

  5. OrdersPayments service

    Service nine sees the user, the agent instance, the approved refund and the approver — and enforces its own policy on all four.

  6. PaymentsPartner API

    Leaving the domain again: the transaction token becomes the input to another exchange, producing an outbound grant with internal detail stripped out. Scenario 05, in reverse.

Going back out

The transaction token chaining profile covers that last step, including claims minimization — the internal token's structure, service names and request context never cross back over the boundary. Your internal topology stays internal.

And then turning it off

Short lifetimes are why this design degrades gracefully: a transaction token expires in minutes, so a compromised internal service has a small window and no external reach. But "revoke this agent everywhere, now" spans the IdP, several authorization servers and possibly a partner, and no token lifetime is short enough to feel like an answer during an incident.

The machinery for that is the Shared Signals Framework, with CAEP and RISC as its profiles: a standard way for one party to tell another that a session ended, a credential changed, risk was elevated, or a grant was revoked, in near real time. Every participant here can subscribe — the agent, the edge, the tool, the internal services — and what they owe in return is enforcement. A service that receives a revocation signal and keeps honouring a cached decision until it expires has turned a five-second problem back into a five-minute one.

This is the biggest operational gap on the site, and it's worth being precise about which kind of gap it is. It isn't a design problem and it isn't an open question — the specs exist, they're profiled for interoperability, and they're increasingly written down as a requirement rather than a nice-to-have. It's that not enough parties have implemented their half yet.

Until it's everywhere: short tokens, rotated refresh tokens, and the refresh token and authorization expiration draft so an agent knows when its grant ends rather than discovering it.

The whole ladder, in one token

Seven scenarios, each adding one piece, and the last one shows why they compose: the transaction token at service nine contains the agent's identity, the user's identity, the specific operation, the human approval and the request that occasioned it. Every one of those came from a different spec, most of them published years before anyone said "agentic," and none of them had to be redesigned to fit together.

That's the argument. Not that this is finished — it isn't — but that the remaining work is a handful of drafts landing in an ecosystem that already has the hard parts built.