---
title: Token exchange
description: On-Behalf-Of versus Custom Token Exchange, and act chains.
sidebar:
  order: 4
---

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](/architecture/auth0/invited-agents) 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:

```text
{ 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.
