Skip to content
Tarn & Timber Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Token exchange

On-Behalf-Of versus Custom Token Exchange, and act chains.

The shop uses On-Behalf-Of exchange and two separate Custom Token Exchange profiles: one for Poppy, and one for independent invited-agent delegation. They serve different callers and preserve different authority. Local tests simulate Auth0; tenant behaviour (actor claims, scope refusals, CTE-to-OBO) needs checking against the real tenant.

On-Behalf-Of Custom Token Exchange
Used by The MCP server The Auth0 proxy, for Poppy agents
Input The MCP client’s access token (Token A) The Poppy session and grant
Output An API token naming the user, with an act chain An Auth0 token for the API or MCP audience
Refresh token None None from Auth0; the agent renews with its Account Token

The CTE column above describes Poppy API credentials. The optional invited-agent profile has a different contract:

Invited-agent CTE Contract
Requesting client The independent personal_agents client
Subject token type urn:poppy:invited-agent
Authority Verified independent actor, live owner approval and the exact destination website client
Result Approved delegated identity for the website session-transfer flow; no Poppy Account Token or direct Shop API grant

actions/invited-agent-token-exchange.js and the destination login Action enforce this separation. Follow invited-agent setup before enabling the profile; its registrations, secret and login-domain requirements are separate from Poppy API exchange.

Reading an act chain

The API token minted by On-Behalf-Of has the user as sub, the MCP server’s client as client_id, and a nested act claim:

{ sub: <user>, client_id: <MCP server>, act: { sub: <MCP server>, act: { sub: <registered MCP client> } } }

The API requires that the outermost act.sub equals client_id and equals a configured MCP server client. That proves the call came through the MCP server. A token with any other act chain is never classified as delegated, and a token carrying act is never accepted as customer.

Where the exact shape is confirmed

Auth0 documents the authorized party as azp or client_id depending on the dialect. The verifier relies on client_id under RFC 9068; confirm the exact shape, including the response when a requested scope isn’t granted, against the real tenant.

Rules

  • The requested scopes are exactly the tool’s declared API scopes, and the cache key includes them.
  • Two MCP clients for the same user never share a cached exchange result.
  • An expired Token A can’t be exchanged.

OBO request and cache

The server sends JSON to the custom-domain /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:token-exchange, client_id, client_secret, subject_token, subject_token_type and requested_token_type both set to urn:ietf:params:oauth:token-type:access_token, the Shop API audience, and sorted space-joined scope.

The memory-only cache key is obo|clientId|audience|scopes|sha256hex(Token A). Single-flight coalesces matching calls; an LRU bound retains at most 1000 entries per supplier. An entry expires at the earlier downstream issuance expiry or Token A expiry, minus 30 seconds. Missing expires_in is never cached. Different Token A bytes, resource clients or scopes never share a result. No user token goes into the Workers Cache API.

The API captures the user’s live lifecycle revision and epoch before replay or execution. It rejects inactive accounts and translates only the called operation’s scopes. An OAuth observation cannot begin account deletion, and payment-domain guards refuse human card changes to OAuth agents even when route guards are bypassed.

The Poppy token-exchange Action

The CTE Action (actions/poppy-token-exchange.js) allows the audiences in POPPY_ALLOWED_AUDIENCES: the company API, the Shop API and the canonical MCP resource. API-purpose subjects must name the exact requested resource. Subjects without a resource are valid only for the company audience, and a session-transfer subject cannot become an API token. The shared Auth0 proxy client holds the user grants. Direct subjects carry resource, gid, and optional cnf_jkt; the Action keeps poppy_sid, poppy_user_id, poppy_gid and poppy_jkt on its single personal-agent actor, and validates grant IDs and key thumbprints before setting the user. Custom actor fields and OBO of a CTE token are the parts to verify against the real tenant.

Was this page helpful?