What this is, and the ideas behind it — how agent identity is made governable, provable, and revocable. Every principle here is exercised live in the Deployment flow and Authentication tabs.
The problem — identity for software that acts on its own
Autonomous agents and workloads increasingly act — call APIs, move money, make decisions —
with no human in the loop for each request. That breaks the assumptions behind a hand-issued
client_id + secret. Four gaps, in particular:
Scale
You don't register one client — you deploy a fleet of short-lived instances. Minting and rotating a secret per instance doesn't hold.
Provenance
A verifier needs to know what kind of agent is calling and who vouches for it — not merely that it presents a bearer secret.
Revocation
When a type is compromised or retired you must deny it everywhere, at once — not wait for a credential to expire.
Delegated authority
An agent acts on behalf of a principal, with authority that must be narrow, explicit, and auditable — not the agent's own blanket access.
Everything below is one coherent answer to those four: a federated trust spine, a two-tier identity, live authorization, and fine-grained delegated grants.
The big picture — one trust spine, two tiers, live authorization
A trust anchor vouches for agent types. Each type is deployed as many instances. An instance authenticates to the authorization server, which resolves it back up the chain to the anchor — live — before issuing a token. Withdraw a type at the anchor and every instance of it is denied at once.
Principles
01
Trust is federated, not pre-shared
Every participant is an OpenID Federation entity. A trust anchor issues signed subordinate statements; anyone can fetch an entity's configuration and resolve its trust chain to the anchor, verifying it cryptographically.
Onboarding is "join the federation" — trust is discoverable and verifiable, not a hand-configured secret.
02
Two tiers: the type vouches, the instance proves
An agent has a long-lived type — the federation entity that holds the vouching key and is what you govern,
register, and revoke — and many ephemeral instances, each with its own key that only proves possession. Each
instance also carries its own attester-minted agent_instance_id — an identity independent of its key,
stable for the instance's lifetime, so every instance is individually attributable and revocable even across key rotation.
Governance lives at the type; runtime proof at the instance. You revoke a type, not chase down instances — and keys are binding material, not identity (the AI Agent Instance profile).
03
Authentication is separate from authorization
Two questions, answered separately. AuthN: does the caller possess the keys it claims? AuthZ: given who it is, is it still vouched-for, active, and within the authority its type is registered for?
The AS re-checks authorization on every call — it isn't baked into a static client record.
04
Attestation binds an instance key to a vouched type
The type / attester signs a Client Attestation that binds the instance's public key via cnf; the
instance signs a Proof-of-Possession with the matching private key. Trust in the attester is federation-resolved.
This is OAuth 2.0 Attestation-Based Client Authentication (draft-09) — a standard, not a bespoke scheme.
05
Workload identity (SPIFFE) is metadata, not auth
A SPIFFE SVID states what and where a workload is. It rides inside the attestation's
workload claim as attributes the AS can authorize on — it does not authenticate.
Infrastructure identity informs authorization; it never replaces proof-of-possession.
06
A client is known by its federation identity
Registration is transparent or explicit (OIDF §12). Automatic: present your trust chain at the token endpoint and the AS provisions you on first use. Explicit: POST your entity configuration up front.
Either way there is no hand-minted client_id + secret — the client is its resolvable entity.
07
Revocation is immediate and fleet-wide
Withdraw the type's subordinate statement at the trust controller and its chain stops resolving — the very next token exchange fails, and every instance of that type is denied at once.
Because authorization is resolved live, revocation is a real-time control, not a slow-propagating list.
08
Standards end-to-end, environment as code
OpenID Federation for trust, the OAuth attestation draft + DPoP for proof, RAR for fine-grained authority — real, interoperable specifications. And the whole environment is git-managed, CI-deployed config-as-code.
Reproducible — a bad change is git revert, not an incident.
09
Authority is delegated and fine-grained (RAR)
An agent acts on behalf of a principal. The token carries Rich Authorization Requests
(authorization_details, RFC 9396) — specific actions on specific resources — and an
act claim naming the agent as the actor, so it is delegation, not impersonation.
A downstream policy engine (PingAuthorize) evaluates each detail — least privilege, per request.
10
Every step is verifiable and auditable
Nothing rests on a shared secret. Each artifact is a signed JWT with a traceable issuer; the actor chain
(act), the attesting type, and the resolved trust chain are all recorded.
"Who did this, on whose behalf, vouched for by whom" is answerable after the fact.
The two-phase gate — what the AS actually checks
A token is issued only if both phases pass. Authentication proves possession; authorization proves standing. They're independent — a perfectly-signed request from a revoked type still fails.
① Authenticate — "are you who you claim?"
• the Client Attestation verifies against a federation-resolved attester key — not a shared secret
• the PoP / DPoP is signed by the cnf key the attestation bound — possession
• freshness (iat/exp + skew), single-use jti, and the challenge if one was issued
• the aud names this AS
② Authorize — "may you, right now?"
• the client's trust chain resolves live to the anchor — revoke and it doesn't
• the entity is active and registered for this registration type
• the requested scope ⊆ what the entity is entitled to
• each RAR authorization_details is within the delegated grant (policy engine)
Phase ① is the client-authentication draft; phase ② is OpenID Federation resolution + policy. Revocation lives entirely in phase ② — which is why it bites on the very next call.
RAR here is an entitlement constraint, not a request — a profile of the attestation
We use Rich Authorization Requests (authorization_details, RFC 9396) — but in a specific
way worth calling out, because it inverts where the details come from and where they bite.
The authorization_details ride inside the client attestation,
signed by the attester. They aren't something the client asks for and a user consents to — they're an explicit,
attester-vouched entitlement the client already holds, and the AS applies them as a ceiling on the grant at
issuance. RAR becomes an extension / profile of the attestation, not a request on the wire.
| Standard RAR · RFC 9396 (user · RP · AS) | This profile · attested entitlement | |
|---|---|---|
| Who brings it | the client requests it (PAR / authorization request) | the attester signs it into the client attestation |
| What it is | a request for authority, to be granted | an entitlement the client already holds — a signed ceiling |
| Authorised by | the resource owner consents (or AS policy) | the attester vouches — federation-resolved, no per-request human |
| Where it bites | the Protected Resource enforces the granted details, at call time | the AS filters the grant at issuance — an instance-level ceiling |
| Shape | requested up → granted → enforced down at the RS | attested with the client → filters the grant at the AS |
Concretely, the attestation carries authorization_details: [{ "type":"sales_agent",
"actions":[…], "locations":[…], "sales_regions":["EMEA"], "privileges":[…] }]. The type names the
entitlement schema; its fields are the explicit constraint. The attester vouches for it, and the token's authority is
granted ⊆ the attested entitlement — an instance filter at the token endpoint, per instance, rather than
policy re-evaluated later at each Protected Resource.
Two distinct layers — don't merge them. This attested RAR is the agent's entitlement envelope
— what this type is allowed to do. On whose behalf it acts is separate: the act claim
(RFC 8693 token exchange) carries the principal → agent actor chain — delegation, never impersonation. One says
what the agent may do; the other says whom it does it for.
The AI Agent Instance profile — a name for every instance
When an agent platform registers as one OAuth client, a whole fleet of concurrent agent
instances collapses into a single client identity — so there's no per-instance policy, no audit attribution,
and no way to contain one compromised instance. The AI Agent Instance profile
(draft-mcguinness-oauth-ai-agent-instance-00) closes that gap: the Agent Attester mints a distinct,
stable, key-independent identity for each instance, and the AS surfaces it — with attestable provenance — in the
issued token.
The claims it defines
agent_instance_id required
the instance identity — attester-minted, unique, stable for the instance's lifetime, and not derived from the proof-of-possession key. Keys rotate; the id doesn't.
agent_platform optional
the orchestration runtime the instance executes under — software provenance,
not the OAuth client_id.
agent_model optional
{ id, version } — the model backing the instance. A model change
requires fresh evidence.
agent_runtime optional
an eat member (Entity Attestation Token, RFC 9711) for
hardware-rooted evidence — consumed by the AS for policy, never surfaced to resource servers verbatim.
Two carriers, one identity. The evidence rides either in a
Client Instance Assertion or in a Client Attestation
(draft-ietf-oauth-attestation-based-client-auth + DPoP). This demo uses the Client Attestation carrier
— the exact ② flow decoded below — with the twist that attester trust is resolved via OpenID Federation rather
than a bilateral AS↔attester agreement. Either way the access token is sender-constrained and the instance is surfaced in
act (delegation) or top-level sub (self-acting), tagged
sub_profile: "ai_agent client_instance".
Advertised in metadata. The client registers ai_agent_instance_profile: true (the AS then
requires instance evidence on every covered grant); the AS advertises ai_agent_instance_profile_supported: true.
Assurance comes in tiers — platform self-attestation, hardware-rooted (agent_runtime EAT), or an independent
third-party attester — and policy can condition on which.
Anatomy of a delegated exchange — envelope vs actor, decoded
The same split, on the wire (example values). The attestation carries the entitlement
envelope — what the type may ever do — plus the instance's identity and provenance. A token exchange then
mints an access token narrowed to one task, stamped with the actor — the agent instance, named by its
agent_instance_id and classified ai_agent per the AI Agent Instance profile
(draft-mcguinness-oauth-ai-agent-instance) — and sender-constrained to the instance key.
① Client Attestation — the Agent Instance Evidence, signed by the Agent Attester
{
"iss": "https://attester.example.com", // the Agent Attester — vouches for the instance
"sub": "https://rp.example.com", // the OAuth client = the Agent Platform (not the identity)
"agent_instance_id": "https://attester.example.com/instances/sess-9f2c", // ← THE instance identity — attester-minted, key-independent
"agent_platform": "urn:example:orchestrator:v5", // provenance — the runtime it executes under
"agent_model": { "id": "urn:example:model:atlas", "version": "7.3" }, // provenance — model + version
"cnf": { "jwk": { "kty":"EC", "crv":"P-256", "x":"…", "y":"…" } }, // binds the instance KEY — binding material, not identity
"workload": { "attested_by":"spiffe", "spiffe_id":"spiffe://banking.demo/payment-agent" }, // SPIFFE infra identity — provenance
"authorization_details": [ // ← the ENTITLEMENT ENVELOPE (the ceiling)
{ "type":"sales_agent",
"actions":["read_accounts","create_opportunity","submit_quote"],
"locations":["https://crm.contoso.com/api"],
"sales_regions":["EMEA"], "privileges":["quota:standard"] } ]
}
Plus a PoP signed by the instance's cnf key (aud = this AS, one-time
jti). Together they authenticate the instance under its vouched type — phase ① of the gate. Mind the three
roles the draft keeps distinct: agent_instance_id is the identity, cnf is the binding
key, and agent_platform / agent_model / workload are provenance.
② Access token — issued by the AS, narrowed + the instance surfaced in act
{
"iss": "https://as.bank.example",
"sub": "https://principal.example/alice", // the PRINCIPAL — on whose behalf
"act": { // the ACTOR — the agent INSTANCE (delegation)
"iss": "https://attester.example.com", // the Agent Attester that vouched (act.iss)
"sub": "https://attester.example.com/instances/sess-9f2c", // = agent_instance_id — the instance, not the key
"sub_profile": "ai_agent client_instance", // classifies the actor as an AI agent instance
"agent_model": { "id": "urn:example:model:atlas", "version": "7.3" }, // provenance, surfaced per policy
"cnf": { "jkt": "…" } }, // sender-constrained to the instance key
"client_id": "https://rp.example.com", // the Agent Platform (the registered client)
"scope": "create_opportunity",
"authorization_details": [ // ⊆ the attested envelope, narrowed to THIS task
{ "type":"sales_agent", "actions":["create_opportunity"],
"locations":["https://crm.contoso.com/api"], "sales_regions":["EMEA"] } ],
"cnf": { "jkt": "…" }, // token sender-constrained to the instance key
"aud": "https://crm.contoso.com/api"
}
Read the two together. The envelope in ① is the ceiling the attester vouched for; the token in
② is ⊆ that envelope — narrowed to one action for one task. sub is the principal, and
act is the agent instance: act.sub is its agent_instance_id,
act.iss the Agent Attester that vouched, act.sub_profile classifies it ai_agent
client_instance. The action is attributable to this instance acting for the principal — never disguised as
the principal (impersonation). cnf binds the token to the instance's key, so a stolen bearer copy is useless.
The federation, in depth — statements, chains, and how trust resolves
The trust spine is OpenID Federation 1.0. Every participant is an Entity with a signed statement; trust is a chain of signatures up to a known anchor key — not Web PKI, not TLS certificates, not a shared secret.
Entity Configuration — self-signed
What an entity says about itself. iss == sub, signed by its own Federation Entity Key,
published at /.well-known/openid-federation. Declares its jwks, its metadata,
and authority_hints — who its superiors are.
Subordinate Statement — superior-signed
What a superior says about a subordinate. iss != sub, signed by the superior's key —
this is the vouching. Carries the subordinate's key and may shape its metadata via metadata_policy.
Deleting it is what revokes the entity.
The trust chain — leaf → anchor
A chain is an ordered set of statements. Each link's iss equals the
next-one-up's sub, and each is signed by a key in the statement above it — so you verify signatures
all the way up; the top is the Trust Anchor's own self-signed configuration, whose key you trust out-of-band.
ES[0] Entity Configuration of the LEAF — self-signed by the leaf ↑ signed by a key in ES[1] Subordinate Statement about the leaf — signed by the superior (vouches) ↑ signed by a key in ES[n] Entity Configuration of the TRUST ANCHOR — self-signed · the root of trust
The client can carry the pre-assembled chain in the trust_chain JWS header, or the AS walks
authority_hints up to the anchor itself (fetching each Subordinate Statement from the superior's
federation_fetch_endpoint). Either way the AS re-verifies every signature up to the anchor.
Roles, metadata policy & trust marks — the anchor governs the ecosystem
Trust Anchor
The root. Its keys are the basis of trust, distributed out-of-band. Issues Subordinate Statements and exposes fetch / list / resolve. Two entities trust each other iff their chains share one anchor.
Intermediate
A superior that is itself subordinate to the anchor — it vouches for a
sub-tree. In the demo, as is an intermediate that issues Subordinate Statements for its own subordinates.
Leaf
An end entity — an agent type, an RP, an AS. Publishes only its Entity Configuration; never a fetch or list endpoint.
Metadata policy — the federation shapes what a member may declare
A Subordinate Statement can carry a metadata_policy that applies to
the subject and everything below it. Operators — value, add, default,
one_of, subset_of, superset_of, essential — let a superior
constrain or set a leaf's effective metadata: cap the scopes it may claim (subset_of), force a
value, require a field. Ecosystem rules are applied at resolution, without editing any member's own config.
Trust marks — attested status & compliance
A trust mark is a separately-signed JWT (trust-mark+jwt) asserting a
member conforms to a profile (e.g. region-emea, workload-inspection). It counts only if its
issuer is one the anchor recognises for that type and it validates (signature, expiry, status). So the AS
can gate not just on "is it in the federation" but "does it hold these attestations."
In this demo: the OpenID Federation Trust Controller is the Trust Anchor + resolver; a fedhost serves each entity's self-signed configuration; the client presents/resolves its chain at the token endpoint and the AS re-derives it to the anchor live. Revocation = the anchor deletes the leaf's Subordinate Statement → the chain no longer resolves → the very next token fails. That is the ⑤ Revoke step in the Deployment tab.
Built on standards — not a bespoke scheme
Every layer is a published specification, so the pieces interoperate and can be verified independently.
OpenID Federation 1.0
the trust spine — entity statements, subordinate statements, trust chains, resolution, §12 registration
OAuth Attestation-Based Client Auth · draft-09
the client attestation + PoP that authenticate an instance under its type
AI Agent Instance profile · draft-mcguinness-…-00
agent_instance_id + provenance (agent_platform/agent_model), surfaced in act as ai_agent client_instance
DPoP · RFC 9449
sender-constrained proof-of-possession — the combined-mode alternative to a separate PoP
RAR · RFC 9396
authorization_details — fine-grained, per-resource authority instead of coarse scopes
Token Exchange · RFC 8693 + act
delegation with an actor claim — the agent acts on behalf of a principal, not as them
SPIFFE
a SPIFFE SVID as the workload's infrastructure identity, carried as attestation metadata
The agent lifecycle, end to end. Phase 1: create an agent type and register it as a federation entity (once). Phase 2: deploy an instance of it that authenticates with a client attestation and gets a token. The concept below frames the two-tier model; each step then runs live.
This drives the live PingFederate instance through a local proxy. Each step shows exactly what
the client does: mint keys, get a one-time challenge from the ClientAttestationChallengeServlet, build the
attestation & proof JWTs, and call the token endpoint — then how the AS authorizes the resolved client entity.
Generate keys
Two EC P-256 keys, created in your browser via WebCrypto: the Client Attester (vouches for the
app) and the client instance key (the key the app proves possession of; its public JWK goes in the attestation’s cnf).
attester public JWK
—
client instance public JWK
—
Fetch a challenge from the servlet
POST /oidf/federation/attestation-challenge → a one-time, short-lived challenge. Note the
Cache-Control: no-store and that every call returns a fresh value.
—
Build the attestation & proof JWTs
The Client Attestation JWT (signed by the attester, binds the client key via cnf) plus a
PoP JWT or DPoP proof (signed by the client key, echoes the challenge). Headers/payloads decoded below.
The attestation also carries a workload claim — attester-asserted attributes about the client workload
(software_id, software_version, environment, instance_id,
runtime, host, git_commit).
Call the token endpoint
Send the artifacts as OAuth-Client-Attestation + OAuth-Client-Attestation-PoP
(or DPoP) to the token endpoint. The OAuth AS, the client https://rp.example.com, and the
validateClientAttestation issuance criterion are already configured (open the console above to
view/edit them). The request reaches the hook, which runs the verifier and — for this demo's untrusted attester —
rejects with attestation_validation_failed. A successful token additionally needs the attester to be
OpenID-Federation-trusted (a trust anchor/controller), which the demo doesn't yet stand up.
requested access — RFC 9396 authorization_details
The attestation asserts the entitlement sales_agent → EMEA (see the decoded
attestation above). Choose what this agent asks for; the verifier decision is computed by the same containment
rule the ClientAttestationVerifier runs — a request is granted only if it stays within the attested
sales_regions and actions (proven in harness selfverify).
—
The Send button is gated by the entitlement decision above — the same containment rule the
ClientAttestationVerifier runs. A denied request (e.g. AMER, or delete_account) disables it.
Live PingFederate does not yet enforce RAR server-side (the attribute-contract phase), so a granted request
authenticates exactly as before.
equivalent curl
—
PingFederate response
—
Agent identity, two tiers — the type vouches, the instance proves
An agent identity has two keys at two levels. You register an agent type once — it is a federation entity with its own entity / attester key, enrolled as a subordinate of the trust anchor so it chains into the federation. Then you deploy instances of that type: each instance mints its own local key and is attested by its type. At the token endpoint the instance presents two signatures — one from each key.
entity_id → entity key. ② The type attests each instance: a Client Attestation JWT signed by the
entity key (in OpenBao), carrying the instance's public key as cnf. ③ At the token endpoint the
instance presents the attestation and a PoP signed by its local key. ④ PingFederate resolves the type's
key up the chain to the anchor and verifies both — the type vouches, the instance proves.Entity / attester key — one per TYPE
Created when the agent type is registered.
Signs the Client Attestation JWT — vouches for each instance and binds its cnf. Lives in
OpenBao transit: non-exportable, sign-on-request. Revoke the whole fleet by pulling the type's subordinate
statement from the anchor.
Instance key — one per INSTANCE
Minted when an instance is deployed.
Signs the PoP — proves possession of the attested cnf key. Local to the instance, ideally
hardware-backed: never leaves. Revoke a single instance via its cnf/jti, without
touching the type.
Why the split: the type is the unit of trust & revocation — register once, kill the whole fleet by de-registering it; the instance is the unit of possession — each deployment is individually bound and individually revocable. Day 0 below runs this live.
Deploy an instance — its two-tier identity (and why revoking the type denies it)
Phase 2 — the instance. Phase 1 got the type a token; this phase is about the instance and
how its identity is built — not another token. Deploy an instance of the type: it mints its own local key
(the cnf/PoP key), the type / attester key signs a Client Attestation binding that key via cnf
(the type vouches for the instance), and the instance key signs the PoP. A SPIFFE agent gives the instance a
JWT-SVID whose spiffe_id + attributes ride in the attestation's workload claim as metadata —
what / where the workload is — which does not authenticate. The key point: an instance has no standing of its
own — it borrows the type's. So revoke the type in Phase 1 and every instance is denied at once (fleet revocation),
exactly what the ⑤ Revoke step above demonstrates. Walk the identity through in your browser, then on a real deployed workload.
cnf, vouching for it), and the instance
key signs the PoP (proving possession). The SPIFFE SVID is not part of authentication — its
spiffe_id + attributes ride in the workload claim as metadata the AS can authorize on.Walk it through — in your browser
…or run it on a real deployed workload
A real agent workload deployed as its own service at
… — it self-attests via the SPIFFE agent on boot and runs the whole exchange
server-side, then returns what happened. Same client_attestation_sdk + spiffe.py, over the network.
Create the agent type & register it in the federation
Phase 1 — the type. An agent type is a federation entity. Mint its
entity / attester key (the key that will vouch for every instance), self-sign its entity
configuration (served at /.well-known/openid-federation), and register it in the trust controller —
the anchor signs a subordinate statement binding entity_id → public key, so the type now chains to the
anchor. This is the type's key, created once; it is not the per-instance key (that comes in Phase 2). The
private key never leaves the entity — here it is generated in your browser; only the public JWK and the signed
statement go out. Everything below is live: real keygen, a real registration, a real /resolve.
/.well-known/openid-federation. ③ Only the public JWK
is written to the controller, which answers with the anchor-signed subordinate statement — the federation's
record of this entity. ④ From then on anyone can /resolve the entity: the controller fetches the
self-signed configuration and verifies it against the key in its record.
Federation members — records held by the trust controller
loading…
Registering the client at the AS — OpenID Federation §12 gives two options
Being a federation member (above) makes the entity resolvable. To get a token it still has to become a client of this AS. §12 defines two ways — PingFederate supports both:
Explicit · §12.2 📄 above · response-signing WIP
The client POSTs its entity configuration to the AS's
federation registration endpoint up front. The AS validates the trust chain, provisions the client, and returns its
metadata (an explicit-registration-response+jwt). A deliberate onboarding step, before any token call.
Automatic · §12.1 this PF servlet · ⚡ above
No registration call at all. The client just presents its
private_key_jwt — carrying its trust_chain — at the token endpoint. Our
TokenEndpointAutoRegistrationFilter intercepts the first hit, walks the chain to the anchor,
provisions the client on the fly, then lets the request authenticate. The token comes back — the client is
provisioned from its trust chain the moment it's first used.
The ⚡ button demonstrates the automatic path — after it runs, watch the client appear below in “Clients registered in PingFederate”, with no prior registration call.
Clients registered in PingFederate
↗ raw from PingFederate ↗ admin consoleloading…
Where the attestation comes from. Earlier flows had the browser sign the Client Attestation itself. In production the workload holds no attester key — it presents its SPIFFE identity and PingFederate mints the attestation server-side, signing with a key that lives in the vault.
The workload proves it is a legitimate instance (its SVID) and that it holds its own instance key (a proof of possession); the attester binds that key, folds in the SVID, and signs.
The hosted attester — a SPIFFE SVID in, a signed Client Attestation out
cnf,
carries the SVID in workload, and signs with the client's own key — inline, or in the vault so the
private half never leaves it. The result is a normal Client Attestation, ready to authenticate at the token endpoint.Mint one — in your browser, against the live attester
Pick where the attester key lives, then run it. Your browser mints a SPIFFE SVID and a
proof of possession of a fresh instance key, and posts them to /federation/attestation. The two
options are the same request to the same endpoint — they differ only in which demo client mints, hence where the
signing key lives.
How PingFederate authorizes a federation client
This is the authorization step that follows authentication. The client first
authenticates — in the attestation flow above, with its attestation credential; here, with its
client_credentials. Based on that authenticated identity, PingFederate then authorizes
it by resolving the client's entity record from the trust controller and granting access only
if the client is a federation member (chain resolves to the anchor), is within policy, its
status is active, and — the crux — the requested scopes are within the scopes registered to its
entity. The trust controller does authorization, not authentication. Computed live —
/api/authorize → trust controller /resolve.
GET /resolve resolves the client and fetches its trust chain, ③ GET /.well-known/openid-federation the anchor's key. ④ PF validates the chain to the anchor, then makes the authorization decision — active member, within policy, and above all whether the requested scopes are within the client's entitlements.