From db70bef2389703d3ffd7c90b2cdeb524885e26f1 Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Mon, 29 Jun 2026 11:20:20 +0200 Subject: [PATCH 1/2] feat(sts): send RFC 8707 resource and audience on token exchange The Python token-propagation plugin omitted resource/audience on every STS token exchange, so issued tokens could not be scoped to a target backend. Accept resource/audience on ADKTokenPropagationPlugin, read them from KAGENT_TOKEN_RESOURCE / KAGENT_TOKEN_AUDIENCE in the CLI, and pass them to exchange_token. Unset values are omitted, so behaviour is unchanged by default. Signed-off-by: QuentinBisson --- .../agentsts-adk/src/agentsts/adk/_base.py | 14 +++++++- .../tests/test_adk_integration.py | 35 +++++++++++++++++++ .../packages/kagent-adk/src/kagent/adk/cli.py | 4 ++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/python/packages/agentsts-adk/src/agentsts/adk/_base.py b/python/packages/agentsts-adk/src/agentsts/adk/_base.py index 9c84975497..29e9a1e919 100644 --- a/python/packages/agentsts-adk/src/agentsts/adk/_base.py +++ b/python/packages/agentsts-adk/src/agentsts/adk/_base.py @@ -105,14 +105,24 @@ def __init__(self, token: str, expiry: Optional[int] = None): class ADKTokenPropagationPlugin(BasePlugin): """Plugin for propagating STS tokens to ADK tools.""" - def __init__(self, sts_integration: Optional[STSIntegrationBase] = None): + def __init__( + self, + sts_integration: Optional[STSIntegrationBase] = None, + resource: Optional[Union[str, list]] = None, + audience: Optional[Union[str, list]] = None, + ): """Initialize the token propagation plugin. Args: sts_integration: The ADK STS integration instance + resource: RFC 8707 resource indicator sent on the token exchange to + scope the issued token to a target backend. Omitted when None. + audience: RFC 8693 audience sent on the token exchange. Omitted when None. """ super().__init__("ADKTokenPropagationPlugin") self.sts_integration = sts_integration + self.resource = resource + self.audience = audience self.token_cache: Dict[str, _TokenCacheEntry] = {} self.actor_token_cache: Optional[_TokenCacheEntry] = None @@ -192,6 +202,8 @@ async def before_run_callback( subject_token_type=TokenType.JWT, actor_token=actor_token, actor_token_type=TokenType.JWT if actor_token else None, + resource=self.resource, + audience=self.audience, ) except Exception as e: logger.warning(f"STS token exchange failed: {e}") diff --git a/python/packages/agentsts-adk/tests/test_adk_integration.py b/python/packages/agentsts-adk/tests/test_adk_integration.py index 6915041c39..7b5d0c6e8e 100644 --- a/python/packages/agentsts-adk/tests/test_adk_integration.py +++ b/python/packages/agentsts-adk/tests/test_adk_integration.py @@ -75,10 +75,37 @@ async def test_subject_token_from_callback(self): subject_token_type=TokenType.JWT, actor_token="actor-token", actor_token_type=TokenType.JWT, + resource=None, + audience=None, ) assert "sess-key-1" in plugin.token_cache assert plugin.token_cache["sess-key-1"].token == "exchanged-token" + @pytest.mark.asyncio + async def test_resource_and_audience_passed_to_exchange(self): + """Configured resource/audience are forwarded to the STS exchange (RFC 8707/8693).""" + sts = Mock(spec=ADKSTSIntegration) + sts.get_subject_token = lambda state: state.get("subject-token") + sts.fetch_actor_token = None + sts._actor_token = "actor-token" + sts.exchange_token = AsyncMock(return_value="exchanged-token") + plugin = ADKTokenPropagationPlugin(sts, resource="https://mcp.example.com", audience="mcp-backend") + ic = self._make_invocation_context( + "sess-resource", + headers=None, + extra_state={"subject-token": "subject-jwt"}, + ) + result = await plugin.before_run_callback(invocation_context=ic) + assert result is None + sts.exchange_token.assert_called_once_with( + subject_token="subject-jwt", + subject_token_type=TokenType.JWT, + actor_token="actor-token", + actor_token_type=TokenType.JWT, + resource="https://mcp.example.com", + audience="mcp-backend", + ) + @pytest.mark.asyncio async def test_subject_token_callback_returns_none(self): """Case: get_subject_token callback returns None -> returns None.""" @@ -124,6 +151,8 @@ async def test_default_callback_extracts_from_headers(self): subject_token_type=TokenType.JWT, actor_token="actor-token", actor_token_type=TokenType.JWT, + resource=None, + audience=None, ) assert plugin.token_cache["sess-key-4"].token == "exchanged-via-headers" @@ -173,6 +202,8 @@ async def test_sts_token_exchange_success(self): subject_token_type=TokenType.JWT, actor_token="actor-token", actor_token_type=TokenType.JWT, + resource=None, + audience=None, ) # optional debug log length check mock_logger.debug.assert_called() # at least one debug log @@ -260,6 +291,8 @@ async def test_dynamic_token_fetch_success_sync(self): subject_token_type=TokenType.JWT, actor_token="dynamic-actor-token", actor_token_type=TokenType.JWT, + resource=None, + audience=None, ) # Verify debug log for dynamic fetch @@ -297,6 +330,8 @@ async def async_fetch_token(): subject_token_type=TokenType.JWT, actor_token="dynamic-actor-token-async", actor_token_type=TokenType.JWT, + resource=None, + audience=None, ) # Verify debug log for dynamic fetch diff --git a/python/packages/kagent-adk/src/kagent/adk/cli.py b/python/packages/kagent-adk/src/kagent/adk/cli.py index e32d0aacbf..a9ad78ca8e 100644 --- a/python/packages/kagent-adk/src/kagent/adk/cli.py +++ b/python/packages/kagent-adk/src/kagent/adk/cli.py @@ -25,6 +25,8 @@ kagent_url_override = os.getenv("KAGENT_URL") sts_well_known_uri = os.getenv("STS_WELL_KNOWN_URI") propagate_token = os.getenv("KAGENT_PROPAGATE_TOKEN", "").lower() == "true" +token_resource = os.getenv("KAGENT_TOKEN_RESOURCE") or None +token_audience = os.getenv("KAGENT_TOKEN_AUDIENCE") or None uvicorn_log_level = os.getenv("UVICORN_LOG_LEVEL", os.getenv("LOG_LEVEL", "info")).lower() @@ -33,7 +35,7 @@ def create_sts_integration() -> Optional[ADKTokenPropagationPlugin]: sts_integration = None if sts_well_known_uri: sts_integration = ADKSTSIntegration(sts_well_known_uri) - return ADKTokenPropagationPlugin(sts_integration) + return ADKTokenPropagationPlugin(sts_integration, resource=token_resource, audience=token_audience) def maybe_add_skills(root_agent: BaseAgent): From b459383896fbdd1283ebeb64a688cb522f59283d Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Tue, 7 Jul 2026 19:18:30 +0200 Subject: [PATCH 2/2] refactor(sts): rename token-exchange env vars to KAGENT_STS_* KAGENT_TOKEN_RESOURCE/AUDIENCE become KAGENT_STS_RESOURCE/AUDIENCE so the variable names make clear they belong to the STS token exchange. Signed-off-by: QuentinBisson --- python/packages/kagent-adk/src/kagent/adk/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/packages/kagent-adk/src/kagent/adk/cli.py b/python/packages/kagent-adk/src/kagent/adk/cli.py index a9ad78ca8e..00a4f31c60 100644 --- a/python/packages/kagent-adk/src/kagent/adk/cli.py +++ b/python/packages/kagent-adk/src/kagent/adk/cli.py @@ -25,8 +25,8 @@ kagent_url_override = os.getenv("KAGENT_URL") sts_well_known_uri = os.getenv("STS_WELL_KNOWN_URI") propagate_token = os.getenv("KAGENT_PROPAGATE_TOKEN", "").lower() == "true" -token_resource = os.getenv("KAGENT_TOKEN_RESOURCE") or None -token_audience = os.getenv("KAGENT_TOKEN_AUDIENCE") or None +token_resource = os.getenv("KAGENT_STS_RESOURCE") or None +token_audience = os.getenv("KAGENT_STS_AUDIENCE") or None uvicorn_log_level = os.getenv("UVICORN_LOG_LEVEL", os.getenv("LOG_LEVEL", "info")).lower()