← marwandiallo.comlabs

Token-exchange playground

RFC 8693 OAuth 2.0 Token Exchange end-to-end. The IdP receives a user's passkey-bound subject_token and the agent's workload-attested actor_token, then mints a downscoped delegated token whose act claim records the agent identity. Edit any input — the three decoded JWTs and the claims diff re-run on every keystroke.

Tokens, side-by-side

Click any token to copy its compact JWS string. The signature segment is a deterministic demo hash (so the lab works offline); the JWT lab at /identity/jwt covers real signing.

subject_token
user, passkey-bound
header
alg:RS256
typ:JWT
kid:idp-2026-05
payload
iss:https://idp.lab.marwandiallo.com
sub:u_marwan
email:marwan@example.com
aud:agents.idp.lab.marwandiallo.com
iat:1780871298
exp:1780874898
amr:["passkey","hwk"]
acr:AAL3
jti:usr-u_marwan-1780871298
actor_token
agent workload, attested
header
alg:RS256
typ:JWT
kid:wl-github-oidc
payload
iss:https://idp.lab.marwandiallo.com
sub:spiffe://prod/agent/code-reviewer/v3
azp:a_code_reviewer
aud:https://idp.lab.marwandiallo.com
iat:1780871298
exp:1780871898
attestation:github-oidc
scope:read:repo write:issues
jti:wl-a_code_reviewer-1780871298
access_token (exchanged)
delegated, downscoped, time-bound
header
alg:RS256
typ:JWT
kid:idp-2026-05
payload
iss:https://idp.lab.marwandiallo.com
sub:u_marwan
azp:a_code_reviewer
aud:api.github.com
scope:read:repo write:issues
iat:1780871298
exp:1780871898
jti:1d86d1a0-88bb-435c-9713-cfbfcb8a2e72
act:{"sub":"spiffe://prod/agent/code-reviewer/v3","azp":"a_code_reviewer","iss":"https://idp.lab.marwandiallo.com","attestation":"github-oidc"}
cnf:{"jkt":"demo-thumbprint-code_r"}

Claims diff

Every claim in the exchanged token, coloured by origin. The point of token exchange is downscoping plus attribution — both should be visible here.

claimoriginvaluewhy
issminted by STShttps://idp.lab.marwandiallo.comRe-issued by the STS. Receiving services trust the STS, not the workload directly.
subfrom subject_tokenu_marwanInherited from subject_token. RFC 8693 §1.2 requires the principal to remain the user.
azpminted by STSa_code_reviewerAuthorized party — the agent's IdP client_id. Resources can pin azp to a specific agent.
audminted by STSapi.github.comMinted for this exchange. The actor_token's own aud was the STS itself; the resulting token is bound to the requested resource.
scopenarrowed by STSread:repo write:issuesSubset of actor_token.scope. Token exchange exists to downscope, not preserve full authority.
iatminted by STS1780871298Fresh per-exchange. exp drives the agent token's TTL; jti enables replay defence.
expminted by STS1780871898Fresh per-exchange. exp drives the agent token's TTL; jti enables replay defence.
jtiminted by STS1d86d1a0-88bb-435c-9713-cfbfcb8a2e72Fresh per-exchange. exp drives the agent token's TTL; jti enables replay defence.
actfrom actor_token{"sub":"spiffe://prod/agent/code-reviewer/v3","azp":"a_code_reviewer","iss":"https://idp.lab.marwandiallo.com","attestation":"github-oidc"}Synthesised by the STS from actor_token. The act claim makes user→agent delegation visible in audit.
cnfminted by STS{"jkt":"demo-thumbprint-code_r"}Sender-constraint (RFC 9449 DPoP / mTLS). Receiving services reject the token if presented by a key the cnf doesn't bind.

RFC 8693 request

POST /token HTTP/1.1
Host: idp.lab.marwandiallo.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
&subject_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImlkcC0yMDI2LTA1In0.eyJpc3MiOiJodHRwczovL2lkcC5sYWIubWFyd2FuZGlhbGxvLmNvbSIsInN1YiI6InVfbWFyd2FuIiwiZW1haWwiOiJtYXJ3YW5AZXhhbXBsZS5jb20iLCJhdWQiOiJhZ2VudHMuaWRwLmxhYi5tYXJ3YW5kaWFsbG8uY29tIiwiaWF0IjoxNzgwODcxMjk4LCJleHAiOjE3ODA4NzQ4OTgsImFtciI6WyJwYXNza2V5IiwiaHdrIl0sImFjciI6IkFBTDMiLCJqdGkiOiJ1c3ItdV9tYXJ3YW4tMTc4MDg3MTI5OCJ9.ZGVtby1kODg1OTk0ZA
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
&actor_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IndsLWdpdGh1Yi1vaWRjIn0.eyJpc3MiOiJodHRwczovL2lkcC5sYWIubWFyd2FuZGlhbGxvLmNvbSIsInN1YiI6InNwaWZmZTovL3Byb2QvYWdlbnQvY29kZS1yZXZpZXdlci92MyIsImF6cCI6ImFfY29kZV9yZXZpZXdlciIsImF1ZCI6Imh0dHBzOi8vaWRwLmxhYi5tYXJ3YW5kaWFsbG8uY29tIiwiaWF0IjoxNzgwODcxMjk4LCJleHAiOjE3ODA4NzE4OTgsImF0dGVzdGF0aW9uIjoiZ2l0aHViLW9pZGMiLCJzY29wZSI6InJlYWQ6cmVwbyB3cml0ZTppc3N1ZXMiLCJqdGkiOiJ3bC1hX2NvZGVfcmV2aWV3ZXItMTc4MDg3MTI5OCJ9.ZGVtby0wZWM1YjcwYw
&actor_token_type=urn:ietf:params:oauth:token-type:jwt
&audience=api.github.com
&scope=read%3Arepo%20write%3Aissues

Audit log line

2026-06-07T22:28:18.000Z principal=u_marwan acting_as=spiffe://prod/agent/code-reviewer/v3 attestation=github-oidc aud=api.github.com scope="read:repo write:issues" ttl=600s jti=1d86d1a0-88bb-435c-9713-cfbfcb8a2e72

Without act, the same line would read principal=u_marwan with no record of the agent — making the call indistinguishable from the user typing it themselves.

References