token request shapes

■ Client (agent instance) ■ Platform (issuer / attester / OAuth client) ■ Authorization Server ■ Resource Server   shaded / dashed border = added vs the previous shape
view: |
baseline shape -- nothing marked new
## 1. token request -- the WAG floor: bearer grant, no client identity

POST /token HTTP/1.1
Host: as.saas.example
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIs...       # signed by PLATFORM per-tenancy issuer
&resource=https%3A%2F%2Fapi.saas.example%2F

## 2. decoded: the WAG grant
{ "alg": "ES256", "kid": "2026-07-14", "typ": "JWT" }
.
{
  "iss": "https://acme.agents.platform.example",
  "sub": "wimse://acme.agents.platform.example/agent/7f3d9a2e",
  "aud": ["https://as.saas.example",
          "https://as.saas.example/token"],
  "exp": 1785271980, "iat": 1785271680,
  "jti": "7d0f5a2b-93c8-4f0e-9c33-1b6a0e6d5f10",
  "name": "Support Triage Agent",
  "namespace": "acme/support",
  "groups": ["support-eng"],
  "roles": ["responder"],
  "ctx": "channel:C0123456789"
}
# BEARER: anyone holding this within exp can redeem it once.
# mitigations: AS jti replay cache, short exp, valid only at /token.

