Uh oh!
There was an error while loading. Please reload this page.
Support actor_token in RFC 8693 token exchange - #6331
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@## main #6331 +/- ##
==========================================
+ Coverage 72.96% 73.01% +0.04%
==========================================
Files 742 742 Lines 78236 78271 +35 ==========================================
+ Hits 57085 57146 +61 + Misses 17172 17130 -42 - Partials 3979 3995 +16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
JAORMX
left a comment
There was a problem hiding this comment.
Panel review against RFC 8693, RFC 8725, RFC 9700, OIDC Core errata set 2, and the current IANA OAuth registry. The ID-token validation issue is the merge blocker; the other comments are concrete conformance and repository-rule fixes.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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" }
Closesstacklok#5815Neither 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.
resolveActorIdentity required an actor_token's "sub" to equal the authenticated client ID, making the token's own client_id claim useless and collapsing actor_token to a no-op self-check that could never assert an actor distinct from the OAuth client. Bind on the actor_token's "client_id" claim instead (the same proof-of-possession role a normal token's client_id plays) and let "sub" flow through as the actor identity, so actor_token can name a delegate persona distinct from the authenticated client while still proving it was minted for that client.
subject_token_type=id_token was accepted but validated with the exact same access-token profile as any other subject token, silently ignoring the semantic differences RFC 8693 assigns to id_token (e.g. an ID token's aud names the relying-party client, not a resource). Rather than validate an ID token as if it were an access token, decline the type until a real ID-token validation profile exists — matching the existing actor_token_type restriction.
The token-exchange delegation doc never covered actor_token at all, and didn't explain why id_token is rejected as a subject/actor token type. Document the client_id/sub split resolveActorIdentity now enforces and the rationale for declining id_token until a real validation profile exists.
Uh oh!
There was an error while loading. Please reload this page.
JAORMX
left a comment
There was a problem hiding this comment.
The earlier ID-token validation, RFC 8693 invalid_request, terminology, and response-drain comments are addressed. One authorization blocker remains: the new distinct actor subject is being reused as the authenticated client identity for delegate policy and the emitted client_id; see the inline comment.
Before merge, please also rebase onto current main so #6333/#6334 release/workflow/chart changes disappear from this PR, and update the title/body: they still advertise id_token support, sub == client_id, and proof-of-possession behavior that the branch now intentionally removed.
resolveActorIdentity's actorSub (the actor_token's asserted identity, which the prior fix let differ from the authenticated client) was being reused as the authenticated client identity for delegate-client policy, AllowedDelegateClients checks, subject-token client_id binding, and the issued token's own client_id. That let a client exchange a subject token issued to a completely different client, simply by presenting an actor_token whose sub happened to equal that subject token's client_id claim — a full bypass of the "subject token was issued to a different client" binding. Route client.GetID() to every policy/binding decision, and reserve actorSub for what it is actually meant to represent: may_act.sub and the emitted act.sub claim.
011aaf5 to
aec1464Compare
JAORMX
left a comment
There was a problem hiding this comment.
Verified aec146474: the authenticated OAuth client now remains authoritative for configured/allowed delegate policy, subject-token client_id binding, and the issued RFC 9068 client_id; the asserted actor is limited to may_act.sub and act.sub. The new regression tests cover the original actor-sub collision bypass. The branch is rebased, unrelated release/workflow files are gone, and the title/body now match the implementation.
The remaining failing unit-test check is GitHub infrastructure: the job never reached checkout/tests because codeload repeatedly returned 429/503 while downloading actions. Security, lint, codegen, docs, Helm, and two lifecycle matrices pass; the third lifecycle matrix is still running.
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, letting theexchange name a specific acting-agent identity distinct from the
authenticated OAuth client itself (RFC 8693 §2.1's actor-delegation
use case). The handler unconditionally rejected both
actor_tokenandactor_token_typebefore this change ("not yet supported").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). The binding check requires
the actor_token's own
client_idclaim to equal the authenticatedclient's ID (proving the token was minted for this client); its
subclaim is returned as the asserted actor identity, which mayname a specific agent instance or delegate persona distinct from the
client's own ID.
may_act.submatching and the emitted
act.subclaim. Every client-identity policydecision — the delegate-client allowlists (
configuredDelegateClients,AllowedDelegateClients), and the subject token's ownclient_idbinding — is bound to the authenticated OAuth client (
client.GetID()),never to the asserted actor. Conflating the two would let a client
exchange a subject token issued to a different client, simply by
presenting an
actor_tokenwhosesubhappens to equal that subjecttoken's
client_idclaim — a full bypass of the "subject token wasissued to a different client" check. A regression test
(
actor_token cannot bypass subject-token client_id binding) pinsthis.
id_tokenremains rejected as asubject_token_typevalue: an IDtoken's claim conventions differ from an access token's (e.g.
audnames the relying-party client, not a resource), and this validator
applies neither a distinct validation profile nor id_token-specific
claim mapping. Accepting the type without that profile would silently
validate an ID token exactly like an access token, ignoring the
declared type's semantics. The same reasoning excludes
id_tokenforactor_token_type: an actor presents a bearer credential(
access_token/jwt), not an identity assertion.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_tokencomposingcorrectly with the configured-delegate-client relaxation (a
mismatched
actor_tokenis still rejected during actor-identityresolution, before delegation consent is ever reached); the asserted
actor flowing into
act.subwhile the issued token's ownclient_idstill names the authenticated client; and the client_id-binding
bypass regression above.
What this enables: a client can present a second, independently-issued
token that both proves it was minted for that client and names a more
specific actor (e.g. a particular agent instance) than the client's own
ID — without that asserted actor ever being able to influence which
subject tokens the client is allowed to exchange.
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 carries the asserted actoridentity; its own
client_idstill 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) to assert a specific actor identity distinct
from their own client ID.
id_tokenremains rejected as asubject_token_type/actor_token_typevalue. No existing request shapechanges behavior.