Part 1 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
Next: Binding a logical agent to a real workload with SPIRE
References: CNCF Tokenetes and the IETF Transaction Tokens draft.
GitHub Repository: https://github.com/darkedges/pf-tts