## 3. AS response
{ "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 900 }

## 4. using it at the RS (many times, until it expires)

GET /docs/42 HTTP/1.1
Host: api.saas.example
Authorization: Bearer eyJ...access.token...

# RS untouched: it trusts its AS exactly as before.
Client                    Platform                       AS                    RS
  |                           |                           |                     |
  |<- (0) WAG assertion ------|                           |                     |
        one signed assertion per token request
  |                           |                           |                     |
  |-- (1) POST /token: assertion + resource ------------->|                     |
  |                           |                           |                     |
                                  (2) AS: iss allowlisted? JWKS by iss; unseen sub -> accept; jti replay?
  |                           |                           |                     |
  |<- (3) access token -----------------------------------|                     |
        ordinary bearer, aud = RS
  |                           |                           |                     |
  |-- (4) GET /resource with Authorization: Bearer ---------------------------->|
  |                           |                           |                     |
  |<- 200 ----------------------------------------------------------------------|
  |                           |                           |                     |
  |-- (5) ...more calls, same token, until it expires ------------------------->|
  |                           |                           |                     |
  |<- 200 ----------------------------------------------------------------------|
  |                           |                           |                     |
    (6) token expired -> new assertion, new token: repeat (0)-(3)
  |                           |                           |                     |
      1 assertion : 1 token request : many RS calls.  RS never changes.
view: |
additions marked vs: grant only
## 1. token request -- same as grant-only, plus one header

POST /token HTTP/1.1
Host: as.saas.example
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...            # proof #1, signed by CLIENT instance key
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIs...       # signed by PLATFORM per-tenancy issuer
&resource=https%3A%2F%2Fapi.saas.example%2F

## 2. decoded: the WAG grant (unchanged)
{ "alg": "ES256", "kid": "2026-07-14", "typ": "JWT" }
.
{
  "iss": "https://acme.agents.platform.example",
  "sub": "wimse://acme.agents.platform.example/agent/7f3d9a2e",
  "aud": ["https://as.saas.example",
          "https://as.saas.example/token"],
  "exp": 1785271980, "iat": 1785271680,
  "jti": "7d0f5a2b-93c8-4f0e-9c33-1b6a0e6d5f10",
  "name": "Support Triage Agent",
  "namespace": "acme/support",
  "groups": ["support-eng"],
  "roles": ["responder"],
  "ctx": "channel:C0123456789"
}
## 2b. decoded: DPoP proof #1 (token endpoint)
{ "typ": "dpop+jwt", "alg": "ES256",
  "jwk": { "kty":"EC", "crv":"P-256", "x":"...instance public key...", "y":"..." } }
.
{ "htm": "POST",
  "htu": "https://as.saas.example/token",
  "iat": 1785271680,
  "jti": "b3c1..." }
## 3. AS response
{ "access_token": "eyJ...", "token_type": "DPoP", "expires_in": 900 }
# token now carries cnf.jkt = thumbprint of the instance key

## 4. using it at the RS

GET /docs/42 HTTP/1.1
Host: api.saas.example
Authorization: DPoP eyJ...access.token...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...            # proof #2: htm GET, htu, ath=hash(token), jti

# RS-side change required: validate proof #2 against cnf.jkt.
# breaks "only the token endpoint changes".
# note: DPoP DOES protect access-token theft -- a stolen token is
# useless without the instance key. it does NOT protect a stolen
# assertion: a thief presents their own DPoP key and gets a fresh
# token bound to it. to close that, put cnf (instance key) in the
# assertion and have the AS require the DPoP key to match.
Client                    Platform                       AS                    RS
  |                           |                           |                     |
  |<- (0) WAG assertion ------|                           |                     |
  |                           |                           |                     |
  |-- (1) POST /token: assertion + DPoP proof#1 --------->|                     |
  |                           |                           |                     |
                                  (2) AS: grant checks; proof#1 sig; bind token to jkt(instance key)
  |                           |                           |                     |
  |<- (3) DPoP-bound token -------------------------------|                     |
  |                           |                           |                     |
  |-- (4) GET /resource: Authorization: DPoP + proof#2 (ath) ------------------>|
  |                           |                           |                     |
                                  (5) RS validates proof#2 vs cnf.jkt  <- RS-side change
  |                           |                           |                     |
  |<- (6) 200 ------------------------------------------------------------------|
  |                           |                           |                     |
view: |
additions marked vs: grant + DPoP
## 1. token request -- the full stack: Platform authenticates as an
## OAuth client (CIMD + private_key_jwt), carries the WAG grant, DPoP
## binds the issued token. stays open for grant-issuer = Enterprise IdP.

POST /token HTTP/1.1
Host: as.saas.example
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...            # proof #1, signed by CLIENT instance key
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIs...       # WAG grant (issuer could be the customer's IdP)
&resource=https%3A%2F%2Fapi.saas.example%2F
&client_id=https%3A%2F%2Fagents.platform.example%2Foauth%2Fclient   # the CIMD URL
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJFUzI1NiIs... # private_key_jwt, key published in the CIMD

## 2. decoded: the WAG grant (unchanged)
{ "alg": "ES256", "kid": "2026-07-14", "typ": "JWT" }
.
{
  "iss": "https://acme.agents.platform.example",
  "sub": "wimse://acme.agents.platform.example/agent/7f3d9a2e",
  "aud": ["https://as.saas.example",
          "https://as.saas.example/token"],
  "exp": 1785271980, "iat": 1785271680,
  "jti": "7d0f5a2b-93c8-4f0e-9c33-1b6a0e6d5f10",
  "name": "Support Triage Agent",
  "namespace": "acme/support",
  "groups": ["support-eng"],
  "roles": ["responder"],
  "ctx": "channel:C0123456789"
}
## 2b. decoded: DPoP proof #1 (unchanged from grant + DPoP)
{ "typ": "dpop+jwt", "alg": "ES256",
  "jwk": { "kty":"EC", "crv":"P-256", "x":"...instance public key...", "y":"..." } }
.
{ "htm": "POST", "htu": "https://as.saas.example/token", "iat": 1785271680, "jti": "b3c1..." }
## 2c. decoded: client_assertion (CLIENT AUTH slot -- authenticates the Platform)
{ "alg": "ES256", "kid": "platform-client-2026" }
.
{ "iss": "https://agents.platform.example/oauth/client",
  "sub": "https://agents.platform.example/oauth/client",   # iss = sub = client_id
  "aud": "https://as.saas.example",
  "exp": 1785271980, "jti": "9a4e..." }
## 2d. the CIMD, fetched by the AS from the client_id URL
{
  "client_id": "https://agents.platform.example/oauth/client",
  "client_name": "Acme Agents Platform",
  "token_endpoint_auth_method": "private_key_jwt",
  "jwks": { "keys": [ { "kty":"EC", "kid":"platform-client-2026", ... } ] }
}
# no registration step: the AS discovers the client by dereferencing
# client_id. same by-reference trust move as the WAG issuer allowlist.
## 3. AS response
{ "access_token": "eyJ...", "token_type": "DPoP", "expires_in": 900 }

## 4. using it at the RS (unchanged from grant + DPoP)

GET /docs/42 HTTP/1.1
Host: api.saas.example
Authorization: DPoP eyJ...access.token...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...            # proof #2: htm GET, htu, ath, jti

# slots recap: client_assertion answers "who is calling" (Platform);
# assertion answers "issue a token FOR this subject" (the agent);
# DPoP binds what comes back. three independent layers.
Client                    Platform                       AS                    RS
  |                           |                           |                     |
  |<- (0) WAG assertion ------|                           |                     |
  |                           |                           |                     |
  |-- (1) POST /token: client_id=CIMD-url + pkjwt ------->|                     |
  |                           |                           |                     |
            plus assertion, DPoP proof#1, resource
  |                           |                           |                     |
  |                           |<- (2) GET client_id URL --|                     |
  |                           |                           |                     |
  |                           |-- (3) CIMD doc (jwks) --->|                     |
  |                           |                           |                     |
                                  (4) AS: pkjwt vs CIMD jwks; iss allowlist; DPoP proof#1
  |                           |                           |                     |
  |<- (5) DPoP-bound token -------------------------------|                     |
  |                           |                           |                     |
  |-- (6) GET /resource: DPoP token + proof#2 --------------------------------->|
  |                           |                           |                     |
  |<- (7) 200 ------------------------------------------------------------------|
  |                           |                           |                     |
view: |
additions marked vs: grant only (alternative branch to DPoP)
## 0. ahead of time (platform-internal): instance makes a keypair,
##    attester binds it into a Client Attestation JWT

## 1. token request -- grant-only plus the two attestation headers

POST /token HTTP/1.1
Host: as.saas.example
OAuth-Client-Attestation: eyJ0eXAiOiJvYXV0aC1jbGllbnQtYXR0ZXN0YXRpb24... # signed by ATTESTER
OAuth-Client-Attestation-PoP: eyJ0eXAiOiJ...cG9w...                 # signed by CLIENT instance key
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIs...       # signed by PLATFORM per-tenancy issuer
&resource=https%3A%2F%2Fapi.saas.example%2F

## 2. decoded: the WAG grant (unchanged)
{ "alg": "ES256", "kid": "2026-07-14", "typ": "JWT" }
.
{
  "iss": "https://acme.agents.platform.example",
  "sub": "wimse://acme.agents.platform.example/agent/7f3d9a2e",
  "aud": ["https://as.saas.example",
          "https://as.saas.example/token"],
  "exp": 1785271980, "iat": 1785271680,
  "jti": "7d0f5a2b-93c8-4f0e-9c33-1b6a0e6d5f10",
  "name": "Support Triage Agent",
  "namespace": "acme/support",
  "groups": ["support-eng"],
  "roles": ["responder"],
  "ctx": "channel:C0123456789"
}
## 2b. decoded: Client Attestation JWT (attester vouches for the instance key)
{ "typ": "oauth-client-attestation+jwt", "alg": "ES256", "kid": "attester-2026-07" }
.
{ "iss": "https://attester.platform.example",
  "sub": "wimse://acme.agents.platform.example/agent/7f3d9a2e",
  "exp": 1785275280,
  "cnf": { "jwk": { "kty":"EC", "crv":"P-256", "x":"...instance key...", "y":"..." } } }
## 2c. decoded: Client Attestation PoP JWT
{ "typ": "oauth-client-attestation-pop+jwt", "alg": "ES256" }
.
{ "iss": "wimse://acme.agents.platform.example/agent/7f3d9a2e",
  "aud": "https://as.saas.example",
  "exp": 1785271980, "jti": "f81a..." }
# signed with the instance-held key that cnf above binds.
# a stolen WAG assertion without this signature gets nothing.
## 3. AS response
{ "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 900 }

## 4. using it at the RS (unchanged: plain bearer, RS untouched)

GET /docs/42 HTTP/1.1
Host: api.saas.example
Authorization: Bearer eyJ...access.token...
Client                    Attester                       AS                    RS
  |                           |                           |                     |
  |-- (1) instance pubkey --->|                           |                     |
  |                           |                           |                     |
  |<- (2) Attestation JWT ----|                           |                     |
        cnf = instance key
  |                           |                           |                     |
  |-- (3) POST /token: attestation hdrs + WAG grant ----->|                     |
  |                           |                           |                     |
            OAuth-Client-Attestation (attester-signed)
  |                           |                           |                     |
            OAuth-Client-Attestation-PoP (instance-signed)
  |                           |                           |                     |
                                  (4) AS: attester sig; PoP vs cnf; grant iss allowlist
  |                           |                           |                     |
  |<- (5) access token -----------------------------------|                     |
        plain bearer, aud = RS
  |                           |                           |                     |
  |-- (6) GET /resource with Authorization: Bearer ---------------------------->|
  |                           |                           |                     |
  |<- (7) 200 ------------------------------------------------------------------|
  |                           |                           |                     |
mermaid source
sequenceDiagram
  participant C as Client (instance)
  participant P as Attester (Platform)
  participant AS as Authorization Server
  participant RS as Resource Server
  C->>P: (1) instance public key
  P->>C: (2) Client Attestation JWT (cnf = instance key)
  C->>AS: (3) POST /token + attestation headers + WAG grant
  AS->>AS: (4) verify attester sig, PoP vs cnf, grant issuer allowlist
  AS->>C: (5) access token (short-lived, aud = RS)
  C->>RS: (6) request + Bearer token
  RS->>C: (7) resource

threat comparison

attacker steals...grant onlygrant + DPoPCIMD/pkjwt + grantCIMD/pkjwt + grant + DPoPattest + grant
the assertion alone
(log leak, queue, cross-domain handoff)
redeem once, within exp redeem with own DPoP key worthless — needs Platform client key*worthless — needs Platform client key* worthless — needs instance key
a whole token request
(inside TLS termination)
1 redemption if the thief wins the race (jti cache) replay window only; proof jti cache closes it replay window only; jti caches close itreplay window only; jti caches close it replay window only; PoP jti cache closes it
a durable key
(the long-lived secret)
no client key exists (issuer key = game over, same for every column) instance key → that instance's tokens only (no extra platform key) Platform client key → redeems every tenant's stolen assertionsPlatform client key → redeems every tenant's stolen assertions instance keys are per-instance, BUT the attester key is platform-wide (≈ issuer key) — as bad as the pkjwt client key
an access token usable (bearer) useless without instance key usable (bearer; no DPoP layer)useless (DPoP layer) usable (still bearer; add DPoP)
who must change AS only AS + RS AS onlyAS only (AS+RS with the DPoP layer) AS only
* only with the binding rule stated in the profile: assertions from allowlisted issuer I are accepted solely from its bound, authenticated client C. RFC 7523 does not link the grant to client authentication by itself.

Key insight of row 2: against a whole-request thief, every shape degrades to "replay within the freshness window" — the wire carries the pkjwt / PoP artifacts right next to the assertion, so signing buys nothing there. What actually defends that vector is short exp + jti replay caching, which the bearer floor already mandates.

So pkjwt (or attest) earns its keep only when the assertion can exist APART from the caller's key material — i.e. row 1 vectors:
  1. issuer and caller in different domains (Enterprise IdP issues the grant, Platform redeems it: the assertion crosses a boundary alone);
  2. assertion-at-rest leaks (logs, queues, analytics pipelines — the client key is never in those);
  3. the AS wants a caller identity for quota / kill-switch / audit, independent of issuer trust.
Single-domain, platform-fronted, wire-capture-only threat model: pkjwt adds little; jti + short exp already do the work. pkjwt vs attest is then just which boundary you're defending: issuer≠caller (pkjwt, platform key) vs platform≠instance (attest, per-instance keys).

Row 3 caveat: attest's per-instance advantage applies only to the EDGE keys. Its root (the attester key) is a platform-level durable key like the pkjwt client key — steal it and you can attest your own key for any sub, re-enabling every stolen assertion; if attester key == issuer key it is literally game over. At the root, pkjwt and attest are equivalent; only DPoP adds no extra platform-level key at all.