fix(sts): deliver the exchanged STS token on MCP tool calls - #2795
Open
ricardomiguel-os wants to merge 1 commit into
Open
fix(sts): deliver the exchanged STS token on MCP tool calls#2795ricardomiguel-os wants to merge 1 commit into
ricardomiguel-os wants to merge 1 commit into
Conversation
The STS token exchange succeeds and the exchanged token is cached, but it never
leaves the pod: MCP tool calls go out carrying no Authorization header at all, so
the upstream server never sees the user delegated to the agent.
HeaderProvider recovered the session with a direct type assertion:
ctx.(interface{ SessionID() string })
That inspects only the context object in hand, without consulting its parents.
createTransport always builds its http.Client with a non-zero Timeout
(defaultTimeout is 30m), and http.Client re-wraps the request context in a
deadline context whenever Timeout > 0 (net/http.setRequestCancel ->
context.WithDeadline). By the time RoundTrip runs, the context in hand is that
wrapper, not ADK's ToolContext, so the assertion never matched, HeaderProvider
returned nil, and no header was injected. No configuration avoids it: the
timeout is non-zero even when unset. The neighbouring propagateToken path keeps
working because it uses a2asrv.CallContextFrom, a Value lookup, and Value
lookups traverse the parent chain.
Store the session ID as a context value in the A2A executor, beside the bearer
token it already stamps, and prefer that lookup in sessionIDFromContext. The
type assertion is retained as a fallback so callers still holding ADK's
ToolContext are unaffected.
Behaviour for requests with no user in context, such as the toolset discovery
performed at startup, is unchanged: no header is injected.
One supporting change: models.contextKey now carries a name. BearerTokenKey and
SessionIDKey are both pointers to it, and pointers to zero-size structs may
share an address, which made the two keys compare equal and let the session
stamp overwrite the bearer token. Caught by the new end-to-end test, which saw
the session ID arrive at the STS as the subject token.
Tests:
- mcp: end-to-end regression driving a real runner, llmagent, mcptoolset,
CreateToolsets/createTransport, TokenPropagationPlugin and MCP server,
asserting the exchanged token reaches the tool. With session recovery by type
assertion alone, every invocation request goes out with an empty Authorization
header.
- sts: HeaderProvider session recovery, including the deadline-wrapped case.
- a2a: the executor stamps the session ID, and the two context keys coexist.
Signed-off-by: Ricardo Gonçalves (rng) <ricardo.goncalves@outsystems.com>
ricardomiguel-os
force-pushed
the
fix/sts-mcp-session-id-context-value
branch
from
September 10, 2026 16:35
31cbb5c to
61ce6e7
Compare
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.
Problem
The STS token exchange succeeds and the exchanged token is cached, but it never leaves the pod. MCP tool calls go out with no
Authorizationheader at all, so the upstream server sees no identity instead of the user delegated to the agent.sessionIDFromContextrecovered the session with a direct type assertion:A type assertion inspects only the context object in hand.
createTransportalways builds itshttp.Clientwith a non-zeroTimeout(defaultTimeoutis 30m), andhttp.Clientre-wraps the request context in a deadline context wheneverTimeout > 0(net/http.setRequestCancel→context.WithDeadline). By the timeRoundTripruns, the context in hand is that wrapper, not ADK'sToolContext, so the assertion never matches andHeaderProviderreturnsnil.No configuration avoids this: the timeout is non-zero even when unset.
The neighbouring
propagateTokenpath keeps working because it usesa2asrv.CallContextFrom, aValuelookup, andValuelookups traverse the parent chain.Fix
a2a/executor.gostamps the session ID as a context value, beside the bearer token it already stamps.sts/plugin.goprefers thatValuelookup insessionIDFromContext, keeping the type assertion as a fallback so callers still holding ADK'sToolContextare unaffected.models/base.gogivescontextKeya name.BearerTokenKeyandSessionIDKeywould otherwise both be pointers to a zero-size struct, and Go may place those at the same address, so the two keys compare equal and the session stamp overwrites the bearer token. The end-to-end test caught this by seeing the session ID arrive at the STS assubject_token.The token cache key, its write side and its eviction are untouched.
NewTokenPropagationPlugin's signature is unchanged.Requests with no user in context, such as the toolset discovery performed at startup, still get no header. That behaviour is asserted in the new test rather than changed.
Evidence
Red/green on the end-to-end test, reverting only the
Valuelookup insessionIDFromContext:With the fix:
go test -race -skip 'TestE2E.*' ./adk/...green across all packages,golangci-lint runreports 0 issues,go build ./...andgofmtclean.Tests
mcp/sts_injection_test.go— end-to-end, nothing stubbed but the LLM: realrunner.Runner,llmagent,mcptoolset, the realCreateToolsets/createTransport(so a realhttp.Clientwith the deadline wrapper), realsts.TokenPropagationPluginperforming a real exchange against anhttptestSTS, and a real MCP server recording everyAuthorizationit receives.sts/plugin_test.go— session recovery precedence: session as a value, the same wrapped in a deadline context, session only viaSessionID(), no session, session present with no cached token, and value-beats-method.a2a/executor_test.go— the executor stamps the session ID, and the two context keys coexist without overwriting each other.