Skip to content

Commit 5fd0c92

Browse files
committed
feat: Add A2A authentication middleware with TIP token propagation support
- Add A2AAuthMiddleware for extracting auth tokens from requests - Support both Authorization header and query string authentication methods - Implement TIP (Trust Identity Propagation) token exchange via IdentityClient - Add VeCredentialService integration for credential storage and retrieval - Support workload token generation and propagation in request scope - Add RemoteVeAgent with automatic credential injection from context - Enhance credential service with ADK BaseCredentialService interface - Add comprehensive test coverage for middleware and credential service Key features: * Extract JWT tokens and delegation chains from incoming requests * Exchange TIP tokens for workload access tokens using IdentityClient * Store credentials in credential service with app_name and user_id scoping * Inject authentication tokens into remote agent HTTP clients at runtime * Support multiple authentication methods (header/querystring) This enables secure A2A communication with automatic credential propagation across the Volcengine Agent runtimes.
1 parent 981fe54 commit 5fd0c92

File tree

15 files changed

+1079
-236
lines changed

15 files changed

+1079
-236
lines changed

veadk/a2a/remote_ve_agent.py

Lines changed: 87 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414

1515
import json
1616
from typing import AsyncGenerator, Literal, Optional
17-
from pydantic import Field
1817

1918
from a2a.client.base_client import BaseClient
2019
import httpx
2120
import requests
22-
from a2a.client.auth import CredentialService
2321
from a2a.types import AgentCard
2422
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
2523

