You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
STS token cache keyed by session ID collapses per-user identity in shared sessions #2181
The STS token-propagation plugin caches the exchanged (delegated) token keyed by session ID, so a session shared by multiple human users runs every user's tool calls under the first user's delegated identity.
Detail
go/adk/pkg/sts/plugin.go:
tokenCache map[string]*TokenCacheEntry // keyed by session ID
BeforeRunCallback computes sessionID = ctx.Session().ID(), and on a cache hit returns the cached token without re-exchanging the current request's bearer:
A single ADK/A2A session (contextID) can legitimately receive messages from different subjects: UserIDCallInterceptor sets CallContext.User per request from x-user-id, and withBearerToken reads the Authorization bearer per request. But once the first request seeds tokenCache[sessionID], later requests with a different subject reuse that entry, so their MCP tool calls (via headerRoundTripper / STS headerProvider) execute under the first subject's exchanged token. On cache expiry the next request re-seeds it, so attribution can also drift nondeterministically.
Proposed fix
Key the exchanged-token cache by sessionID + subject (the incoming bearer's/x-user-id's subject), or bypass the cache when the incoming subject differs from the cached entry's. That lets a shared session multiplex per-user identity and lets downstreams drop the consent workaround.
No, we're not using the #1935 share-session feature, and I don't think it covers this case, though it's closely related.
Context: we run a gateway in front of kagent that maps a chat thread to a single A2A session (contextID). Several people collaborate in that one thread, and the gateway forwards each sender's own bearer token per message. So it's a genuine multi-user single-session case, but it arrives over A2A rather than through the UI share flow.
As I read #1935, it deliberately runs a shared session under the owner's identity rather than the caller's:
ShareContext.UserID is the owner's ID, and A2ARequestHandler forces it onto the callback: if sc, ok := ShareContextFrom(ctx); ok { req.Header.Set("X-User-Id", sc.UserID) }.
The caller's identity is recorded only as msg.Metadata["initiated_by"] (attribution), not for authorization.
So even driven via a share token, tool calls would still execute as the owner, which is the thing we need to avoid.
The underlying issue looks like two halves, and the token cache is only one of them:
Session state is keyed by userID (executor.go: GetSession(appName, userID, sessionID)), so a shared conversation needs a stable userID, which forces a single identity.
The STS exchanged token is cached per session (sts/plugin.go: tokenCache keyed by session ID), so the first caller's exchange is reused for everyone.
What would unblock the interactive multi-user case is decoupling the conversation/session key from the per-request acting subject, and keying the STS exchange/cache by that subject, so one shared session can carry per-message identity for tool-call authorization.
Happy to expand this issue to cover both halves (or split them) and to help on the implementation. One design question: is per-user acting identity within a single shared session something you'd want to support, or is the intended model that multiple users always fork into separate sessions?
Activity
EItanya commentedon Jul 9, 2026
Are you using the new shared session feature introduced by @onematchfox in #1935?
QuentinBisson commentedon Jul 9, 2026
No, we're not using the #1935 share-session feature, and I don't think it covers this case, though it's closely related.
Context: we run a gateway in front of kagent that maps a chat thread to a single A2A session (
contextID). Several people collaborate in that one thread, and the gateway forwards each sender's own bearer token per message. So it's a genuine multi-user single-session case, but it arrives over A2A rather than through the UI share flow.As I read #1935, it deliberately runs a shared session under the owner's identity rather than the caller's:
ShareContext.UserIDis the owner's ID, andA2ARequestHandlerforces it onto the callback:if sc, ok := ShareContextFrom(ctx); ok { req.Header.Set("X-User-Id", sc.UserID) }.msg.Metadata["initiated_by"](attribution), not for authorization.So even driven via a share token, tool calls would still execute as the owner, which is the thing we need to avoid.
The underlying issue looks like two halves, and the token cache is only one of them:
userID(executor.go:GetSession(appName, userID, sessionID)), so a shared conversation needs a stableuserID, which forces a single identity.sts/plugin.go:tokenCachekeyed by session ID), so the first caller's exchange is reused for everyone.What would unblock the interactive multi-user case is decoupling the conversation/session key from the per-request acting subject, and keying the STS exchange/cache by that subject, so one shared session can carry per-message identity for tool-call authorization.
Happy to expand this issue to cover both halves (or split them) and to help on the implementation. One design question: is per-user acting identity within a single shared session something you'd want to support, or is the intended model that multiple users always fork into separate sessions?
docs(slack): gate interim owner-only checks on kagent#1933 and #2181
fix(slack): close silent-drop and stranded-state gaps in the adapter (#…
fix: key STS token cache by acting subject, not session alone (kagent…
fix: key STS token cache by acting subject, not session alone (kagent…
fix(sts): key the ADK token cache by acting subject, not session alon…