Trusted AI Agent Transactions, Part 2: PingFederate Token Exchange Tokenetes has implemented a Transaction Token Service (TTS) using PingFederate's RFC 8693 token exchange protocol to securely bind user, agent, and workload identities in AI agent transactions. The design uses a subject token for the user and a SPIRE-issued actor token for the agent workload, ensuring neither can substitute for the other. PingFederate validates both tokens and issues a short-lived Txn-Token that carries immutable transaction context. Part 1 //01-the-identity-problem.md separated the user, logical agent, runtime workload, agent execution, and transaction identities. This part shows where they are joined safely. In the Tokenetes architecture, a Transaction Token Service, or TTS, uses the RFC 8693 token exchange protocol to mint a Txn-Token. This implementation assigns that logical role to PingFederate. The agent sends: subject token = the user's access token subject token type = access token actor token = the agent workload's JWT-SVID actor token type = JWT requested token type = urn:ietf:params:oauth:token-type:txn token audience = the transaction trust domain scope = the narrow purpose of this transaction The subject token answers who authorized the external invocation. The SPIRE actor token is an agent-specific extension that proves which attested workload is requesting the exchange. Neither token is accepted as a substitute for the other. The base Transaction Tokens profile relies on authenticated TTS clients; this design additionally carries explicit actor evidence so PingFederate can bind a logical AI agent to its runtime workload. The following payloads are illustrative decoded claims. In the implementation, claims are used only after the complete token has passed signature, issuer, audience, algorithm, key ID, and time validation. Raw tokens are never written to application or audit logs. The subject token is the user's OAuth access token: { "iss": "https://pingfederate.example", "sub": "user-123", "aud": "wai-agent-token-exchange", "scope": "openid profile mcp:invoke", "client id": "wai-browser", "iat": 1770000000, "exp": 1770000300, "jti": "subject-token-id" } It represents the authenticated user. It does not prove which agent workload is presenting it, and it is not forwarded to the MCP gateway or downstream services. The actor token is a JWT-SVID issued by SPIRE to the running agent workload: { "iss": "https://spire.example.org", "sub": "spiffe://example.org/agent/demo", "aud": "urn:pingfederate:wai:token-exchange" , "iat": 1770000000, "exp": 1770000060 } It proves the SPIFFE workload identity to PingFederate. It does not identify the user, and it does not let the workload choose a logical AgentID . The agent submits both tokens to PingFederate using a form-encoded RFC 8693 request: POST /as/token.oauth2 HTTP/1.1 Host: pingfederate.example Content-Type: application/x-www-form-urlencoded grant type=urn:ietf:params:oauth:grant-type:token-exchange &subject token=REDACTED USER ACCESS TOKEN &subject token type=urn:ietf:params:oauth:token-type:access token &actor token=REDACTED SPIRE JWT SVID &actor token type=urn:ietf:params:oauth:token-type:jwt &requested token type=urn:ietf:params:oauth:token-type:txn token &audience=example.org &scope=mcp:invoke &request details=%7B%22target%22%3A%22mcp-gateway%22%2C%22tool%22%3A%22customer.read%22%7D The values are redacted here intentionally. Tokens belong in the protected request body over validated TLS, never in a URL, log message, audit event, or error response. The PingFederate Token Exchange Processor Policy validates both sides of the delegation: AgentID The caller never supplies an authoritative logical agent identity. A workload such as spiffe://example.org/agent/demo is mapped to a configured logical agent such as urn:agent:demo inside the trusted policy boundary. After validating the subject and actor independently, PingFederate should issue a short-lived signed JWT conforming to the Transaction Tokens profile. Its protected header identifies it as a Txn-Token: { "typ": "txntoken+jwt", "alg": "ES256", "kid": "pingfederate-transaction-signing-key" } The body carries the immutable transaction context: { "iss": "https://pingfederate.example/transaction-token-service", "sub": "user-123", "aud": "example.org", "scope": "mcp:invoke", "txn": "019...", "req wl": "spiffe://example.org/agent/demo", "tctx": { "target": "mcp-gateway", "tool": "customer.read", "agent": { "id": "urn:agent:demo", "instance id": "019..." }, "workload": { "id": "spiffe://example.org/agent/demo" } }, "rctx": { "authn": "urn:ietf:rfc:6749" }, "iat": 0, "exp": 0 } The response uses the normal OAuth token response shape: { "access token": "REDACTED TRANSACTION JWT", "issued token type": "urn:ietf:params:oauth:token-type:txn token", "token type": "N A" } The Txn-Token is not sent in the OAuth Authorization header between internal workloads. It uses its dedicated header: Txn-Token: REDACTED TRANSACTION JWT The token is not replay resistant, so TLS, its very short lifetime, trust-domain audience validation, and immediate-caller SPIFFE mTLS are all required. The same Txn-Token remains unchanged through the MCP gateway, MCP server, and protected API. Each service validates it before using any decoded claim. Arbitrary claims are not copied from either input token. The original user access token and actor JWT-SVID stop at the exchange boundary and are not propagated to MCP servers or downstream APIs. The repository already implements the difficult security boundaries: RFC 8693 exchange, independent subject and actor validation, workload-to-agent binding, short lifetime, immutable propagation, strict issuer and audience checks, SPIFFE mTLS at each hop, target authorization, and correlated audit. It currently represents the result with application-specific transaction claims and bearer-token transport. To claim Tokenetes-style Transaction Tokens conformance, it still needs to emit typ: txntoken+jwt , request and return the Transaction Token type URN, map the context to txn , scope , tctx , rctx , and req wl , use the trust domain as aud , and propagate the JWT in the Txn-Token header. Validation must reject the older application token shape once that migration is enabled. The reference implementation manages PingFederate with the official Terraform provider. Terraform owns the token processors, token exchange policy, access token manager, mapping, scope, and OAuth client. Plugin field names are discovered from the running PingFederate version before configuration is generated. This avoids guessing descriptor fields or weakening validation when product versions differ. Startup and configuration are separate trust operations. The profile starts the pinned product and installs the tested custom plugin. Scope creation and Terraform apply remain explicit operations, so starting a container cannot silently rotate an OAuth client secret or mutate token policy. The repository-owned profile overlay is mounted read-only. A local generator creates short-lived bootstrap TLS material, datastore credentials, and system keys with cryptographically secure randomness. Those values are written to an ignored local file and never committed. The clean-volume test then: This is important because a configuration that only works against a long-lived developer volume is not a reproducible security control. Previous: The identity problem behind AI agents //01-the-identity-problem.md Next: Binding a logical agent to a real workload with SPIRE //03-spire-workload-identity.md References: CNCF Tokenetes https://www.cncf.io/projects/tokenetes/ and the IETF Transaction Tokens draft https://datatracker.ietf.org/doc/draft-ietf-oauth-transaction-tokens/ . GitHub Repository: https://github.com/darkedges/pf-tts https://github.com/darkedges/pf-tts