24+
from veadk.integrations.ve_identity.utils import generate_headers
25+
from veadk.utils.auth import VE_TIP_TOKEN_CREDENTIAL_KEY, VE_TIP_TOKEN_HEADER
2626
from veadk.utils.logger import get_logger
2727
from google.adk.utils.context_utils import Aclosing
2828
from google.adk.events.event import Event
@@ -67,18 +67,20 @@ class RemoteVeAgent(RemoteA2aAgent):
6767
with a configured `base_url` is provided. If both are given, they must
6868
not conflict.
6969
auth_token (Optional[str]):
70-
Optional authentication token used for secure access. If not provided,
71-
the agent will be accessed without authentication.
70+
Optional authentication token used for secure access during initialization.
71+
If not provided, the agent will be accessed without authentication.
72+
Note: For runtime authentication, use the credential service in InvocationContext.
7273
auth_method (Literal["header", "querystring"] | None):
73-
The method of attaching the authentication token.
74-
- `"header"`: Token is passed via HTTP `Authorization` header.
75-
- `"querystring"`: Token is passed as a query parameter.
74+
The method of attaching the authentication token at runtime.
75+
- `"header"`: Token is retrieved from credential service and passed via HTTP `Authorization` header.
76+
- `"querystring"`: Token is retrieved from credential service and passed as a query parameter.
77+
- `None`: No runtime authentication injection (default).
78+
The credential is loaded from `InvocationContext.credential_service` using the
79+
app_name and user_id from the context.
7680
httpx_client (Optional[httpx.AsyncClient]):
7781
An optional, pre-configured `httpx.AsyncClient` to use for communication.
7882
This allows for client sharing and advanced configurations (e.g., proxies).
7983
If its `base_url` is set, it will be used as the agent's location.
80-
credential_service (Optional[CredentialService]):
81-
Optional credential service for injecting auth token.
8284
8385
Raises:
8486
ValueError:
@@ -90,52 +92,43 @@ class RemoteVeAgent(RemoteA2aAgent):
9092
9193
Examples:
9294
```python
93-
# Example 1: Connect using a URL
95+
# Example 1: Connect using a URL (no authentication)
9496
agent = RemoteVeAgent(
9597
name="public_agent",
9698
url="https://vefaas.example.com/agents/public"
9799
)
98100
99-
# Example 2: Using Bearer token in header
101+
# Example 2: Using static Bearer token in header for initialization
100102
agent = RemoteVeAgent(
101103
name="secured_agent",
102104
url="https://vefaas.example.com/agents/secure",
103105
auth_token="my_secret_token",
104106
auth_method="header"
105107
)
106108
107-
# Example 3: Using a pre-configured httpx_client
109+
# Example 3: Using runtime authentication with credential service
110+
# The auth token will be automatically injected from InvocationContext.credential_service
111+
agent = RemoteVeAgent(
112+
name="dynamic_auth_agent",
113+
url="https://vefaas.example.com/agents/secure",
114+
auth_method="header" # Will load credential at runtime
115+
)
116+
117+
# Example 4: Using a pre-configured httpx_client
108118
import httpx
109119
client = httpx.AsyncClient(
110120
base_url="https://vefaas.example.com/agents/query",
111121
timeout=600
112122
)
113123
agent = RemoteVeAgent(
114124
name="query_agent",
115-
auth_token="my_secret_token",
116-
auth_method="querystring",
125+
auth_method="querystring", # Will load credential at runtime
117126
httpx_client=client
118127
)
119-
120-
# Example 4: Using a credential service
121-
from veadk.a2a.credentials import VeCredentialStore
122-
credential_service = VeCredentialStore()
123-
credential_service.set_credentials(
124-
session_id="session_123",
125-
security_scheme_name="inbound_auth",
126-
credential="bearer_token_xyz"
127-
)
128-
agent = RemoteVeAgent(
129-
name="secured_agent",
130-
url="https://vefaas.example.com/agents/secure",
131-
credential_service=credential_service
132-
)
133128
```
134129
"""
135130

136-
credential_service: Optional[CredentialService] = Field(
137-
None, description="Optional credential service for injecting auth token."
138-
)
131+
auth_method: Literal["header", "querystring"] | None = None
139132

140133
def __init__(
141134
self,
@@ -144,7 +137,6 @@ def __init__(
144137
auth_token: Optional[str] = None,
145138
auth_method: Literal["header", "querystring"] | None = None,
146139
httpx_client: Optional[httpx.AsyncClient] = None,
147-
credential_service: Optional[CredentialService] = None,
148140
):
149141
# Determine the effective URL for the agent and handle conflicts.
150142
effective_url = url
@@ -225,9 +217,9 @@ def __init__(
225217
if not client_was_provided:
226218
self._httpx_client_needs_cleanup = True
227219

228-
# Set credential service if provided
229-
if credential_service:
230-
self.credential_service = credential_service
220+
# Set auth_method if provided
221+
if auth_method:
222+
self.auth_method = auth_method
231223

232224
async def _run_async_impl(
233225
self, ctx: InvocationContext
@@ -268,14 +260,17 @@ async def _inject_auth_token(self, ctx: InvocationContext) -> None:
268260
"""Inject authentication token from credential service into the HTTP client.
269261
270262
This method retrieves the authentication token from the credential service
271-
using the session ID and updates the HTTP client headers to include the
272-
Bearer token for subsequent requests.
263+
in the InvocationContext and updates the HTTP client headers or query params
264+
based on the configured auth_method.
273265
274266
Args:
275-
ctx: Invocation context containing session information
267+
ctx: Invocation context containing credential service and user information
276268
"""
277-
# Skip if no credential service configured
278-
if not self.credential_service:
269+
# Skip if no credential service in context
270+
if not ctx.credential_service:
271+
logger.debug(
272+
"No credential service in InvocationContext, skipping auth token injection"
273+
)
279274
return
280275

281276
# Skip if client is not initialized or not a BaseClient
@@ -302,30 +297,64 @@ async def _inject_auth_token(self, ctx: InvocationContext) -> None:
302297
return
303298

304299
try:
305-
from a2a.client import ClientCallContext
306-
307-
# Get credentials from credential service using session ID
308-
token = await self.credential_service.get_credentials(
309-
security_scheme_name="inbound_auth",
310-
context=ClientCallContext(
311-
state={"userId": ctx.user_id, "sessionId": ctx.session.id}
312-
),
300+
from veadk.utils.auth import build_auth_config
301+
from google.adk.agents.callback_context import CallbackContext
302+
303+
# Inject TIP token via header
304+
workload_auth_config = build_auth_config(
305+
auth_method="apikey",
306+
credential_key=VE_TIP_TOKEN_CREDENTIAL_KEY,
307+
header_name=VE_TIP_TOKEN_HEADER,
313308
)
314309

315-
if not token:
316-
return
310+
tip_credential = await ctx.credential_service.load_credential(
311+
auth_config=workload_auth_config,
312+
callback_context=CallbackContext(ctx),
313+
)
317314

318-
# Add "Bearer " prefix if not already present
319-
if not token.startswith("Bearer "):
320-
token = f"Bearer {token}"
315+
if tip_credential:
316+
self._a2a_client._transport.httpx_client.headers.update(
317+
{VE_TIP_TOKEN_HEADER: tip_credential.api_key}
318+
)
319+
logger.debug(
320+
f"Injected TIP token via header for app={ctx.app_name}, user={ctx.user_id}"
321+
)
321322

322-
# Update HTTP client headers
323-
self._a2a_client._transport.httpx_client.headers.update(
324-
{"Authorization": token}
323+
# Build auth config based on auth_method
324+
auth_config = build_auth_config(
325+
credential_key="inbound_auth",
326+
auth_method=self.auth_method or "header",
327+
header_scheme="bearer",
325328
)
326-
logger.debug(
327-
f"Injected auth token for user {ctx.user_id} and session {ctx.session.id}"
329+
330+
# Load credential from credential service
331+
credential = await ctx.credential_service.load_credential(
332+
auth_config=auth_config,
333+
callback_context=CallbackContext(ctx),
328334
)
329335

336+
if not credential:
337+
logger.debug(
338+
f"No credential loaded, skipping auth token injection for app={ctx.app_name}, user={ctx.user_id}"
339+
)
340+
return
341+
342+
# Inject credential based on auth_method
343+
if self.auth_method == "querystring":
344+
# Extract API key
345+
api_key = credential.api_key
346+
new_params = dict(self._a2a_client._transport.httpx_client.params)
347+
new_params.update({"token": api_key})
348+
self._a2a_client._transport.httpx_client.params = new_params
349+
logger.debug(
350+
f"Injected auth token via querystring for app={ctx.app_name}, user={ctx.user_id}"
351+
)
352+
else:
353+
if headers := generate_headers(credential):
354+
self._a2a_client._transport.httpx_client.headers.update(headers)
355+
logger.debug(
356+
f"Injected auth token via header for app={ctx.app_name}, user={ctx.user_id}"
357+
)
358+
330359
except Exception as e:
331360
logger.warning(f"Failed to inject auth token: {e}", exc_info=True)

veadk/a2a/ve_a2a_server.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,19 @@
2323
from veadk.memory.short_term_memory import ShortTermMemory
2424

2525
from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
26+
from google.adk.auth.credential_service.base_credential_service import (
27+
BaseCredentialService,
28+
)
2629

2730

2831
class VeA2AServer:
2932
def __init__(
30-
self, agent: Agent, url: str, app_name: str, short_term_memory: ShortTermMemory
33+
self,
34+
agent: Agent,
35+
url: str,
36+
app_name: str,
37+
short_term_memory: ShortTermMemory,
38+
credential_service: BaseCredentialService,
3139
):
3240
self.agent_card = get_agent_card(agent, url)
3341

@@ -36,7 +44,8 @@ def __init__(
3644
agent=agent,
3745
app_name=app_name,
3846
short_term_memory=short_term_memory,
39-
),
47+
credential_service=credential_service,
48+
)
4049
)
4150

4251
self.task_store = InMemoryTaskStore()
@@ -56,7 +65,11 @@ def build(self) -> FastAPI:
5665

5766

5867
def init_app(
59-
server_url: str, app_name: str, agent: Agent, short_term_memory: ShortTermMemory
68+
server_url: str,
69+
app_name: str,
70+
agent: Agent,
71+
short_term_memory: ShortTermMemory,
72+
credential_service: BaseCredentialService,
6073
) -> FastAPI:
6174
"""Init the fastapi application in terms of VeADK agent.
6275
@@ -75,5 +88,6 @@ def init_app(
7588
url=server_url,
7689
app_name=app_name,
7790
short_term_memory=short_term_memory,
91+
credential_service=credential_service,
7892
)
7993
return server.build()

0 commit comments

Comments
 (0)