Skip to content

Commit e0a85db

Browse files
committed
fix(app): add bearer token authentication support for Bedrock
When AWS_BEARER_TOKEN_BEDROCK env var is set, the Bedrock client now uses UNSIGNED signature with a before-sign event handler to inject the Bearer token, instead of relying on IAM credential chain. Signed-off-by: Kexin Su <kexin.su823@gmail.com>
1 parent fb611b0 commit e0a85db

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

python/packages/kagent-adk/src/kagent/adk/models/_bedrock.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
33
Uses boto3's Converse API which provides a consistent interface across all
44
Bedrock-supported models (Anthropic, Meta, Mistral, Amazon, Cohere, etc.).
5-
Authenticates via the standard AWS credential chain (env vars, IAM role, etc.).
5+
6+
Supports two authentication methods:
7+
- **Bearer token**: Set ``AWS_BEARER_TOKEN_BEDROCK`` env var (API key auth).
8+
- **IAM credentials**: Standard AWS credential chain (env vars, IAM role, etc.).
69
"""
710

811
from __future__ import annotations
@@ -16,6 +19,8 @@
1619
from typing import TYPE_CHECKING, Any, AsyncGenerator, Optional
1720

1821
import boto3
22+
from botocore import UNSIGNED
23+
from botocore.config import Config
1924
from google.adk.models import BaseLlm
2025
from google.adk.models.llm_response import LlmResponse
2126
from google.genai import types
@@ -54,12 +59,33 @@ def _sanitize_tool_id(tool_id: str, id_map: dict[str, str], counter: list[int])
5459
return sanitized
5560

5661

62+
def _inject_bearer_token(token: str, request, **kwargs):
63+
"""Event handler that injects a Bearer token into the Authorization header.
64+
65+
Registered on the ``before-sign`` event so it runs after boto3 builds the
66+
request but before SigV4 signing (which is disabled via UNSIGNED).
67+
"""
68+
request.headers["Authorization"] = f"Bearer {token}"
69+
70+
5771
def _get_bedrock_client(extra_headers: Optional[dict[str, str]] = None):
5872
region = os.environ.get("AWS_DEFAULT_REGION") or os.environ.get("AWS_REGION") or "us-east-1"
5973
kwargs: dict[str, Any] = {"region_name": region}
6074
if extra_headers:
6175
# boto3 doesn't support custom headers natively; log and ignore
6276
logger.warning("extra_headers are not supported for Bedrock models and will be ignored.")
77+
78+
bearer_token = os.environ.get("AWS_BEARER_TOKEN_BEDROCK", "").strip()
79+
if bearer_token:
80+
logger.info("Using bearer token authentication for Bedrock")
81+
kwargs["config"] = Config(signature_version=UNSIGNED)
82+
client = boto3.client("bedrock-runtime", **kwargs)
83+
client.meta.events.register(
84+
"before-sign.bedrock-runtime.*",
85+
lambda request, **kw: _inject_bearer_token(bearer_token, request, **kw),
86+
)
87+
return client
88+
6389
return boto3.client("bedrock-runtime", **kwargs)
6490

6591

python/packages/kagent-adk/tests/unittests/models/test_bedrock.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77

8-
from kagent.adk.models._bedrock import KAgentBedrockLlm, _get_bedrock_client
8+
from kagent.adk.models._bedrock import KAgentBedrockLlm, _get_bedrock_client, _inject_bearer_token
99

1010

1111
class TestGetBedrockClient:
@@ -30,6 +30,53 @@ def test_defaults_to_us_east_1(self):
3030
_get_bedrock_client()
3131
assert mock_boto.call_args.kwargs["region_name"] == "us-east-1"
3232

33+
def test_bearer_token_uses_unsigned_config(self):
34+
"""When AWS_BEARER_TOKEN_BEDROCK is set, client uses UNSIGNED signature."""
35+
from botocore import UNSIGNED
36+
37+
env = {k: v for k, v in __import__("os").environ.items() if k not in ("AWS_DEFAULT_REGION", "AWS_REGION")}
38+
env["AWS_BEARER_TOKEN_BEDROCK"] = "test-token-123"
39+
with mock.patch.dict("os.environ", env, clear=True):
40+
with mock.patch("kagent.adk.models._bedrock.boto3.client") as mock_boto:
41+
mock_client = mock.MagicMock()
42+
mock_boto.return_value = mock_client
43+
_get_bedrock_client()
44+
config = mock_boto.call_args.kwargs["config"]
45+
assert config.signature_version == UNSIGNED
46+
47+
def test_bearer_token_registers_event_handler(self):
48+
"""When AWS_BEARER_TOKEN_BEDROCK is set, a before-sign handler is registered."""
49+
env = {k: v for k, v in __import__("os").environ.items() if k not in ("AWS_DEFAULT_REGION", "AWS_REGION")}
50+
env["AWS_BEARER_TOKEN_BEDROCK"] = "test-token-123"
51+
with mock.patch.dict("os.environ", env, clear=True):
52+
with mock.patch("kagent.adk.models._bedrock.boto3.client") as mock_boto:
53+
mock_client = mock.MagicMock()
54+
mock_boto.return_value = mock_client
55+
_get_bedrock_client()
56+
mock_client.meta.events.register.assert_called_once()
57+
call_args = mock_client.meta.events.register.call_args
58+
assert call_args[0][0] == "before-sign.bedrock-runtime.*"
59+
60+
def test_no_bearer_token_uses_standard_auth(self):
61+
"""When AWS_BEARER_TOKEN_BEDROCK is not set, standard credential chain is used."""
62+
env = {k: v for k, v in __import__("os").environ.items() if k not in ("AWS_DEFAULT_REGION", "AWS_REGION", "AWS_BEARER_TOKEN_BEDROCK")}
63+
with mock.patch.dict("os.environ", env, clear=True):
64+
with mock.patch("kagent.adk.models._bedrock.boto3.client") as mock_boto:
65+
mock_client = mock.MagicMock()
66+
mock_boto.return_value = mock_client
67+
_get_bedrock_client()
68+
assert "config" not in mock_boto.call_args.kwargs
69+
mock_client.meta.events.register.assert_not_called()
70+
71+
72+
class TestInjectBearerToken:
73+
def test_injects_authorization_header(self):
74+
"""_inject_bearer_token sets the correct Authorization header."""
75+
mock_request = mock.MagicMock()
76+
mock_request.headers = {}
77+
_inject_bearer_token("my-secret-token", mock_request)
78+
assert mock_request.headers["Authorization"] == "Bearer my-secret-token"
79+
3380

3481
class TestKAgentBedrockLlm:
3582
def test_default_construction(self):

0 commit comments

Comments
 (0)