Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Agentic token exchange: nested actor chains, and the governed flatten

How to give a chain of AI agents verifiable identity — so that every token says who acted on whose behalf, and the one place where that rule is deliberately broken is governed by policy rather than by a hard-coded shortcut.

Reference implementation on PingFederate (identity plane) and PingAuthorize (authorization plane), as config-as-code. Everything here is the real configuration from a working system, including the parts that didn't work the way the design said they would.


The two ideas

1. Delegation, not impersonation. Every agent gets its token by exchanging the token it was handed (RFC 8693). The human stays as sub; each hop wraps the calling agent around the incoming act chain. The token records the whole delegation path, so "which agent did this, for which principal?" is always answerable.

{
  "sub": "alice",
  "act": {
    "sub": "urn:agent:northwind-payments:v1",
    "act": { "sub": "urn:agent:northwind-concierge:v1" }
  }
}

2. The governed flatten. For one downstream audience — an internal event sink — the chain is deliberately collapsed: the root actor (the innermost act.sub) is promoted to sub, and act is dropped. The agent acts as itself, because the true principal of "a payment event was emitted" is the orchestration platform, not Alice. PingAuthorize governs the flip, so it can only happen for that audience, and only for an authorised agent.

{
  "sub": "urn:agent:northwind-concierge:v1",
  "aud": "https://events.northwind.example"
}

That second idea is the interesting one, and the dangerous one. It is exactly the shape of an impersonation bug — which is why it is fenced by an audience, structurally prevented from carrying a chain, permitted by policy, and enforced at both ends.


Start here

Document What it covers
Token exchange and the nested act chain Impersonation vs delegation; how act nests and which end is the root; the three PingFederate objects; why the derived-act OGNL doesn't work and what ships instead; secretless client auth; the gotchas.
The flatten function Why flatten at all; what it means precisely; how the audience selects a chainless token type; the fragile string-walk vs. the policy-governed design; how it's contained.
Policy model The PingAuthorize TokenExchange domain, the flatten-governance policy, the audience-restriction rule — and the one-character default that stops an additive policy from denying the entire estate.
Config-as-code workflow Apply → export → commit → deploy, against an ephemeral identity provider.
Demo runbook Spin the whole thing up and prove it with curl — complete verified config inventory, policy pseudo-code, real wire captures (PF 13.1.1 + PAP 11.1, 2026-07-23), troubleshooting index.
Exchanges on the wire The two token-exchange requests, side by side, with the resulting tokens and the errors you should expect.
What the event sink enforces The receiving end — why it rejects a delegated token.

The configuration

Path
pingfederate/terraform/token-exchange.tf Subject-token processor, token-exchange processor policy, per-agent access-token mappings that mint the nested act (production reference: attestation client auth).
pingfederate/terraform/flatten.tf The audience-scoped events ATM (no act in its contract) + the flatten mapping.
pingfederate/terraform/demo/ Self-contained, verified demo config — applies to a stock PF 13.x and mints the whole chain (see the runbook).
pingauthorize/policy/ The TokenExchange domain, flatten-governance policy, audience-restriction rule — including the verified snapshot + deployment package (token-exchange-flatten.*, importable into any PAP / embedded PDP).
pingauthorize/dsconfig/ Embedded-PDP runtime config.
demo/ Runnable demo UI + mock event sink & bank API — every button executes the exact curl it shows; demo.sh is the headless version.

The shape of it

flowchart TD
    A[Alice — OIDC login<br/>sub=alice, no act] -->|exchange| C[Concierge agent<br/>sub=alice, act={concierge}]
    C -->|exchange| P[Payments agent<br/>sub=alice, act={payments, act={concierge}}]
    P -->|delegated token| BANK[Bank API<br/>authorise on principal + actor chain]
    P -->|"exchange, resource=events"| F[FLATTENED token<br/>sub=concierge, aud=events, NO act]
    F --> ES[Event sink<br/>requires: no act, sub=agent, aud=events]
    F -.->|replayed at the bank| X[DENY — invalid_audience]

    PAZ[[PingAuthorize PDP]] -.->|governs the flip| F
    PAZ -.->|governs the call| BANK

    style F fill:#3b2f00,stroke:#c79a00,color:#fff
    style X fill:#4a1414,stroke:#c74a4a,color:#fff
Loading

Five things worth stealing

The audience is the switch. The flatten reuses the same token endpoint, the same processor policy, and the same subject token as a normal hop. The only meaningful difference is the resource parameter — which selects an access-token manager whose contract has no act attribute at all. The chain isn't stripped by a rule you could bypass; it is structurally unrepresentable in that token type.

Propose vs. authorise are different privileges. Computing the root actor is trivial and needs no privilege (the agent holds the token; it can decode its own act). Authorising the promotion of an actor to a subject is the privileged step. So the agent proposes, and the PDP governs. The alternative — extracting the root actor by string-walking the act JSON in an OGNL expression — is brittle, and worse, it fails open to "". A security decision does not belong in a string function.

Under a deny-biased combining algorithm, "absent" must be a value, not an error. The flatten policy's requestedAudience attribute has defaultValue = "". Without it, every ordinary banking request — which carries no audience — leaves the attribute unresolved → INDETERMINATE → and under DenyOverrides that is a DENY. Adding one additive policy would have denied the whole estate, with a failure mode that looks nothing like its cause.

Fence a flatten at both ends. The event sink rejects any token that still carries an act, or whose sub is a human. The bank API denies any token minted for another audience. Each token is usable at exactly one audience and useless at the other — so an upstream bug can't quietly become downstream over-sharing.

Ship the constraint, don't hide it. The design called for act to be derived from the inbound chain. PingFederate 13.x rejects OGNL referencing act on an access-token mapping (ognl_expression_invalid_attribute), so what ships is a TEXT literal encoding a fixed topology — correct here, but it does not generalise to arbitrary depth. That constraint is documented rather than smoothed over, because a hard-coded chain that looks derived is precisely the thing that survives into production and then silently mislabels an actor.


Standards

RFC 8693 (Token Exchange, act) · RFC 8707 (Resource Indicators) · RFC 9068 (JWT access tokens) · RFC 9449 (DPoP) · RFC 9396 (Rich Authorization Requests) · OAuth 2.0 Attestation-Based Client Authentication

Scope

This is an explainer with a runnable proof. The docs describe the identity/authorization configuration and the reasoning behind it, extracted from a working agentic-banking demo; the demo/ directory and pingfederate/terraform/demo/

  • pingauthorize/policy/token-exchange-flatten.snapshot make the whole token pipeline independently spinnable and verifiable with curl — see the demo runbook for the verified captures (PF 13.1.1 + PAP 11.1, 2026-07-23) and the deliberate demo deltas (client secrets in place of the attestation jar; a curl-able login stand-in). The identifiers (urn:agent:northwind-*, *.northwind.example) are illustrative.

The Terraform and the policy objects are real — including the workarounds, which are re-verified against live product versions in the runbook.

Architecture

Licence

Apache 2.0.

About

Agentic token exchange: nested RFC 8693 actor chains (delegation, not impersonation) + an audience-scoped flatten governed by policy. PingFederate + PingAuthorize config-as-code, with the constraints the design didn't survive.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages