Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion python/packages/agentsts-adk/src/agentsts/adk/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}")
Expand Down
35 changes: 35 additions & 0 deletions python/packages/agentsts-adk/tests/test_adk_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion python/packages/kagent-adk/src/kagent/adk/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_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()


Expand All @@ -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):
Expand Down
Loading