Support actor_token and id_token in RFC 8693 token exchange - #6331
Open
jhrozek wants to merge 3 commits into
Open
Support actor_token and id_token in RFC 8693 token exchange#6331jhrozek wants to merge 3 commits into
jhrozek wants to merge 3 commits into
Conversation
An agent calling the token-exchange grant could previously only be
identified by its own OAuth client credentials at the endpoint. RFC
8693 also defines an explicit actor_token: a second, self-issued JWT
the agent presents alongside the user's subject_token, giving the
exchange a request-level proof of possession distinct from client
authentication. The handler unconditionally rejected both actor_token
and actor_token_type before this change ("not yet supported"), and
only accepted urn:...:access_token/jwt as subject_token_type, so a
subject token minted as an OIDC id_token (a legitimate shape from many
IdPs) had no way to be exchanged.
What changed:
- Accept actor_token + actor_token_type instead of rejecting them
outright. resolveActorIdentity validates actor_token against the
server's own JWKS (self-issued only — an actor_token is never
accepted from an external trusted issuer) and requires its "sub" to
equal the authenticated client's ID before the exchange proceeds.
- Accept id_token as a valid subject_token_type value.
- Removed the now-superseded validateExchangeParams helper (the old,
actor_token-rejecting parameter validator) in favor of the new
validateFormParams/resolveActorIdentity split.
- Corrected docs/arch/token-delegation-act-chain.md, which still
described RFC 8693's chained-act-claim nesting as "not implemented"
— that landed separately in stacklok#6149 before this branch was rebased;
the doc had gone stale, not the behavior.
- Added an integration test proving actor_token composes correctly
with the configured-delegate-client relaxation (a client granted
blanket self-issued-token trust): a mismatched actor_token must
still be rejected during actor-identity resolution before delegation
consent is ever reached, so that blanket trust can never be misread
as also loosening the actor_token binding check.
What this enables: a client can additionally prove it holds a second,
independently-issued token bound to its own client_id at exchange time,
and subject tokens minted as id_tokens by IdPs that issue that shape
become exchangeable.
What this deliberately does NOT do, by design: actor_token's own claims
never flow into the delegated token's "act" claim. Because "sub" must
equal client.GetID(), the resulting actor identity is identical whether
or not actor_token is supplied — this is actor-token *confirmation*
(proof of possession), not RFC 8693's general actor-delegation use case
of asserting a distinct sub-client-granularity actor. That's a
scope boundary recorded in resolveActorIdentity's doc comment, not an
oversight.
No new server configuration is introduced — actor_token/actor_token_type
are request-time form parameters at /oauth/token, not RunConfig/CRD
fields. Example request, assuming a confidential client already
registered for the token-exchange grant:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<user's JWT>
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&actor_token=<agent's own self-issued JWT, sub=agent-client-id>
&actor_token_type=urn:ietf:params:oauth:token-type:jwt
&client_id=agent-client-id
&client_secret=...
The delegated access token's "act" claim is unaffected by actor_token's
presence — it always names the authenticated client:
"act": { "sub": "agent-client-id" }
Closes stacklok#5815
jhrozek
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
rdimitrov and
tgrunnagle
as code owners
August 14, 2026 14:45
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6331 +/- ##
==========================================
+ Coverage 72.94% 72.96% +0.01%
==========================================
Files 742 742
Lines 78236 78270 +34
==========================================
+ Hits 57070 57106 +36
+ Misses 17188 17182 -6
- Partials 3978 3982 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Neither followed docs/arch's numbered-and-indexed convention, neither was linked from docs/arch/README.md or referenced anywhere else. token-delegation-act-chain.md duplicated content already in docs/arch/17-token-exchange-delegation.md (nested provenance, depth cap). token-delegation-actor-id.md was an open design question (should act.sub be a SPIFFE URI) written as a doc file instead of a tracked issue - it belongs in the epic's issue tracker, not shipped as architecture documentation.
Extends the delegate-client e2e suite with a live HTTP round trip against the deployed pod: a matching self-issued actor_token still resolves to the delegate client as the recorded actor, and a mismatched actor_token is rejected with a real 400 from the running server, not just in unit tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An agent calling the token-exchange grant could previously only be
identified by its own OAuth client credentials at the endpoint. RFC
8693 also defines an explicit
actor_token: a second, self-issued JWTthe agent presents alongside the user's
subject_token, giving theexchange a request-level proof of possession distinct from client
authentication. The handler unconditionally rejected both
actor_tokenand
actor_token_typebefore this change ("not yet supported"), andonly accepted
urn:...:access_token/jwtassubject_token_type, soa subject token minted as an OIDC
id_token(a legitimate shape frommany IdPs) had no way to be exchanged.
What changed:
actor_token+actor_token_typeinstead of rejecting themoutright.
resolveActorIdentityvalidatesactor_tokenagainst theserver's own JWKS (self-issued only — an
actor_tokenis neveraccepted from an external trusted issuer) and requires its
subtoequal the authenticated client's ID before the exchange proceeds.
id_tokenas a validsubject_token_typevalue.validateExchangeParamshelper (the old,actor_token-rejecting parameter validator) in favor of the newvalidateFormParams/resolveActorIdentitysplit.docs/arch/token-delegation-act-chain.md, which stilldescribed RFC 8693's chained-
act-claim nesting as "not implemented"— that landed separately in Add a consent model for external OIDC subject tokens #6149 before this branch was rebased;
the doc had gone stale, not the behavior.
actor_tokencomposes correctlywith the configured-delegate-client relaxation (a client granted
blanket self-issued-token trust): a mismatched
actor_tokenmuststill be rejected during actor-identity resolution before delegation
consent is ever reached, so that blanket trust can never be misread
as also loosening the
actor_tokenbinding check.What this enables: a client can additionally prove it holds a
second, independently-issued token bound to its own
client_idatexchange time, and subject tokens minted as
id_tokens by IdPs thatissue that shape become exchangeable.
What this deliberately does NOT do, by design:
actor_token's ownclaims never flow into the delegated token's
actclaim. Becausesubmust equal
client.GetID(), the resulting actor identity is identicalwhether or not
actor_tokenis supplied — this is actor-tokenconfirmation (proof of possession), not RFC 8693's general
actor-delegation use case of asserting a distinct sub-client-granularity
actor. That's a scope boundary recorded in
resolveActorIdentity's doccomment, not an oversight.
No new server configuration is introduced —
actor_token/actor_token_typeare request-time form parameters at
/oauth/token, notRunConfig/CRDfields. Example request, assuming a confidential client already
registered for the token-exchange grant:
The delegated access token's
actclaim is unaffected byactor_token'spresence — it always names the authenticated client:
Fixes #5815
Type of change
Test plan
task test)task lint-fix)API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Does this introduce a user-facing change?
Yes: clients performing RFC 8693 token exchange against the embedded
authorization server can now supply
actor_token/actor_token_type(previously always rejected), and
id_tokenis now an acceptedsubject_token_typevalue in addition toaccess_token/jwt. Noexisting request shape changes behavior.