Skip to content

fix: key STS token cache by acting subject, not session alone - #2459

Open
QuentinBisson wants to merge 4 commits into
kagent-dev:mainfrom
QuentinBisson:fix/sts-token-cache-per-subject-rebased
Open

fix: key STS token cache by acting subject, not session alone#2459
QuentinBisson wants to merge 4 commits into
kagent-dev:mainfrom
QuentinBisson:fix/sts-token-cache-per-subject-rebased

Conversation

@QuentinBisson

@QuentinBisson QuentinBisson commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Supersedes #2317 (auto-closed by stale-bot; rebase onto current main hit real conflicts with #2106's resource/audience support, so GitHub would not let the original be reopened).

Ref #2181 (Go side; the Python counterpart #2460 carries the closing reference).

What

A session shared by multiple subjects (e.g. multiple users routed through the same A2A session) currently runs every caller's tool calls under whichever subject's exchanged token was cached first. The STS token cache was keyed only by session ID.

This keys the cache by (sessionID, subject), where subject is derived from the acting bearer token's issuer-scoped sub claim (or a hash of the raw token for opaque/sub-less tokens). Both BeforeRunCallback (exchange path) and HeaderProvider (MCP header injection path) resolve the acting subject from the request's own bearer before doing the cache lookup, so each caller gets its own exchanged token.

HeaderProvider now also recovers the bearer from the A2A CallContext when models.BearerTokenKey isn't threaded to the MCP request context, matching what the round-tripper's propagate-token path already reads.

Cache lifetime

Keying by (sessionID, subject) holds one entry per caller instead of one per
session, so an entry whose token carries no exp claim would pin a slot per
caller for the lifetime of the process. Those entries now get a bounded
lifetime, which keeps every entry evictable.

AfterRunCallback tracks the earliest expiry in the cache and only walks it
once something can actually be evicted, matching the Python plugin in #2460.

An absent iss leaves sub unqualified, so issuer-less tokens partition by
token hash rather than by a key that two issuers omitting iss could both
produce.

go-unit-tests is red on main as well (CRD validation in go/api/v1alpha2
and go/api/v1alpha3); it is not caused by this diff.

@QuentinBisson
QuentinBisson marked this pull request as ready for review August 17, 2026 13:07
Copilot AI lite review requested due to automatic review settings August 17, 2026 13:07
Rebased onto current main; adapts to the RFC 8707 resource/audience
constructor params and the a2a-go v2 CallContext API.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
@QuentinBisson
QuentinBisson force-pushed the fix/sts-token-cache-per-subject-rebased branch from ab5600f to 5503c06 Compare August 17, 2026 13:08
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Aug 17, 2026
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Aug 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a cross-subject identity leak in the Go STS token-propagation plugin by ensuring exchanged (delegated) tokens are cached per (session, acting subject) rather than per session alone. This aligns token propagation with the shared-session model where different callers within the same session must not inherit each other’s delegated authority.

Changes:

  • Re-keys the STS token cache from sessionID to a composite (sessionID, subject) key, where subject is derived from the acting bearer’s issuer-scoped sub (or a hash fallback for opaque/sub-less tokens).
  • Ensures both the exchange path (BeforeRunCallback) and MCP injection path (HeaderProvider) resolve the acting bearer/subject before cache lookup, including a CallContext fallback for transport-layer requests.
  • Updates cache cleanup behavior to sweep expired entries across all (session, subject) entries and adds/updates unit tests for the new behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
go/adk/pkg/sts/plugin.go Implements per-(session, subject) caching and acting-bearer recovery for both exchange and header injection paths.
go/adk/pkg/sts/plugin_test.go Adds coverage for issuer-scoped subject partitioning, shared-session per-subject behavior, and CallContext-based bearer recovery.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Keying the cache by (session, subject) multiplies entries per session, so an
entry whose token carries no exp claim now pins one slot per caller instead of
one per session. Give those a bounded lifetime so every entry stays evictable.

Track the earliest expiry so AfterRunCallback only walks the cache once
something can actually be evicted, matching the Python plugin.

An issuer-less token leaves sub unqualified; those partition by token hash
rather than by a key two issuers could both produce.

Signed-off-by: Quentin Bisson <quentin@giantswarm.io>
@QuentinBisson
QuentinBisson force-pushed the fix/sts-token-cache-per-subject-rebased branch from 74ccaaa to 7ac8cd3 Compare August 17, 2026 19:46
An empty subject identifies no principal, so an entry stored under it
would be shared by every credential-less caller in a session. The cache
accessors now refuse it.

Name the key's input for what it is, the credential the request
authenticates with, rather than the caller's bearer specifically.

Signed-off-by: Quentin Bisson <quentin@giantswarm.io>
@QuentinBisson
QuentinBisson force-pushed the fix/sts-token-cache-per-subject-rebased branch from 7ac8cd3 to a63d354 Compare August 17, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants